feat(my-joy-beta): revise Tags to use new KeyValue

This commit is contained in:
Sérgio Ramos 2017-12-21 19:20:22 +00:00
parent 2538453d98
commit 8f7694f7f4
15 changed files with 10003 additions and 5122 deletions

View File

@ -69,15 +69,9 @@
}, },
"workspaces": ["packages/*", "prototypes/*"], "workspaces": ["packages/*", "prototypes/*"],
"resolutions": { "resolutions": {
"graphql": "0.12.3", "hoist-non-react-statics": "2.3.1",
"lodash": "4.17.4",
"lodash.keys": "4.2.0",
"lodash.defaults": "4.2.0",
"lodash.assign": "4.2.0",
"isarray": "'2.0.2",
"codemirror": "5.32.0",
"react": "16.2.0", "react": "16.2.0",
"react-dom": "16.2.0", "react-dom": "16.2.0",
"hoist-non-react-statics": "2.3.1" "styled-components": "2.3.0"
} }
} }

View File

@ -6,16 +6,13 @@
"repository": "github:yldio/joyent-portal", "repository": "github:yldio/joyent-portal",
"main": "build/", "main": "build/",
"scripts": { "scripts": {
"dev": "dev": "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
"REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start",
"start": "PORT=3069 joyent-react-scripts start", "start": "PORT=3069 joyent-react-scripts start",
"build": "NODE_ENV=production joyent-react-scripts build", "build": "NODE_ENV=production joyent-react-scripts build",
"lint-ci": "lint-ci": "eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
"eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`", "lint": "eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
"lint":
"eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`",
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom", "test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test-ci": "echo 0", "test-ci": "npm run test",
"prepublish": "echo 0" "prepublish": "echo 0"
}, },
"dependencies": { "dependencies": {

View File

@ -1,86 +1,64 @@
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import renderer from 'react-test-renderer';
import { reduxForm } from 'redux-form';
import 'jest-styled-components'; import 'jest-styled-components';
import Theme from '@mocks/theme';
import Store from '@mocks/store'; import { KeyValue } from '../key-value';
import KeyValue from '../key-value';
const KeyValueForm = reduxForm()(KeyValue); // KeyValue.propTypes = {
// input: PropTypes.oneOf(['input', 'textarea']).isRequired,
// type: PropTypes.string.isRequired,
// method: PropTypes.oneOf(['add', 'edit']).isRequired,
// removing: PropTypes.bool.isRequired,
// expanded: PropTypes.bool.isRequired,
// onToggleExpanded: PropTypes.func,
// onCancel: PropTypes.func,
// onRemove: PropTypes.func
// };
it('renders <KeyValue /> without throwing', () => {
const tree = renderer
.create(
<Store>
<KeyValueForm />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue textarea /> with textareas', () => { it('renders <KeyValue /> without throwing', () => expect(renderer.create(<Theme><KeyValue /></Theme>).toJSON()).toMatchSnapshot());
const tree = renderer
.create(
<Store>
<KeyValueForm textarea />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue expanded /> expanded', () => { it('renders <KeyValue input="input" /> without throwing', () =>
const tree = renderer expect(
.create( renderer.create(<Theme><KeyValue input="input" /></Theme>).toJSON()
<Store> ).toMatchSnapshot());
<KeyValueForm expanded />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue submitting /> with loader', () => { it('renders <KeyValue input="textarea" /> without throwing', () =>
const tree = renderer expect(
.create( renderer.create(<Theme><KeyValue input="textarea" /></Theme>).toJSON()
<Store> ).toMatchSnapshot());
<KeyValueForm submitting />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue first /> without top margin', () => { it('renders <KeyValue type="tag" /> without throwing', () =>
const tree = renderer expect(renderer.create(<Theme><KeyValue type="tag" /></Theme>).toJSON()).toMatchSnapshot());
.create(
<Store>
<KeyValueForm first />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue last /> with bottom border', () => { it('renders <KeyValue method="add" /> without throwing', () =>
const tree = renderer expect(
.create( renderer.create(<Theme><KeyValue method="add" /></Theme>).toJSON()
<Store> ).toMatchSnapshot());
<KeyValueForm last />
</Store>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders <KeyValue label /> with proper label', () => { it('renders <KeyValue method="edit" /> without throwing', () =>
const tree = renderer expect(
.create( renderer.create(<Theme><KeyValue method="edit" /></Theme>).toJSON()
<Store> ).toMatchSnapshot());
<KeyValueForm label="Label" />
</Store> it('renders <KeyValue removing /> without throwing', () =>
) expect(renderer.create(<Theme><KeyValue removing /></Theme>).toJSON()).toMatchSnapshot());
.toJSON();
expect(tree).toMatchSnapshot(); it('renders <KeyValue submitting removing /> without throwing', () =>
}); expect(
renderer.create(<Theme><KeyValue submitting removing /></Theme>).toJSON()
).toMatchSnapshot());
it('renders <KeyValue expanded /> without throwing', () =>
expect(renderer.create(<Theme><KeyValue expanded /></Theme>).toJSON()).toMatchSnapshot());
it('renders <KeyValue expanded removing /> without throwing', () =>
expect(
renderer.create(<Theme><KeyValue expanded removing /></Theme>).toJSON()
).toMatchSnapshot());
it('renders <KeyValue expanded submitting removing /> without throwing', () =>
expect(
renderer.create(<Theme><KeyValue expanded submitting removing /></Theme>).toJSON()
).toMatchSnapshot());

View File

@ -2,12 +2,11 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTheme } from 'styled-components'; import { withTheme } from 'styled-components';
import { Row, Col } from 'react-styled-flexboxgrid'; import { Row, Col } from 'react-styled-flexboxgrid';
import Value from 'react-redux-values';
import { Field } from 'redux-form'; import { Field } from 'redux-form';
import styled from 'styled-components'; import styled from 'styled-components';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import titleCase from 'title-case'; import titleCase from 'title-case';
import { Margin, Padding } from 'styled-components-spacing'; import { Padding } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component'; import Flex, { FlexItem } from 'styled-flex-component';
import Editor from 'joyent-ui-toolkit/dist/es/editor'; import Editor from 'joyent-ui-toolkit/dist/es/editor';
@ -19,16 +18,13 @@ import {
Card, Card,
CardHeader, CardHeader,
CardHeaderMeta, CardHeaderMeta,
CardHeaderBox,
CardOutlet, CardOutlet,
ChevronIcon,
FormGroup, FormGroup,
FormLabel, FormLabel,
Input, Input,
FormMeta, FormMeta,
Button, Button,
Textarea, Textarea,
Editor,
Divider, Divider,
DeleteIcon DeleteIcon
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
@ -44,7 +40,7 @@ const CollapsedKeyValue = styled.span`
class ValueTextareaField extends PureComponent { class ValueTextareaField extends PureComponent {
render() { render() {
const { input, submitting } = this.props; const { input = {}, submitting } = this.props;
return input.value === 'user-script' ? ( return input.value === 'user-script' ? (
<Field name="value" component={Editor} /> <Field name="value" component={Editor} />
@ -57,7 +53,7 @@ class ValueTextareaField extends PureComponent {
const TextareaKeyValue = ({ type, submitting }) => [ const TextareaKeyValue = ({ type, submitting }) => [
<Row key="key"> <Row key="key">
<Col xs={12}> <Col xs={12}>
<FormGroup name="name" reduxForm fluid> <FormGroup name="name" field={Field} fluid>
<FormLabel>{titleCase(type)} key</FormLabel> <FormLabel>{titleCase(type)} key</FormLabel>
<Input type="text" disabled={submitting} /> <Input type="text" disabled={submitting} />
<FormMeta /> <FormMeta />
@ -67,7 +63,7 @@ const TextareaKeyValue = ({ type, submitting }) => [
</Row>, </Row>,
<Row key="value"> <Row key="value">
<Col xs={12}> <Col xs={12}>
<FormGroup name="value" reduxForm fluid> <FormGroup name="value" field={Field} fluid>
<FormLabel>{titleCase(type)} value</FormLabel> <FormLabel>{titleCase(type)} value</FormLabel>
<Field <Field
name="name" name="name"
@ -85,14 +81,15 @@ const TextareaKeyValue = ({ type, submitting }) => [
const InputKeyValue = ({ type, submitting }) => ( const InputKeyValue = ({ type, submitting }) => (
<Flex full justifyStart contentStretch> <Flex full justifyStart contentStretch>
<FlexItem basis="auto"> <FlexItem basis="auto">
<FormGroup name="name" reduxForm fluid> <FormGroup name="name" field={Field} fluid>
<FormLabel>{titleCase(type)} key</FormLabel> <FormLabel>{titleCase(type)} key</FormLabel>
<Input type="text" disabled={submitting} /> <Input type="text" disabled={submitting} />
<FormMeta /> <FormMeta />
</FormGroup> </FormGroup>
</FlexItem> </FlexItem>
<FlexItem basis={remcalc(12)} />
<FlexItem basis="auto"> <FlexItem basis="auto">
<FormGroup name="value" reduxForm fluid> <FormGroup name="value" field={Field} fluid>
<FormLabel>{titleCase(type)} value</FormLabel> <FormLabel>{titleCase(type)} value</FormLabel>
<Input disabled={submitting} /> <Input disabled={submitting} />
<FormMeta /> <FormMeta />
@ -101,123 +98,114 @@ const InputKeyValue = ({ type, submitting }) => (
</Flex> </Flex>
); );
const KeyValue = withTheme( export const KeyValue = ({
({ input = 'input',
input = 'input', type = 'metadata',
type = 'metadata', method = 'add',
method = 'add', error = null,
error = null, expanded = true,
expanded = true, submitting = false,
submitting = false, pristine = true,
pristine = true, removing = false,
removing = false, handleSubmit,
handleSubmit, onToggleExpanded = () => null,
onToggleExpanded = () => null, onCancel = () => null,
onCancel = () => null, onRemove = () => null,
onRemove = () => null, theme = {}
theme }) => {
}) => { const handleHeaderClick = method === 'edit' && onToggleExpanded;
const handleHeaderClick = method === 'edit' && onToggleExpanded;
return ( return (
<form onSubmit={handleSubmit}> <Card collapsed={!expanded} actionable={!expanded} shadow>
<Card collapsed={!expanded} actionable={!expanded} shadow> <CardHeader
<CardHeader secondary={false}
secondary={false} transparent={false}
transparent={false} actionable={Boolean(handleHeaderClick)}
actionable={Boolean(handleHeaderClick)} onClick={handleHeaderClick}
onClick={handleHeaderClick} >
> <CardHeaderMeta>
<CardHeaderMeta> {method === 'add' ? (
{method === 'add' ? ( <H4>{`${titleCase(method)} ${type}`}</H4>
<H4>{`${titleCase(method)} ${type}`}</H4> ) : (
) : ( <CollapsedKeyValue>
<CollapsedKeyValue> <Field
<Field name="name"
name="name" type="text"
type="text" component={({ input = {} }) =>
component={({ input }) => expanded ? `${input.value}: ` : <b>{`${input.value}: `}</b>
expanded ? ( }
`${input.value}: ` />
) : ( <Field
<b>{`${input.value}: `}</b> name="value"
) type="text"
} component={({ input = {} }) => input.value}
/> />
<Field </CollapsedKeyValue>
name="value" )}
type="text" </CardHeaderMeta>
component={({ input }) => input.value} </CardHeader>
/> <CardOutlet>
</CollapsedKeyValue> <Padding all={1}>
)} {error && !submitting ? (
</CardHeaderMeta> <Row>
</CardHeader> <Col xs={12}>
<CardOutlet> <Message error>
<Padding all={1}> <MessageTitle>Ooops!</MessageTitle>
{error && !submitting ? ( <MessageDescription>{error}</MessageDescription>
<Row> </Message>
<Col xs={12}> </Col>
<Message error> </Row>
<MessageTitle>Ooops!</MessageTitle> ) : null}
<MessageDescription>{error}</MessageDescription> {input === 'input' ? (
</Message> <InputKeyValue type={type} submitting={submitting} />
</Col> ) : (
</Row> <TextareaKeyValue type={type} submitting={submitting} />
) : null} )}
{input === 'input' ? ( <Row between="xs" middle="xs">
<InputKeyValue type={type} submitting={submitting} /> <Col xs={method === 'add' ? 12 : 7}>
) : ( <Button
<TextareaKeyValue type={type} submitting={submitting} /> type="button"
)} onClick={onCancel}
<Row between="xs" middle="xs"> disabled={submitting}
<Col xs={method === 'add' ? 12 : 7}> secondary
<Button marginless
type="button" >
onClick={onCancel} <span>Cancel</span>
disabled={submitting} </Button>
secondary <Button
marginless type="submit"
> disabled={pristine}
<span>Cancel</span> loading={submitting && !removing}
</Button> marginless
<Button >
type="submit" <span>{method === 'add' ? 'Create' : 'Save'}</span>
disabled={pristine} </Button>
loading={submitting && !removing} </Col>
marginless <Col xs={method === 'add' ? false : 5}>
> <Button
<span>{method === 'add' ? 'Create' : 'Save'}</span> type="button"
</Button> onClick={onRemove}
</Col> disabled={submitting}
<Col xs={method === 'add' ? false : 5}> loading={removing}
<Button secondary
type="button" right
onClick={onRemove} icon
disabled={submitting} error
loading={removing} marginless
secondary >
right <DeleteIcon
icon disabled={submitting}
error fill={submitting ? undefined : theme.red}
marginless />
> <span>Delete</span>
<DeleteIcon </Button>
disabled={submitting} </Col>
fill={submitting ? undefined : theme.red} </Row>
/> </Padding>
<span>Delete</span> </CardOutlet>
</Button> </Card>
</Col> );
</Row> };
</Padding>
</CardOutlet>
</Card>
<Divider height={remcalc(13)} transparent />
</form>
);
}
);
KeyValue.propTypes = { KeyValue.propTypes = {
input: PropTypes.oneOf(['input', 'textarea']).isRequired, input: PropTypes.oneOf(['input', 'textarea']).isRequired,
@ -230,4 +218,9 @@ KeyValue.propTypes = {
onRemove: PropTypes.func onRemove: PropTypes.func
}; };
export default props => <KeyValue {...props} />; export default withTheme(({ handleSubmit, ...rest }) => (
<form onSubmit={handleSubmit}>
<KeyValue {...rest} />
<Divider height={remcalc(13)} transparent />
</form>
));

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Field } from 'redux-form';
import KeyValue from './key-value'; import KeyValue from './key-value';
import { import {
@ -14,7 +15,7 @@ export const MenuForm = ({ searchable, onAdd }) => (
<form> <form>
<Row> <Row>
<Col xs={7} sm={5}> <Col xs={7} sm={5}>
<FormGroup name="filter" fluid reduxForm> <FormGroup name="filter" field={Field} fluid>
<FormLabel>Filter</FormLabel> <FormLabel>Filter</FormLabel>
<Input disabled={!searchable} fluid /> <Input disabled={!searchable} fluid />
</FormGroup> </FormGroup>

View File

@ -1,180 +1,75 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
import { Row, Col } from 'react-styled-flexboxgrid'; import { Row, Col } from 'react-styled-flexboxgrid';
import { Margin } from 'styled-components-spacing'; import { Margin } from 'styled-components-spacing';
import Value from 'react-redux-values';
import ReduxForm from 'declarative-redux-form';
import { KeyValue } from '@components/instances'; import { KeyValue } from '@components/instances';
import { withTheme } from 'styled-components'; import { Field } from 'redux-form';
import remcalc from 'remcalc';
import { import {
H3,
H4,
Divider,
FormGroup, FormGroup,
FormLabel, FormLabel,
Input, Input,
Button, Button,
StatusLoader,
CloseIcon,
TagItem, TagItem,
TagList, TagList,
TagItemContainer TagItemContainer
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
const FlexEnd = styled(Col)` export const MenuForm = ({ addable = true, searchable, onAdd }) => (
display: flex; <form>
justify-content: flex-end; <Row>
`; <Col xs={7} sm={5}>
<FormGroup name="filter" field={Field} fluid>
const CloseIconActionable = styled(CloseIcon)` <FormLabel>Filter</FormLabel>
cursor: pointer; <Input disabled={!searchable} fluid />
`; </FormGroup>
</Col>
const Tags = ({ <Col xs={5} sm={7}>
handleRemove, <FormGroup right>
addKey, <FormLabel>&#8291;</FormLabel>
handleCreate, <Button
handleClear, type="button"
theme, disabled={!searchable || !addable}
toggleEdit, onClick={onAdd}
removeTag, small
filterTags, icon
state, fluid
edit,
tags
}) => {
const _filterTags = (
<Col md={4} xs={12}>
<FormGroup fluid>
<FormLabel>Filter</FormLabel>
<Input fluid type="text" onChange={filterTags} />
</FormGroup>
</Col>
);
const _addTag = (
<Value name={`${addKey}-expanded`}>
{({ value: expanded, onValueChange }) =>
!expanded ? (
<FlexEnd mdOffset={4} md={4} xs={12}>
<Button secondary onClick={toggleEdit}>
Edit
</Button>
<Button
type="button"
onClick={() => onValueChange(!expanded)}
disabled={edit}
>
Add tag
</Button>
</FlexEnd>
) : (
[
<FlexEnd mdOffset={4} md={4} xs={12}>
<Button disabled secondary onClick={toggleEdit}>
Edit
</Button>
<Button
type="button"
onClick={() => onValueChange(!expanded)}
disabled
>
Add tag
</Button>
</FlexEnd>,
<Col xs={12}>
<ReduxForm
form={addKey}
onSubmit={handleCreate}
id={addKey}
onClear={() => handleClear(addKey)}
onToggleExpanded={() => onValueChange(!expanded)}
onRemove={() => handleRemove(addKey)}
expanded={expanded}
label="tag"
create
>
{KeyValue}
</ReduxForm>
</Col>
]
)
}
</Value>
);
const _title = (
<Margin bottom={3} key="tag-title">
<H3>
{tags.length} {tags.length === 1 ? 'Tag' : 'Tags'}
</H3>
</Margin>
);
const _noTags =
!tags ||
(tags.length === 0 && <H4 key="no-tags">No tags have been added yet</H4>);
const _list = tags.length > 0 && (
<TagList key="tag-list">
{tags
.sort(
(a, b) =>
a.initialValues.name.toLowerCase() <
b.initialValues.name.toLowerCase()
? -1
: 1
)
.map(tag => (
<Margin
right={1}
bottom={1}
key={`${tag.initialValues.name}-${tag.initialValues.value}`}
> >
<TagItem> Add tag
{state[ </Button>
`${tag.initialValues.name}-${tag.initialValues.value}-deleting` </FormGroup>
] ? ( </Col>
<StatusLoader small /> </Row>
) : ( </form>
<TagItemContainer> );
{tag.initialValues.name}: {tag.initialValues.value}
{edit && (
<Margin left={2}>
<CloseIconActionable
onClick={() =>
removeTag(
tag.initialValues.name,
tag.initialValues.value
)
}
fill={theme.grey}
height={remcalc(9)}
/>
</Margin>
)}
</TagItemContainer>
)}
</TagItem>
</Margin>
))}
</TagList>
);
return [ export const AddForm = props => (
<Row bottom="md" key="tag-row"> <KeyValue {...props} method="add" input="input" type="tag" expanded />
{_filterTags} );
{_addTag}
</Row>,
<Margin key="tag-divider" bottom={4} top={2}>
<Divider height="1px" />
</Margin>,
_title,
_list,
_noTags
];
};
export default withTheme(Tags); export const EditForm = props => (
<KeyValue {...props} method="edit" input="input" type="tag" expanded />
);
const Tag = ({ name, value, onClick }) => (
<Margin right={1} bottom={1} key={`${name}-${value}`}>
<TagItem onClick={onClick}>
<TagItemContainer>
{name}: {value}
</TagItemContainer>
</TagItem>
</Margin>
);
export default ({ values, onToggleEditing, ...rest }) => (
<TagList {...rest}>
{values.map(({ id, name, ...tag }) => (
<Tag
key={id}
id={id}
name={name}
{...tag}
onClick={onToggleEditing && (() => onToggleEditing(name))}
/>
))}
</TagList>
);

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import paramCase from 'param-case'; import paramCase from 'param-case';
import Value, { set } from 'react-redux-values'; import { Margin } from 'styled-components-spacing';
import { set } from 'react-redux-values';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form'; import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form';
@ -12,12 +13,10 @@ import remcalc from 'remcalc';
import { import {
ViewContainer, ViewContainer,
Title,
StatusLoader, StatusLoader,
Message, Message,
MessageDescription, MessageDescription,
MessageTitle, MessageTitle,
Button,
Divider, Divider,
H3 H3
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
@ -25,7 +24,6 @@ import {
import GetMetadata from '@graphql/list-metadata.gql'; import GetMetadata from '@graphql/list-metadata.gql';
import UpdateMetadata from '@graphql/update-metadata.gql'; import UpdateMetadata from '@graphql/update-metadata.gql';
import DeleteMetadata from '@graphql/delete-metadata.gql'; import DeleteMetadata from '@graphql/delete-metadata.gql';
import { KeyValue } from '@components/instances';
import parseError from '@state/parse-error'; import parseError from '@state/parse-error';
import { import {
@ -71,9 +69,9 @@ const Metadata = ({
: null; : null;
const _count = !_loading ? ( const _count = !_loading ? (
<H3 marginBottom={remcalc(24)} marginTop={addOpen && remcalc(24)}> <Margin bottom={4} top={addOpen && 4}>
{metadata.length} key:value pair <H3>{metadata.length} key:value pair</H3>
</H3> </Margin>
) : null; ) : null;
const _metadata = const _metadata =

View File

@ -1,126 +1,110 @@
import React, { Component } from 'react'; import React from 'react';
import { Margin } from 'styled-components-spacing';
import paramCase from 'param-case'; import paramCase from 'param-case';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { set } from 'react-redux-values'; import { set } from 'react-redux-values';
import { SubmissionError, reset, stopSubmit } from 'redux-form'; import { SubmissionError, reset, stopSubmit, startSubmit } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import find from 'lodash.find'; import find from 'lodash.find';
import get from 'lodash.get'; import get from 'lodash.get';
import intercept from 'apr-intercept';
import remcalc from 'remcalc';
import { ViewContainer, StatusLoader, Divider, H3 } from 'joyent-ui-toolkit';
import { import {
ViewContainer, default as TagList,
StatusLoader, MenuForm as TagsMenuForm,
Message, AddForm as TagsAddForm,
MessageDescription, EditForm as TagsEditForm
MessageTitle } from '@components/instances/tags';
} from 'joyent-ui-toolkit';
import TagsComponent from '@components/instances/tags';
import GetTags from '@graphql/list-tags.gql'; import GetTags from '@graphql/list-tags.gql';
import UpdateTags from '@graphql/update-tags.gql'; import UpdateTags from '@graphql/update-tags.gql';
import DeleteTag from '@graphql/delete-tag.gql'; import DeleteTag from '@graphql/delete-tag.gql';
import Index from '@state/gen-index';
import parseError from '@state/parse-error';
const TAG_FORM_KEY = (name, field) => `instance-tag-${name}-${field}`; const MENU_FORM_NAME = 'instance-tags-list-menu';
const CREATE_TAG_FORM_KEY = name => `instance-create-tag-${name}`; const ADD_FORM_NAME = 'instance-tags-add-new';
const EDIT_FORM_KEY = field => `instance-tags-${paramCase(field)}`;
class Tags extends Component { const Tags = ({
constructor(props) { tags = [],
super(props); addOpen,
const { values: tags } = props; editing,
this.state = { loading,
tags, handleToggleAddOpen,
edit: false handleToggleEditing,
}; handleCancel,
} handleEdit,
handleRemove,
handleCreate
}) => {
const _loading = !(loading && !tags.length) ? null : <StatusLoader />;
componentWillReceiveProps = ({ values: tags }) => { const _add = addOpen ? (
this.setState({ <ReduxForm
tags form={ADD_FORM_NAME}
}); onSubmit={handleCreate}
}; onCancel={() => handleToggleAddOpen(false)}
>
{TagsAddForm}
</ReduxForm>
) : null;
filterTags = e => { const _line = !_loading
const value = e.target.value; ? [
const { values: tags } = this.props; <Divider key="line" height={remcalc(1)} />,
<Divider key="after-line-space" height={remcalc(24)} transparent />
]
: null;
this.setState({ const _count = !_loading ? (
tags: tags.filter( <Margin bottom={4} top={addOpen && 4}>
tag => <H3>{tags.length} tags</H3>
tag.initialValues.value.includes(value) || </Margin>
tag.initialValues.name.includes(value) ) : null;
)
});
};
removeTag = (name, value) => { const _tags = !_loading ? (
const { handleRemove } = this.props; <TagList values={tags} onToggleEditing={!editing && handleToggleEditing} />
) : null;
handleRemove(name); const _edit = editing ? (
<ReduxForm
form={editing.form}
initialValues={{ name: editing.name, value: editing.value }}
onSubmit={handleEdit}
onCancel={() => handleToggleEditing(false)}
onToggleExpanded={() => handleToggleEditing(false)}
onRemove={() => handleRemove(editing.form, editing)}
removing={editing.removing}
>
{TagsEditForm}
</ReduxForm>
) : null;
this.setState({ return (
[`${name}-${value}-deleting`]: true <ViewContainer main>
}); <ReduxForm
}; form={MENU_FORM_NAME}
searchable={!_loading}
toggleEdit = () => { addable={!editing}
const { edit } = this.state; onAdd={() => handleToggleAddOpen(!addOpen)}
>
this.setState({ {TagsMenuForm}
edit: !edit </ReduxForm>
}); <Divider height={remcalc(11)} transparent />
}; {_line}
{_loading}
render = () => { {_add}
const { {_edit}
instance, {_count}
values = [], {_tags}
loading, </ViewContainer>
error, );
handleRemove, };
handleClear,
handleCreate
} = this.props;
const _loading = !(loading && !values.length) ? null : <StatusLoader />;
const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name);
const { edit, tags } = this.state;
// tags items forms
const _tags = !_loading && (
<TagsComponent
toggleEdit={this.toggleEdit}
removeTag={this.removeTag}
filterTags={this.filterTags}
state={this.state}
edit={edit}
handleCreate={handleCreate}
handleClear={handleClear}
addKey={_addKey}
tags={tags}
handleRemove={handleRemove}
/>
);
// fetching error
const _error =
error && !values.length && !_loading ? (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance tags
</MessageDescription>
</Message>
) : null;
return (
<ViewContainer center={Boolean(_loading)} main>
{_loading}
{_error}
{_tags}
</ViewContainer>
);
};
}
export default compose( export default compose(
graphql(UpdateTags, { name: 'updateTags' }), graphql(UpdateTags, { name: 'updateTags' }),
@ -138,84 +122,152 @@ export default compose(
const instance = find(get(rest, 'machines', []), ['name', name]); const instance = find(get(rest, 'machines', []), ['name', name]);
const tags = get(instance, 'tags', []); const tags = get(instance, 'tags', []);
const values = tags.map(({ name, value }) => {
const field = paramCase(name);
const form = TAG_FORM_KEY(name, field);
return {
form,
initialValues: {
name,
value
}
};
});
return { return {
values, tags,
instance, instance,
index: Index(tags),
loading, loading,
error, error,
refetch refetch
}; };
} }
}), }),
connect(null, (dispatch, ownProps) => { connect(
const { instance, refetch, updateTags, deleteTag } = ownProps; ({ values, form }, { tags, index, ...ownProps }) => {
// get search value
const filter = get(form, `${MENU_FORM_NAME}.values.filter`, false);
// if user is searching something, get items that match that query
const filtered = filter
? index.search(filter).map(({ ref }) => find(tags, ['id', ref]))
: tags;
return { const editingTagName = get(values, 'editing-tag', null);
// reset sets values to initialValues const removingTagName = get(values, 'removing-tag', null);
handleClear: form => dispatch(reset(form)), const editingTag =
handleRemove: name => editingTagName && find(filtered, ['name', editingTagName]);
Promise.resolve( const removingTag = editingTagName === removingTagName;
// set removing=true (so that we can have a specific removing spinner)
// because remove button is not a submit button, we have to manually flip that flag return {
dispatch([set({ name: `${name}-removing`, value: true })]) ...ownProps,
) tags: filtered,
.then(() => addOpen: get(values, 'add-tags-open', false),
// call mutation editing: editingTag && {
...editingTag,
removing: Boolean(removingTag),
form: EDIT_FORM_KEY(editingTag.name)
}
};
},
(dispatch, ownProps) => {
return {
handleToggleAddOpen: value =>
dispatch(set({ name: `add-tags-open`, value })),
handleToggleEditing: value =>
dispatch(set({ name: `editing-tag`, value })),
handleEdit: async ({ name, value }, _, { form, initialValues }) => {
const { tags, instance, deleteTag, updateTags, refetch } = ownProps;
// call mutations
const [err] = await intercept(
Promise.all([
deleteTag({
variables: {
id: instance.id,
name: initialValues.name
}
}),
updateTags({
variables: {
id: instance.id,
tags: [{ name, value }]
}
})
])
);
if (err) {
// show mutation error
throw new SubmissionError({
_error: parseError(err)
});
}
dispatch([
set({ name: `editing-tag`, value: false }),
reset(form),
startSubmit(form)
]);
// fetch tags again (even though we are polling)
return refetch();
},
handleRemove: async (form, { name }) => {
const { instance, deleteTag, refetch } = ownProps;
dispatch([
set({ name: `removing-tag`, value: name }),
startSubmit(form)
]);
// call mutation
const [err] = await intercept(
deleteTag({ deleteTag({
variables: { variables: {
id: instance.id, id: instance.id,
name name
} }
}) })
) );
// fetch tags again
.then(() => refetch()) if (err) {
// we only flip removing and submitting when there is an error. // show mutation error
// the reason for that is that tags is updated asyncronously and
// it takes longer to have an efect than the mutation
.catch(error =>
dispatch([
set({ name: `${name}-removing`, value: false }),
stopSubmit(name, {
_error: error.graphQLErrors
.map(({ message }) => message)
.join('\n')
})
])
),
handleCreate: ({ name, value }) =>
// call mutation
updateTags({
variables: {
id: instance.id,
tags: [{ name, value }]
}
})
// fetch tags again
.then(() => refetch())
// reset create new tags form
.then(() => dispatch(reset(CREATE_TAG_FORM_KEY(instance.name))))
// submit is flipped once the promise is resolved
.catch(error => {
throw new SubmissionError({ throw new SubmissionError({
_error: error.graphQLErrors _error: parseError(err)
.map(({ message }) => message)
.join('\n')
}); });
}) }
};
}) dispatch([
set({ name: `editing-tag`, value: false }),
set({ name: `removing-tag`, value: false }),
reset(form),
startSubmit(form)
]);
// fetch tags again (even though we are polling)
return refetch();
},
handleCreate: async ({ name, value }) => {
const { updateTags, instance, refetch } = ownProps;
// call mutation
const [err] = await intercept(
updateTags({
variables: {
id: instance.id,
tags: [{ name, value }]
}
})
);
if (err) {
// show mutation error
throw new SubmissionError({
_error: parseError(err)
});
}
dispatch([
// reset create new tags form
reset(ADD_FORM_NAME),
stopSubmit(ADD_FORM_NAME),
// close add form
set({ name: `add-tags-open`, value: false })
]);
// fetch tags again (even though we are polling)
return refetch();
}
};
}
)
)(Tags); )(Tags);

View File

@ -0,0 +1,4 @@
module.exports = {
'^joyent-ui-toolkit/dist/es/editor$': '<rootDir>/src/mocks/editor',
'^redux-form$': '<rootDir>/src/mocks/redux-form'
};

View File

@ -0,0 +1 @@
export default () => <span>joyent-maifest-editor</span>

View File

@ -0,0 +1,3 @@
import React from 'react';
export const Field = ({ component = "input", children, ...rest }) => React.createElement(component, rest, children);

View File

@ -11,6 +11,7 @@ class ManifestEditorBundle extends Component {
this.handleRender = this.handleRender.bind(this); this.handleRender = this.handleRender.bind(this);
} }
handleRender(ManifestEditor) { handleRender(ManifestEditor) {
if (ManifestEditor) { if (ManifestEditor) {
setTimeout(() => { setTimeout(() => {
@ -20,6 +21,7 @@ class ManifestEditorBundle extends Component {
return <Loader />; return <Loader />;
} }
render() { render() {
if (!this.state.ManifestEditor) { if (!this.state.ManifestEditor) {
return ( return (

View File

@ -1,5 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import is from 'styled-is';
export default styled.li` export default styled.li`
border: ${remcalc(1)} solid ${props => props.theme.grey}; border: ${remcalc(1)} solid ${props => props.theme.grey};
@ -10,4 +11,8 @@ export default styled.li`
display: flex; display: flex;
align-items: center; align-items: center;
flex-grow: 1; flex-grow: 1;
${is('onClick')`
cursor: pointer;
`};
`; `;

View File

@ -2273,9 +2273,9 @@ code-point-at@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
codemirror@5.32.0, codemirror@^5.18.2, codemirror@^5.32.0: codemirror@^5.18.2, codemirror@^5.32.0:
version "5.32.0" version "5.33.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.32.0.tgz#cb6ff5d8ef36d0b10f031130e2d9ebeee92c902e" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.33.0.tgz#462ad9a6fe8d38b541a9536a3997e1ef93b40c6a"
coleman-liau@^1.0.0: coleman-liau@^1.0.0:
version "1.0.2" version "1.0.2"
@ -6566,10 +6566,6 @@ lodash._reinterpolate@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
lodash.assign@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
lodash.camelcase@^4.3.0: lodash.camelcase@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
@ -6582,7 +6578,7 @@ lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
lodash.defaults@4.2.0, lodash.defaults@^4.2.0: lodash.defaults@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@ -6654,7 +6650,7 @@ lodash.isundefined@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48" resolved "https://registry.yarnpkg.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz#23ef3d9535565203a66cefd5b830f848911afb48"
lodash.keys@4.2.0, lodash.keys@^4.2.0: lodash.keys@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205"
@ -6699,10 +6695,18 @@ lodash.uniq@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
lodash@4.17.2, lodash@4.17.4, "lodash@>=3.5 <5", lodash@^3.10.1, lodash@^3.3.1, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1: lodash@4.17.2:
version "4.17.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.1.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
version "4.17.4" version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
lodash@^3.10.1, lodash@^3.3.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
log-symbols@^1.0.2: log-symbols@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
@ -10118,9 +10122,9 @@ styled-components-spacing@^2.1.3:
react-create-component-from-tag-prop "^1.2.1" react-create-component-from-tag-prop "^1.2.1"
styled-components-breakpoint "^1.0.0-preview.3" styled-components-breakpoint "^1.0.0-preview.3"
styled-components@2.2.4: styled-components@2.2.4, styled-components@2.3.0, styled-components@^2.2.3, styled-components@^2.3.0:
version "2.2.4" version "2.3.0"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.3.0.tgz#d9cf4574e140fea6426e48632ed0ca4494537718"
dependencies: dependencies:
buffer "^5.0.3" buffer "^5.0.3"
css-to-react-native "^2.0.3" css-to-react-native "^2.0.3"
@ -10132,19 +10136,6 @@ styled-components@2.2.4:
stylis "^3.4.0" stylis "^3.4.0"
supports-color "^3.2.3" supports-color "^3.2.3"
styled-components@^2.2.3, styled-components@^2.3.0:
version "2.3.3"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.3.3.tgz#351d0be84699db750c73d95617e4334157421f71"
dependencies:
buffer "^5.0.3"
css-to-react-native "^2.0.3"
fbjs "^0.8.9"
hoist-non-react-statics "^1.2.0"
is-plain-object "^2.0.1"
prop-types "^15.5.4"
stylis "^3.4.0"
supports-color "^3.2.3"
styled-flex-component@^2.1.0: styled-flex-component@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/styled-flex-component/-/styled-flex-component-2.1.0.tgz#b1efab209965288a9e1d6dbe981692693568cca2" resolved "https://registry.yarnpkg.com/styled-flex-component/-/styled-flex-component-2.1.0.tgz#b1efab209965288a9e1d6dbe981692693568cca2"