More Polish (#2394)

* Remove Import/Export buttons from dashboards index

* Reduce max label chars to 50

* Change format of dashboard "modified" column to relative time

Absolute time is included as a tooltip

* Redistribute column widths in dashboards table

Optimzied for long names

* Improve component spacer

Now supports more fine grained "stretch to fit" controls

* Introduce editable description component

Intended mainly for use in index list views

* Allow dashboard descriptions to be editable in place

* Give modified column a tad more space

* Standardize empty states of tabs in organization view

* Update test
pull/10616/head
alexpaxton 2019-01-09 17:15:59 -08:00 committed by GitHub
parent 7875051ace
commit 7cf643d1d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 350 additions and 75 deletions

View File

@ -6,6 +6,14 @@
.component-spacer { .component-spacer {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
&.component-spacer--stretch-w {
width: 100%;
}
&.component-spacer--stretch-h {
height: 100%;
}
} }
.component-spacer--left { .component-spacer--left {
@ -24,10 +32,6 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
&.component-spacer--stretch {
width: 100%;
}
&.component-spacer--left > * { &.component-spacer--left > * {
margin-right: $ix-marg-a; margin-right: $ix-marg-a;
@ -53,10 +57,6 @@
.component-spacer--vertical { .component-spacer--vertical {
flex-direction: column; flex-direction: column;
&.component-spacer--stretch {
height: 100%;
}
&.component-spacer--left { &.component-spacer--left {
align-items: flex-start; align-items: flex-start;
} }

View File

@ -9,14 +9,16 @@ interface Props {
children: JSX.Element | JSX.Element[] children: JSX.Element | JSX.Element[]
align: Alignment align: Alignment
stackChildren?: Stack stackChildren?: Stack
stretchToFit?: boolean stretchToFitWidth?: boolean
stretchToFitHeight?: boolean
} }
const ComponentSpacer: SFC<Props> = ({ const ComponentSpacer: SFC<Props> = ({
children, children,
align, align,
stackChildren = Stack.Columns, stackChildren = Stack.Columns,
stretchToFit = false, stretchToFitWidth = false,
stretchToFitHeight = false,
}) => ( }) => (
<div <div
className={classnames('component-spacer', { className={classnames('component-spacer', {
@ -25,7 +27,8 @@ const ComponentSpacer: SFC<Props> = ({
'component-spacer--right': align === Alignment.Right, 'component-spacer--right': align === Alignment.Right,
'component-spacer--horizontal': stackChildren === Stack.Columns, 'component-spacer--horizontal': stackChildren === Stack.Columns,
'component-spacer--vertical': stackChildren === Stack.Rows, 'component-spacer--vertical': stackChildren === Stack.Rows,
'component-spacer--stretch': stretchToFit, 'component-spacer--stretch-w': stretchToFitWidth,
'component-spacer--stretch-h': stretchToFitHeight,
})} })}
> >
{children} {children}

View File

@ -200,18 +200,6 @@
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
*/ */
.index-list--description {
user-select: none;
font-size: 12px;
color: $g12-forge;
white-space: nowrap;
&.untitled {
color: $g9-mountain;
font-style: italic;
}
}
.index-list--labels { .index-list--labels {
margin-left: $ix-marg-b; margin-left: $ix-marg-b;
} }

View File

@ -20,6 +20,7 @@ interface Props {
onCloneDashboard: (dashboard: Dashboard) => void onCloneDashboard: (dashboard: Dashboard) => void
onExportDashboard: (dashboard: Dashboard) => void onExportDashboard: (dashboard: Dashboard) => void
onDeleteDashboard: (dashboard: Dashboard) => void onDeleteDashboard: (dashboard: Dashboard) => void
onUpdateDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void notify: (message: Notification) => void
searchTerm: string searchTerm: string
} }
@ -34,6 +35,7 @@ export default class DashboardsIndexContents extends Component<Props> {
onCreateDashboard, onCreateDashboard,
defaultDashboardLink, defaultDashboardLink,
onSetDefaultDashboard, onSetDefaultDashboard,
onUpdateDashboard,
searchTerm, searchTerm,
} = this.props } = this.props
@ -48,6 +50,7 @@ export default class DashboardsIndexContents extends Component<Props> {
onExportDashboard={onExportDashboard} onExportDashboard={onExportDashboard}
defaultDashboardLink={defaultDashboardLink} defaultDashboardLink={defaultDashboardLink}
onSetDefaultDashboard={onSetDefaultDashboard} onSetDefaultDashboard={onSetDefaultDashboard}
onUpdateDashboard={onUpdateDashboard}
/> />
</div> </div>
) )

View File

@ -28,6 +28,7 @@ import {
getDashboardsAsync, getDashboardsAsync,
importDashboardAsync, importDashboardAsync,
deleteDashboardAsync, deleteDashboardAsync,
updateDashboardAsync,
} from 'src/dashboards/actions/v2' } from 'src/dashboards/actions/v2'
import {setDefaultDashboard} from 'src/shared/actions/links' import {setDefaultDashboard} from 'src/shared/actions/links'
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges' import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
@ -59,6 +60,7 @@ interface Props {
handleGetDashboards: typeof getDashboardsAsync handleGetDashboards: typeof getDashboardsAsync
handleDeleteDashboard: typeof deleteDashboardAsync handleDeleteDashboard: typeof deleteDashboardAsync
handleImportDashboard: typeof importDashboardAsync handleImportDashboard: typeof importDashboardAsync
handleUpdateDashboard: typeof updateDashboardAsync
notify: (message: Notification) => void notify: (message: Notification) => void
retainRangesDashTimeV1: (dashboardIDs: string[]) => void retainRangesDashTimeV1: (dashboardIDs: string[]) => void
dashboards: Dashboard[] dashboards: Dashboard[]
@ -88,7 +90,7 @@ class DashboardIndex extends PureComponent<Props, State> {
} }
public render() { public render() {
const {dashboards, notify, links} = this.props const {dashboards, notify, links, handleUpdateDashboard} = this.props
const {searchTerm} = this.state const {searchTerm} = this.state
return ( return (
@ -103,12 +105,6 @@ class DashboardIndex extends PureComponent<Props, State> {
placeholderText="Filter dashboards by name..." placeholderText="Filter dashboards by name..."
onSearch={this.filterDashboards} onSearch={this.filterDashboards}
/> />
<Button
onClick={this.handleToggleOverlay}
icon={IconFont.Import}
text="Import"
titleText="Import a dashboard from a file"
/>
<Button <Button
color={ComponentColor.Primary} color={ComponentColor.Primary}
onClick={this.handleCreateDashboard} onClick={this.handleCreateDashboard}
@ -127,6 +123,7 @@ class DashboardIndex extends PureComponent<Props, State> {
onCreateDashboard={this.handleCreateDashboard} onCreateDashboard={this.handleCreateDashboard}
onCloneDashboard={this.handleCloneDashboard} onCloneDashboard={this.handleCloneDashboard}
onExportDashboard={this.handleExportDashboard} onExportDashboard={this.handleExportDashboard}
onUpdateDashboard={handleUpdateDashboard}
notify={notify} notify={notify}
searchTerm={searchTerm} searchTerm={searchTerm}
/> />
@ -271,6 +268,7 @@ const mdtp = {
handleGetDashboards: getDashboardsAsync, handleGetDashboards: getDashboardsAsync,
handleDeleteDashboard: deleteDashboardAsync, handleDeleteDashboard: deleteDashboardAsync,
handleImportDashboard: importDashboardAsync, handleImportDashboard: importDashboardAsync,
handleUpdateDashboard: updateDashboardAsync,
retainRangesDashTimeV1: retainRangesDashTimeV1Action, retainRangesDashTimeV1: retainRangesDashTimeV1Action,
} }

View File

@ -28,6 +28,7 @@ interface Props {
onCreateDashboard: () => void onCreateDashboard: () => void
onCloneDashboard: (dashboard: Dashboard) => void onCloneDashboard: (dashboard: Dashboard) => void
onExportDashboard: (dashboard: Dashboard) => void onExportDashboard: (dashboard: Dashboard) => void
onUpdateDashboard: (dashboard: Dashboard) => void
onSetDefaultDashboard: (dashboardLink: string) => void onSetDefaultDashboard: (dashboardLink: string) => void
} }
@ -62,26 +63,26 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
columnName={headerKeys[0]} columnName={headerKeys[0]}
sortKey={headerKeys[0]} sortKey={headerKeys[0]}
sort={sortKey === headerKeys[0] ? sortDirection : Sort.None} sort={sortKey === headerKeys[0] ? sortDirection : Sort.None}
width="50%" width="62%"
onClick={this.handleClickColumn} onClick={this.handleClickColumn}
/> />
<IndexList.HeaderCell <IndexList.HeaderCell
columnName={headerKeys[1]} columnName={headerKeys[1]}
sortKey={headerKeys[1]} sortKey={headerKeys[1]}
sort={sortKey === headerKeys[1] ? sortDirection : Sort.None} sort={sortKey === headerKeys[1] ? sortDirection : Sort.None}
width="10%" width="17%"
onClick={this.handleClickColumn} onClick={this.handleClickColumn}
/> />
<IndexList.HeaderCell <IndexList.HeaderCell
columnName={headerKeys[2]} columnName={headerKeys[2]}
sortKey={headerKeys[2]} sortKey={headerKeys[2]}
sort={sortKey === headerKeys[2] ? sortDirection : Sort.None} sort={sortKey === headerKeys[2] ? sortDirection : Sort.None}
width="30%" width="11%"
onClick={this.handleClickColumn} onClick={this.handleClickColumn}
/> />
<IndexList.HeaderCell <IndexList.HeaderCell
columnName="" columnName=""
width="20%" width="10%"
alignment={Alignment.Right} alignment={Alignment.Right}
/> />
</IndexList.Header> </IndexList.Header>
@ -102,6 +103,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
onExportDashboard, onExportDashboard,
onCloneDashboard, onCloneDashboard,
onDeleteDashboard, onDeleteDashboard,
onUpdateDashboard,
} = this.props } = this.props
const {sortKey, sortDirection} = this.state const {sortKey, sortDirection} = this.state
@ -119,6 +121,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
onCloneDashboard={onCloneDashboard} onCloneDashboard={onCloneDashboard}
onExportDashboard={onExportDashboard} onExportDashboard={onExportDashboard}
onDeleteDashboard={onDeleteDashboard} onDeleteDashboard={onDeleteDashboard}
onUpdateDashboard={onUpdateDashboard}
/> />
)} )}
</SortingHat> </SortingHat>

View File

@ -15,6 +15,7 @@ const setup = (override = {}) => {
onDeleteDashboard: jest.fn(), onDeleteDashboard: jest.fn(),
onCloneDashboard: jest.fn(), onCloneDashboard: jest.fn(),
onExportDashboard: jest.fn(), onExportDashboard: jest.fn(),
onUpdateDashboard: jest.fn(),
...override, ...override,
} }

View File

@ -13,6 +13,7 @@ import {
Stack, Stack,
Label, Label,
} from 'src/clockface' } from 'src/clockface'
import EditableDescription from 'src/shared/components/editable_description/EditableDescription'
// Types // Types
import {Dashboard} from 'src/types/v2' import {Dashboard} from 'src/types/v2'
@ -30,6 +31,7 @@ interface Props {
onDeleteDashboard: (dashboard: Dashboard) => void onDeleteDashboard: (dashboard: Dashboard) => void
onCloneDashboard: (dashboard: Dashboard) => void onCloneDashboard: (dashboard: Dashboard) => void
onExportDashboard: (dashboard: Dashboard) => void onExportDashboard: (dashboard: Dashboard) => void
onUpdateDashboard: (dashboard: Dashboard) => void
} }
export default class DashboardsIndexTableRow extends PureComponent<Props> { export default class DashboardsIndexTableRow extends PureComponent<Props> {
@ -40,7 +42,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
return ( return (
<IndexList.Row key={`dashboard-id--${id}`} disabled={false}> <IndexList.Row key={`dashboard-id--${id}`} disabled={false}>
<IndexList.Cell> <IndexList.Cell>
<ComponentSpacer stackChildren={Stack.Rows} align={Alignment.Left}> <ComponentSpacer
stackChildren={Stack.Rows}
align={Alignment.Left}
stretchToFitWidth={true}
>
<ComponentSpacer <ComponentSpacer
stackChildren={Stack.Columns} stackChildren={Stack.Columns}
align={Alignment.Left} align={Alignment.Left}
@ -50,21 +56,17 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
</Link> </Link>
{this.labels} {this.labels}
</ComponentSpacer> </ComponentSpacer>
{this.description} <EditableDescription
description={dashboard.description}
placeholder={`Describe ${dashboard.name}`}
onUpdate={this.handleUpdateDescription}
/>
</ComponentSpacer> </ComponentSpacer>
</IndexList.Cell> </IndexList.Cell>
<IndexList.Cell>Owner does not come back from API</IndexList.Cell> <IndexList.Cell>Owner does not come back from API</IndexList.Cell>
<IndexList.Cell> {this.lastModifiedCell}
{moment(dashboard.meta.updatedAt).format(UPDATED_AT_TIME_FORMAT)}
</IndexList.Cell>
<IndexList.Cell alignment={Alignment.Right} revealOnHover={true}> <IndexList.Cell alignment={Alignment.Right} revealOnHover={true}>
<ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}> <ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}>
<Button
size={ComponentSize.ExtraSmall}
text="Export"
icon={IconFont.Export}
onClick={this.handleExport}
/>
<Button <Button
size={ComponentSize.ExtraSmall} size={ComponentSize.ExtraSmall}
text="Clone" text="Clone"
@ -106,6 +108,21 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
) )
} }
private get lastModifiedCell(): JSX.Element {
const {dashboard} = this.props
const relativeTimestamp = moment(dashboard.meta.updatedAt).fromNow()
const absoluteTimestamp = moment(dashboard.meta.updatedAt).format(
UPDATED_AT_TIME_FORMAT
)
return (
<IndexList.Cell>
<span title={absoluteTimestamp}>{relativeTimestamp}</span>
</IndexList.Cell>
)
}
private get name(): string { private get name(): string {
const {dashboard} = this.props const {dashboard} = this.props
@ -120,23 +137,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
} }
} }
private get description(): JSX.Element { private handleUpdateDescription = (description: string): void => {
const {dashboard} = this.props const {onUpdateDashboard} = this.props
const dashboard = {...this.props.dashboard, description}
if (dashboard.description) { onUpdateDashboard(dashboard)
return (
<div className="index-list--description">{dashboard.description}</div>
)
}
return (
<div className="index-list--description untitled">No description</div>
)
}
private handleExport = () => {
const {onExportDashboard, dashboard} = this.props
onExportDashboard(dashboard)
} }
private handleClone = () => { private handleClone = () => {

View File

@ -12,6 +12,7 @@ interface Props {
onDeleteDashboard: (dashboard: Dashboard) => void onDeleteDashboard: (dashboard: Dashboard) => void
onCloneDashboard: (dashboard: Dashboard) => void onCloneDashboard: (dashboard: Dashboard) => void
onExportDashboard: (dashboard: Dashboard) => void onExportDashboard: (dashboard: Dashboard) => void
onUpdateDashboard: (dashboard: Dashboard) => void
} }
export default class DashboardsIndexTableRows extends PureComponent<Props> { export default class DashboardsIndexTableRows extends PureComponent<Props> {
@ -21,6 +22,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
onExportDashboard, onExportDashboard,
onCloneDashboard, onCloneDashboard,
onDeleteDashboard, onDeleteDashboard,
onUpdateDashboard,
} = this.props } = this.props
return dashboards.map(d => ( return dashboards.map(d => (
@ -30,6 +32,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
onExportDashboard={onExportDashboard} onExportDashboard={onExportDashboard}
onCloneDashboard={onCloneDashboard} onCloneDashboard={onCloneDashboard}
onDeleteDashboard={onDeleteDashboard} onDeleteDashboard={onDeleteDashboard}
onUpdateDashboard={onUpdateDashboard}
/> />
)) ))
} }

