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 testpull/10616/head
parent
7875051ace
commit
7cf643d1d5
|
@ -6,6 +6,14 @@
|
|||
.component-spacer {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&.component-spacer--stretch-w {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.component-spacer--stretch-h {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.component-spacer--left {
|
||||
|
@ -24,10 +32,6 @@
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&.component-spacer--stretch {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.component-spacer--left > * {
|
||||
margin-right: $ix-marg-a;
|
||||
|
||||
|
@ -53,10 +57,6 @@
|
|||
.component-spacer--vertical {
|
||||
flex-direction: column;
|
||||
|
||||
&.component-spacer--stretch {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.component-spacer--left {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
|
|
@ -9,14 +9,16 @@ interface Props {
|
|||
children: JSX.Element | JSX.Element[]
|
||||
align: Alignment
|
||||
stackChildren?: Stack
|
||||
stretchToFit?: boolean
|
||||
stretchToFitWidth?: boolean
|
||||
stretchToFitHeight?: boolean
|
||||
}
|
||||
|
||||
const ComponentSpacer: SFC<Props> = ({
|
||||
children,
|
||||
align,
|
||||
stackChildren = Stack.Columns,
|
||||
stretchToFit = false,
|
||||
stretchToFitWidth = false,
|
||||
stretchToFitHeight = false,
|
||||
}) => (
|
||||
<div
|
||||
className={classnames('component-spacer', {
|
||||
|
@ -25,7 +27,8 @@ const ComponentSpacer: SFC<Props> = ({
|
|||
'component-spacer--right': align === Alignment.Right,
|
||||
'component-spacer--horizontal': stackChildren === Stack.Columns,
|
||||
'component-spacer--vertical': stackChildren === Stack.Rows,
|
||||
'component-spacer--stretch': stretchToFit,
|
||||
'component-spacer--stretch-w': stretchToFitWidth,
|
||||
'component-spacer--stretch-h': stretchToFitHeight,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -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 {
|
||||
margin-left: $ix-marg-b;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ interface Props {
|
|||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
searchTerm: string
|
||||
}
|
||||
|
@ -34,6 +35,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
|||
onCreateDashboard,
|
||||
defaultDashboardLink,
|
||||
onSetDefaultDashboard,
|
||||
onUpdateDashboard,
|
||||
searchTerm,
|
||||
} = this.props
|
||||
|
||||
|
@ -48,6 +50,7 @@ export default class DashboardsIndexContents extends Component<Props> {
|
|||
onExportDashboard={onExportDashboard}
|
||||
defaultDashboardLink={defaultDashboardLink}
|
||||
onSetDefaultDashboard={onSetDefaultDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
getDashboardsAsync,
|
||||
importDashboardAsync,
|
||||
deleteDashboardAsync,
|
||||
updateDashboardAsync,
|
||||
} from 'src/dashboards/actions/v2'
|
||||
import {setDefaultDashboard} from 'src/shared/actions/links'
|
||||
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
|
||||
|
@ -59,6 +60,7 @@ interface Props {
|
|||
handleGetDashboards: typeof getDashboardsAsync
|
||||
handleDeleteDashboard: typeof deleteDashboardAsync
|
||||
handleImportDashboard: typeof importDashboardAsync
|
||||
handleUpdateDashboard: typeof updateDashboardAsync
|
||||
notify: (message: Notification) => void
|
||||
retainRangesDashTimeV1: (dashboardIDs: string[]) => void
|
||||
dashboards: Dashboard[]
|
||||
|
@ -88,7 +90,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {dashboards, notify, links} = this.props
|
||||
const {dashboards, notify, links, handleUpdateDashboard} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
|
@ -103,12 +105,6 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
placeholderText="Filter dashboards by name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<Button
|
||||
onClick={this.handleToggleOverlay}
|
||||
icon={IconFont.Import}
|
||||
text="Import"
|
||||
titleText="Import a dashboard from a file"
|
||||
/>
|
||||
<Button
|
||||
color={ComponentColor.Primary}
|
||||
onClick={this.handleCreateDashboard}
|
||||
|
@ -127,6 +123,7 @@ class DashboardIndex extends PureComponent<Props, State> {
|
|||
onCreateDashboard={this.handleCreateDashboard}
|
||||
onCloneDashboard={this.handleCloneDashboard}
|
||||
onExportDashboard={this.handleExportDashboard}
|
||||
onUpdateDashboard={handleUpdateDashboard}
|
||||
notify={notify}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
|
@ -271,6 +268,7 @@ const mdtp = {
|
|||
handleGetDashboards: getDashboardsAsync,
|
||||
handleDeleteDashboard: deleteDashboardAsync,
|
||||
handleImportDashboard: importDashboardAsync,
|
||||
handleUpdateDashboard: updateDashboardAsync,
|
||||
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ interface Props {
|
|||
onCreateDashboard: () => void
|
||||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
onSetDefaultDashboard: (dashboardLink: string) => void
|
||||
}
|
||||
|
||||
|
@ -62,26 +63,26 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
|||
columnName={headerKeys[0]}
|
||||
sortKey={headerKeys[0]}
|
||||
sort={sortKey === headerKeys[0] ? sortDirection : Sort.None}
|
||||
width="50%"
|
||||
width="62%"
|
||||
onClick={this.handleClickColumn}
|
||||
/>
|
||||
<IndexList.HeaderCell
|
||||
columnName={headerKeys[1]}
|
||||
sortKey={headerKeys[1]}
|
||||
sort={sortKey === headerKeys[1] ? sortDirection : Sort.None}
|
||||
width="10%"
|
||||
width="17%"
|
||||
onClick={this.handleClickColumn}
|
||||
/>
|
||||
<IndexList.HeaderCell
|
||||
columnName={headerKeys[2]}
|
||||
sortKey={headerKeys[2]}
|
||||
sort={sortKey === headerKeys[2] ? sortDirection : Sort.None}
|
||||
width="30%"
|
||||
width="11%"
|
||||
onClick={this.handleClickColumn}
|
||||
/>
|
||||
<IndexList.HeaderCell
|
||||
columnName=""
|
||||
width="20%"
|
||||
width="10%"
|
||||
alignment={Alignment.Right}
|
||||
/>
|
||||
</IndexList.Header>
|
||||
|
@ -102,6 +103,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
|||
onExportDashboard,
|
||||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onUpdateDashboard,
|
||||
} = this.props
|
||||
|
||||
const {sortKey, sortDirection} = this.state
|
||||
|
@ -119,6 +121,7 @@ class DashboardsTable extends PureComponent<Props & WithRouterProps, State> {
|
|||
onCloneDashboard={onCloneDashboard}
|
||||
onExportDashboard={onExportDashboard}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
/>
|
||||
)}
|
||||
</SortingHat>
|
||||
|
|
|
@ -15,6 +15,7 @@ const setup = (override = {}) => {
|
|||
onDeleteDashboard: jest.fn(),
|
||||
onCloneDashboard: jest.fn(),
|
||||
onExportDashboard: jest.fn(),
|
||||
onUpdateDashboard: jest.fn(),
|
||||
...override,
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
Stack,
|
||||
Label,
|
||||
} from 'src/clockface'
|
||||
import EditableDescription from 'src/shared/components/editable_description/EditableDescription'
|
||||
|
||||
// Types
|
||||
import {Dashboard} from 'src/types/v2'
|
||||
|
@ -30,6 +31,7 @@ interface Props {
|
|||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
||||
|
@ -40,7 +42,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
return (
|
||||
<IndexList.Row key={`dashboard-id--${id}`} disabled={false}>
|
||||
<IndexList.Cell>
|
||||
<ComponentSpacer stackChildren={Stack.Rows} align={Alignment.Left}>
|
||||
<ComponentSpacer
|
||||
stackChildren={Stack.Rows}
|
||||
align={Alignment.Left}
|
||||
stretchToFitWidth={true}
|
||||
>
|
||||
<ComponentSpacer
|
||||
stackChildren={Stack.Columns}
|
||||
align={Alignment.Left}
|
||||
|
@ -50,21 +56,17 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
</Link>
|
||||
{this.labels}
|
||||
</ComponentSpacer>
|
||||
{this.description}
|
||||
<EditableDescription
|
||||
description={dashboard.description}
|
||||
placeholder={`Describe ${dashboard.name}`}
|
||||
onUpdate={this.handleUpdateDescription}
|
||||
/>
|
||||
</ComponentSpacer>
|
||||
</IndexList.Cell>
|
||||
<IndexList.Cell>Owner does not come back from API</IndexList.Cell>
|
||||
<IndexList.Cell>
|
||||
{moment(dashboard.meta.updatedAt).format(UPDATED_AT_TIME_FORMAT)}
|
||||
</IndexList.Cell>
|
||||
{this.lastModifiedCell}
|
||||
<IndexList.Cell alignment={Alignment.Right} revealOnHover={true}>
|
||||
<ComponentSpacer align={Alignment.Left} stackChildren={Stack.Columns}>
|
||||
<Button
|
||||
size={ComponentSize.ExtraSmall}
|
||||
text="Export"
|
||||
icon={IconFont.Export}
|
||||
onClick={this.handleExport}
|
||||
/>
|
||||
<Button
|
||||
size={ComponentSize.ExtraSmall}
|
||||
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 {
|
||||
const {dashboard} = this.props
|
||||
|
||||
|
@ -120,23 +137,11 @@ export default class DashboardsIndexTableRow extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
private get description(): JSX.Element {
|
||||
const {dashboard} = this.props
|
||||
private handleUpdateDescription = (description: string): void => {
|
||||
const {onUpdateDashboard} = this.props
|
||||
const dashboard = {...this.props.dashboard, description}
|
||||
|
||||
if (dashboard.description) {
|
||||
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)
|
||||
onUpdateDashboard(dashboard)
|
||||
}
|
||||
|
||||
private handleClone = () => {
|
||||
|
|
|
@ -12,6 +12,7 @@ interface Props {
|
|||
onDeleteDashboard: (dashboard: Dashboard) => void
|
||||
onCloneDashboard: (dashboard: Dashboard) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => void
|
||||
onUpdateDashboard: (dashboard: Dashboard) => void
|
||||
}
|
||||
|
||||
export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
||||
|
@ -21,6 +22,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
|||
onExportDashboard,
|
||||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onUpdateDashboard,
|
||||
} = this.props
|
||||
|
||||
return dashboards.map(d => (
|
||||
|
@ -30,6 +32,7 @@ export default class DashboardsIndexTableRows extends PureComponent<Props> {
|
|||
onExportDashboard={onExportDashboard}
|
||||
onCloneDashboard={onCloneDashboard}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onUpdateDashboard={onUpdateDashboard}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -159,19 +159,29 @@ export default class Buckets extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private get emptyState(): JSX.Element {
|
||||
const {org} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
if (_.isEmpty(searchTerm)) {
|
||||
return (
|
||||
<EmptyState size={ComponentSize.Large}>
|
||||
<EmptyState.Text text="Oh noes I dun see na buckets" />
|
||||
<EmptyState size={ComponentSize.Medium}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyState size={ComponentSize.Large}>
|
||||
<EmptyState.Text text="No buckets match your query" />
|
||||
<EmptyState size={ComponentSize.Medium}>
|
||||
<EmptyState.Text text="No Buckets match your query" />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
||||
|
@ -15,6 +16,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
|
|||
|
||||
interface Props {
|
||||
dashboards: Dashboard[]
|
||||
orgName: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -66,9 +68,23 @@ export default class Dashboards extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
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 (
|
||||
<EmptyState size={ComponentSize.Large}>
|
||||
<EmptyState.Text text="Oh noes I dun see na dashbardsss" />
|
||||
<EmptyState size={ComponentSize.Medium}>
|
||||
<EmptyState.Text text="No Dashboards match your query" />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
HEX_CODE_CHAR_LENGTH,
|
||||
INPUT_ERROR_COLOR,
|
||||
} from 'src/organizations/constants/LabelColors'
|
||||
const MAX_LABEL_CHARS = 75
|
||||
const MAX_LABEL_CHARS = 50
|
||||
|
||||
// Utils
|
||||
import {validateHexCode} from 'src/organizations/utils/labels'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import {ComponentSize, EmptyState, IconFont, Input} from 'src/clockface'
|
||||
|
@ -15,6 +16,7 @@ import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader
|
|||
|
||||
interface Props {
|
||||
members: ResourceOwner[]
|
||||
orgName: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -60,9 +62,23 @@ export default class Members extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
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 (
|
||||
<EmptyState size={ComponentSize.Medium}>
|
||||
<EmptyState.Text text="This org has been abandoned" />
|
||||
<EmptyState.Text text="No Members match your query" />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import TabbedPageHeader from 'src/shared/components/tabbed_page/TabbedPageHeader'
|
||||
|
@ -12,6 +13,7 @@ import {Task} from 'src/api'
|
|||
|
||||
interface Props {
|
||||
tasks: Task[]
|
||||
orgName: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -62,9 +64,23 @@ export default class Tasks extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
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 (
|
||||
<EmptyState size={ComponentSize.Medium}>
|
||||
<EmptyState.Text text="I see nay a task" />
|
||||
<EmptyState.Text text="No Tasks match your query" />
|
||||
</EmptyState>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ class OrganizationView extends PureComponent<Props> {
|
|||
>
|
||||
{(members, loading) => (
|
||||
<Spinner loading={loading}>
|
||||
<Members members={members} />
|
||||
<Members members={members} orgName={org.name} />
|
||||
</Spinner>
|
||||
)}
|
||||
</GetOrgResources>
|
||||
|
@ -107,7 +107,7 @@ class OrganizationView extends PureComponent<Props> {
|
|||
>
|
||||
{(dashboards, loading) => (
|
||||
<Spinner loading={loading}>
|
||||
<Dashboards dashboards={dashboards} />
|
||||
<Dashboards dashboards={dashboards} orgName={org.name} />
|
||||
</Spinner>
|
||||
)}
|
||||
</GetOrgResources>
|
||||
|
@ -120,7 +120,7 @@ class OrganizationView extends PureComponent<Props> {
|
|||
<GetOrgResources<Task[]> organization={org} fetcher={getTasks}>
|
||||
{(tasks, loading) => (
|
||||
<Spinner loading={loading}>
|
||||
<Tasks tasks={tasks} />
|
||||
<Tasks tasks={tasks} orgName={org.name} />
|
||||
</Spinner>
|
||||
)}
|
||||
</GetOrgResources>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -22,7 +22,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ComponentSpacer align={Alignment.Left} stretchToFit={true}>
|
||||
<ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
|
||||
<label className="task-form--form-label">
|
||||
{schedule === TaskSchedule.interval ? 'Interval' : 'Cron'}
|
||||
</label>
|
||||
|
@ -37,7 +37,7 @@ export default class TaskScheduleFormFields extends PureComponent<Props> {
|
|||
/>
|
||||
</ComponentSpacer>
|
||||
|
||||
<ComponentSpacer align={Alignment.Left} stretchToFit={true}>
|
||||
<ComponentSpacer align={Alignment.Left} stretchToFitWidth={true}>
|
||||
<label className="task-form--form-label">Offset</label>
|
||||
<Input
|
||||
name="offset"
|
||||
|
|
Loading…
Reference in New Issue