View File

@ -159,19 +159,29 @@ export default class Buckets extends PureComponent<Props, State> {
} }
private get emptyState(): JSX.Element { private get emptyState(): JSX.Element {
const {org} = this.props
const {searchTerm} = this.state const {searchTerm} = this.state
if (_.isEmpty(searchTerm)) { if (_.isEmpty(searchTerm)) {
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Medium}>
<EmptyState.Text text="Oh noes I dun see na buckets" /> <EmptyState.Text
text={`${org.name} does not own any Buckets , why not create one?`}
highlightWords={['Buckets']}
/>
<Button
text="Create Bucket"
icon={IconFont.Plus}
color={ComponentColor.Primary}
onClick={this.handleOpenModal}
/>
</EmptyState> </EmptyState>
) )
} }
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Medium}>
<EmptyState.Text text="No buckets match your query" /> <EmptyState.Text text="No Buckets match your query" />
</EmptyState> </EmptyState>
) )
} }

View File

@ -1,5 +1,6 @@
// Libraries // Libraries
import React, {PureComponent, ChangeEvent} from 'react' import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
// Components // Components
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader' import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
@ -15,6 +16,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
dashboards: Dashboard[] dashboards: Dashboard[]
orgName: string
} }
interface State { interface State {
@ -66,9 +68,23 @@ export default class Dashboards extends PureComponent<Props, State> {
} }
private get emptyState(): JSX.Element { private get emptyState(): JSX.Element {
const {orgName} = this.props
const {searchTerm} = this.state
if (_.isEmpty(searchTerm)) {
return (
<EmptyState size={ComponentSize.Medium}>
<EmptyState.Text
text={`${orgName} does not own any Dashboards , why not create one?`}
highlightWords={['Dashboards']}
/>
</EmptyState>
)
}
return ( return (
<EmptyState size={ComponentSize.Large}> <EmptyState size={ComponentSize.Medium}>
<EmptyState.Text text="Oh noes I dun see na dashbardsss" /> <EmptyState.Text text="No Dashboards match your query" />
</EmptyState> </EmptyState>
) )
} }

View File

@ -26,7 +26,7 @@ import {
HEX_CODE_CHAR_LENGTH, HEX_CODE_CHAR_LENGTH,
INPUT_ERROR_COLOR, INPUT_ERROR_COLOR,
} from 'src/organizations/constants/LabelColors' } from 'src/organizations/constants/LabelColors'
const MAX_LABEL_CHARS = 75 const MAX_LABEL_CHARS = 50
// Utils // Utils
import {validateHexCode} from 'src/organizations/utils/labels' import {validateHexCode} from 'src/organizations/utils/labels'

View File

@ -1,5 +1,6 @@
// Libraries // Libraries
import React, {PureComponent, ChangeEvent} from 'react' import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
// Components // Components
import {ComponentSize, EmptyState, IconFont, Input} from 'src/clockface' import {ComponentSize, EmptyState, IconFont, Input} from 'src/clockface'
@ -15,6 +16,7 @@ import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader
interface Props { interface Props {
members: ResourceOwner[] members: ResourceOwner[]
orgName: string
} }
interface State { interface State {
@ -60,9 +62,23 @@ export default class Members extends PureComponent<Props, State> {
} }
private get emptyState(): JSX.Element { private get emptyState(): JSX.Element {
const {orgName} = this.props
const {searchTerm} = this.state
if (_.isEmpty(searchTerm)) {
return (
<EmptyState size={ComponentSize.Medium}>
<EmptyState.Text
text={`${orgName} doesn't have any Members , why not invite some?`}
highlightWords={['Members']}
/>
</EmptyState>
)
}
return ( return (
<EmptyState size={ComponentSize.Medium}> <EmptyState size={ComponentSize.Medium}>
<EmptyState.Text text="This org has been abandoned" /> <EmptyState.Text text="No Members match your query" />
</EmptyState> </EmptyState>
) )
} }

View File

@ -1,5 +1,6 @@
// Libraries // Libraries
import React, {PureComponent, ChangeEvent} from 'react' import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
// Components // Components
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader' import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
@ -12,6 +13,7 @@ import {Task} from 'src/api'
interface Props { interface Props {
tasks: Task[] tasks: Task[]
orgName: string
} }
interface State { interface State {
@ -62,9 +64,23 @@ export default class Tasks extends PureComponent<Props, State> {
} }
private get emptyState(): JSX.Element { private get emptyState(): JSX.Element {
const {orgName} = this.props
const {searchTerm} = this.state
if (_.isEmpty(searchTerm)) {
return (
<EmptyState size={ComponentSize.Medium}>
<EmptyState.Text
text={`${orgName} does not own any Tasks , why not create one?`}
highlightWords={'Tasks'}
/>
</EmptyState>
)
}
return ( return (
<EmptyState size={ComponentSize.Medium}> <EmptyState size={ComponentSize.Medium}>
<EmptyState.Text text="I see nay a task" /> <EmptyState.Text text="No Tasks match your query" />
</EmptyState> </EmptyState>
) )
} }

View File

@ -75,7 +75,7 @@ class OrganizationView extends PureComponent<Props> {
> >
{(members, loading) => ( {(members, loading) => (
<Spinner loading={loading}> <Spinner loading={loading}>
<Members members={members} /> <Members members={members} orgName={org.name} />
</Spinner> </Spinner>
)} )}
</GetOrgResources> </GetOrgResources>
@ -107,7 +107,7 @@ class OrganizationView extends PureComponent<Props> {
> >
{(dashboards, loading) => ( {(dashboards, loading) => (
<Spinner loading={loading}> <Spinner loading={loading}>
<Dashboards dashboards={dashboards} /> <Dashboards dashboards={dashboards} orgName={org.name} />
</Spinner> </Spinner>
)} )}
</GetOrgResources> </GetOrgResources>
@ -120,7 +120,7 @@ class OrganizationView extends PureComponent<Props> {
<GetOrgResources<Task[]> organization={org} fetcher={getTasks}> <GetOrgResources<Task[]> organization={org} fetcher={getTasks}>
{(tasks, loading) => ( {(tasks, loading) => (
<Spinner loading={loading}> <Spinner loading={loading}>
<Tasks tasks={tasks} /> <Tasks tasks={tasks} orgName={org.name} />
</Spinner> </Spinner>
)} )}
</GetOrgResources> </GetOrgResources>

View File

@ -0,0 +1,83 @@
/*
Editable Description Component Styles
------------------------------------------------------------------------------
*/
@import 'src/style/modules';
$rename-dash-title-padding: 5px;
.editable-description {
height: $form-xs-height;
width: 100%;
}
.editable-description--preview,
.input.editable-description--input > input {
font-size: $form-xs-font;
font-weight: 400;
font-family: $ix-text-font;
padding: 0 $rename-dash-title-padding;
}
.editable-description--preview,
.editable-description--input {
position: relative;
width: 100%;
}
.editable-description--preview {
border-radius: $radius;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@include no-user-select();
color: $g13-mist;
transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease;
border: $ix-border solid transparent;
height: $form-xs-height;
line-height: $form-xs-height - $ix-border;
.icon {
position: relative;
top: -2px;
display: inline-block;
margin-left: $ix-marg-b;
opacity: 0;
transition: opacity 0.25s ease;
color: $g11-sidewalk;
}
&:hover .icon {
opacity: 1;
}
&.untitled {
color: $g9-mountain;
font-style: italic;
}
&:hover {
cursor: text;
color: $g20-white;
// background-color: $g3-castle;
// border-color: $g3-castle;
}
}
/* Ensure placeholder text matches font weight of title */
.input.editable-description--input > input {
&::-webkit-input-placeholder {
font-weight: $page-title-weight !important;
}
&::-moz-placeholder {
font-weight: $page-title-weight !important;
}
&:-ms-input-placeholder {
font-weight: $page-title-weight !important;
}
&:-moz-placeholder {
font-weight: $page-title-weight !important;
}
}

View File

@ -0,0 +1,130 @@
// Libraries
import React, {Component, KeyboardEvent, ChangeEvent} from 'react'
import classnames from 'classnames'
// Components
import {Input, ComponentSize} from 'src/clockface'
import {ClickOutside} from 'src/shared/components/ClickOutside'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
// Styles
import 'src/shared/components/editable_description/EditableDescription.scss'
interface Props {
onUpdate: (name: string) => void
description: string
placeholder?: string
}
interface State {
isEditing: boolean
workingDescription: string
}
@ErrorHandling
class EditableDescription extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isEditing: false,
workingDescription: props.description,
}
}
public render() {
const {description} = this.props
const {isEditing} = this.state
if (isEditing) {
return (
<div className="editable-description">
<ClickOutside onClickOutside={this.handleStopEditing}>
{this.input}
</ClickOutside>
</div>
)
}
return (
<div className="editable-description">
<div
className={this.previewClassName}
onClick={this.handleStartEditing}
>
{description || 'No description'}
<span className="icon pencil" />
</div>
</div>
)
}
private get input(): JSX.Element {
const {placeholder} = this.props
const {workingDescription} = this.state
return (
<Input
size={ComponentSize.ExtraSmall}
maxLength={90}
autoFocus={true}
spellCheck={false}
placeholder={placeholder}
onFocus={this.handleInputFocus}
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
customClass="editable-description--input"
value={workingDescription}
/>
)
}
private handleStartEditing = (): void => {
this.setState({isEditing: true})
}
private handleStopEditing = async (): Promise<void> => {
const {workingDescription} = this.state
const {onUpdate} = this.props
await onUpdate(workingDescription)
this.setState({isEditing: false})
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({workingDescription: e.target.value})
}
private handleKeyDown = async (
e: KeyboardEvent<HTMLInputElement>
): Promise<void> => {
const {onUpdate, description} = this.props
const {workingDescription} = this.state
if (e.key === 'Enter') {
await onUpdate(workingDescription)
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
this.setState({isEditing: false, workingDescription: description})
}
}
private handleInputFocus = (e: ChangeEvent<HTMLInputElement>): void => {
e.currentTarget.select()
}
private get previewClassName(): string {
const {description} = this.props
return classnames('editable-description--preview', {
untitled: description === '',
})
}
}
export default EditableDescription

View File

@ -22,7 +22,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
return ( return (
<> <>
<ComponentSpacer align={Alignment.Left} stretchToFit={true}> <ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
<label className="task-form--form-label"> <label className="task-form--form-label">
{schedule === TaskSchedule.interval ? 'Interval' : 'Cron'} {schedule === TaskSchedule.interval ? 'Interval' : 'Cron'}
</label> </label>
@ -37,7 +37,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
/> />
</ComponentSpacer> </ComponentSpacer>
<ComponentSpacer align={Alignment.Left} stretchToFit={true}> <ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
<label className="task-form--form-label">Offset</label> <label className="task-form--form-label">Offset</label>
<Input <Input
name="offset" name="offset"