diff --git a/.gitignore b/.gitignore index b0ad877c..4614796c 100644 --- a/.gitignore +++ b/.gitignore @@ -118,9 +118,6 @@ Session.vim # temporary .netrwhist *~ -# auto-generated tag files -tags - ### Windows ### # Windows image file caches diff --git a/joyent.code-workspace b/joyent.code-workspace index 678ac1e2..8410215f 100644 --- a/joyent.code-workspace +++ b/joyent.code-workspace @@ -8,6 +8,9 @@ }, { "path": "packages/my-joy-beta" + }, + { + "path": "." } ], "settings": {} diff --git a/packages/my-joy-beta/package.json b/packages/my-joy-beta/package.json index 853906b1..baf8945e 100644 --- a/packages/my-joy-beta/package.json +++ b/packages/my-joy-beta/package.json @@ -6,11 +6,14 @@ "repository": "github:yldio/joyent-portal", "main": "build/", "scripts": { - "dev": "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start", + "dev": + "REACT_APP_GQL_PORT=4000 PORT=3069 REACT_APP_GQL_PROTOCOL=http joyent-react-scripts start", "start": "PORT=3069 joyent-react-scripts start", "build": "NODE_ENV=production joyent-react-scripts build", - "lint-ci": "eslint . --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`", - "lint": "eslint . --fix --ext .js --ext .md && echo 0 `# stylelint './src/**/*.js'`", + "lint-ci": + "eslint . --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-ci": "redrun test", "prepublish": "echo 0" @@ -43,6 +46,7 @@ "redux-form": "^7.1.2", "remcalc": "^1.0.9", "styled-components": "^2.2.3", + "styled-flex-component": "^1.1.0", "title-case": "^2.1.1" }, "devDependencies": { diff --git a/packages/my-joy-beta/src/components/instances/__tests__/tags.js b/packages/my-joy-beta/src/components/instances/__tests__/tags.js new file mode 100644 index 00000000..c60691bc --- /dev/null +++ b/packages/my-joy-beta/src/components/instances/__tests__/tags.js @@ -0,0 +1,19 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import Store from '@mocks/store'; +import 'jest-styled-components'; + +import Tags from '../tags'; + + +it('renders without throwing', () => { + const tree = renderer + .create( + + + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/packages/my-joy-beta/src/components/instances/home.js b/packages/my-joy-beta/src/components/instances/home.js index 61537929..de713508 100644 --- a/packages/my-joy-beta/src/components/instances/home.js +++ b/packages/my-joy-beta/src/components/instances/home.js @@ -206,75 +206,65 @@ export default withTheme( - - - - - - - - + + + - - - - - - - {instance.image && ( - - )} + + + + + + + + + + + {instance.image && ( + + )} + + {instance.ips.map((ip, i) => ( {create ? `Create ${label}` : `Edit ${label}`}

+

{create ? `Create ${label}` : `Edit ${label}`}

) : ( + ) : ( ); @@ -102,6 +108,7 @@ const KeyValue = ({ + + + ) : ( + [ + + + + , + + handleClear(addKey)} + onToggleExpanded={() => onValueChange(!expanded)} + onRemove={() => handleRemove(addKey)} + expanded={expanded} + label="tag" + create + > + {KeyValue} + + + ] + )} + + ); + + const _title = ( + +

+ {tags.length} {tags.length === 1 ? 'Tag' : 'Tags'} +

+
+ ); + + const _noTags = + !tags || + (tags.length === 0 &&

No tags have been added yet

); + + const _list = tags.length > 0 && ( + + {tags + .sort( + (a, b) => + a.initialValues.name.toLowerCase() < + b.initialValues.name.toLowerCase() + ? -1 + : 1 + ) + .map(tag => ( + + + {state[ + `${tag.initialValues.name}-${tag.initialValues.value}-deleting` + ] ? ( + + ) : ( + + {tag.initialValues.name}: {tag.initialValues.value} + {edit && ( + + + removeTag( + tag.initialValues.name, + tag.initialValues.value + )} + fill={theme.grey} + height={remcalc(9)} + /> + + )} + + )} + + + ))} + + ); + + return [ + + {_filterTags} + {_addTag} + , + + + , + _title, + _list, + _noTags + ]; +}; + +export default withTheme(Tags); diff --git a/packages/my-joy-beta/src/containers/instances/tags.js b/packages/my-joy-beta/src/containers/instances/tags.js index 0041be0e..d249d430 100644 --- a/packages/my-joy-beta/src/containers/instances/tags.js +++ b/packages/my-joy-beta/src/containers/instances/tags.js @@ -1,124 +1,126 @@ -import React from 'react'; +import React, { Component } from 'react'; import paramCase from 'param-case'; import { compose, graphql } from 'react-apollo'; -import Value, { set } from 'react-redux-values'; -import { SubmissionError, reset, startSubmit, stopSubmit } from 'redux-form'; -import ReduxForm from 'declarative-redux-form'; +import { set } from 'react-redux-values'; +import { SubmissionError, reset, stopSubmit } from 'redux-form'; import { connect } from 'react-redux'; import find from 'lodash.find'; import get from 'lodash.get'; import { ViewContainer, - Title, StatusLoader, Message, MessageDescription, - MessageTitle, - Button + MessageTitle } from 'joyent-ui-toolkit'; +import TagsComponent from '@components/instances/tags'; + import GetTags from '@graphql/list-tags.gql'; import UpdateTags from '@graphql/update-tags.gql'; import DeleteTag from '@graphql/delete-tag.gql'; -import { KeyValue } from '@components/instances'; const TAG_FORM_KEY = (name, field) => `instance-tag-${name}-${field}`; const CREATE_TAG_FORM_KEY = name => `instance-create-tag-${name}`; -const Tags = ({ - instance, - values = [], - loading, - error, - handleRemove, - handleClear, - handleUpdate, - handleCreate -}) => { - const _title = Tags; - const _loading = !(loading && !values.length) ? null : ; +class Tags extends Component { + constructor(props) { + super(props); + const { values: tags } = props; + this.state = { + tags, + edit: false + }; + } - // tags items forms - const _tags = - !_loading && - values.map(({ form, initialValues }, i) => ( - - {({ value: expanded, onValueChange }) => ( - handleUpdate(newValues, form)} - destroyOnUnmount - id={form} - onClear={() => handleClear(form)} - onToggleExpanded={() => onValueChange(!expanded)} - onRemove={() => handleRemove(form)} - label="tag" - last={values.length - 1 === i} - first={i === 0} - expanded={expanded} - > - {KeyValue} - - )} - - )); + componentWillReceiveProps = ({ values: tags }) => { + this.setState({ + tags + }); + }; - // create tags form - const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name); - const _add = _tags && - _addKey && ( - - {({ value: expanded, onValueChange }) => - !expanded ? ( - - ) : ( - handleClear(_addKey)} - onToggleExpanded={() => onValueChange(!expanded)} - onRemove={() => handleRemove(_addKey)} - expanded={expanded} - label="tag" - create - > - {KeyValue} - - ) - } - + filterTags = e => { + const value = e.target.value; + const { values: tags } = this.props; + + this.setState({ + tags: tags.filter( + tag => + tag.initialValues.value.includes(value) || + tag.initialValues.name.includes(value) + ) + }); + }; + + removeTag = (name, value) => { + const { handleRemove } = this.props; + + handleRemove(name); + + this.setState({ + [`${name}-${value}-deleting`]: true + }); + }; + + toggleEdit = () => { + const { edit } = this.state; + + this.setState({ + edit: !edit + }); + }; + + render = () => { + const { + instance, + values = [], + loading, + error, + handleRemove, + handleClear, + handleCreate + } = this.props; + const _loading = !(loading && !values.length) ? null : ; + const _addKey = instance && CREATE_TAG_FORM_KEY(instance.name); + const { edit, tags } = this.state; + + // tags items forms + const _tags = !_loading && ( + ); - // fetching error - const _error = - error && !values.length && !_loading ? ( - - Ooops! - - An error occurred while loading your instance tags - - - ) : null; + // fetching error + const _error = + error && !values.length && !_loading ? ( + + Ooops! + + An error occurred while loading your instance tags + + + ) : null; - return ( - - {_title} - {_loading} - {_error} - {_tags} - {_add} - - ); -}; + return ( + + {_loading} + {_error} + {_tags} + + ); + }; +} export default compose( graphql(UpdateTags, { name: 'updateTags' }), @@ -159,26 +161,23 @@ export default compose( } }), connect(null, (dispatch, ownProps) => { - const { instance, values, refetch, updateTags, deleteTag } = ownProps; + const { instance, refetch, updateTags, deleteTag } = ownProps; return { // reset sets values to initialValues handleClear: form => dispatch(reset(form)), - handleRemove: form => + handleRemove: name => Promise.resolve( // 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 - dispatch([ - set({ name: `${form}-removing`, value: true }), - startSubmit(form) - ]) + dispatch([set({ name: `${name}-removing`, value: true })]) ) .then(() => // call mutation deleteTag({ variables: { id: instance.id, - name: get(find(values, ['form', form]), 'initialValues.name') + name } }) ) @@ -189,40 +188,14 @@ export default compose( // it takes longer to have an efect than the mutation .catch(error => dispatch([ - set({ name: `${form}-removing`, value: false }), - stopSubmit(form, { + set({ name: `${name}-removing`, value: false }), + stopSubmit(name, { _error: error.graphQLErrors .map(({ message }) => message) .join('\n') }) ]) ), - handleUpdate: ({ name, value }, form) => - // delete old tag and add a new one - Promise.all([ - deleteTag({ - variables: { - id: instance.id, - name: get(find(values, ['form', form]), 'initialValues.name') - } - }), - updateTags({ - variables: { - id: instance.id, - tags: [{ name, value }] - } - }) - ]) - // fetch tags again - .then(() => refetch()) - // submit is flipped once the promise is resolved - .catch(error => { - throw new SubmissionError({ - _error: error.graphQLErrors - .map(({ message }) => message) - .join('\n') - }); - }), handleCreate: ({ name, value }) => // call mutation updateTags({ diff --git a/packages/ui-toolkit/src/index.js b/packages/ui-toolkit/src/index.js index 0f2375f3..192c5c84 100644 --- a/packages/ui-toolkit/src/index.js +++ b/packages/ui-toolkit/src/index.js @@ -88,6 +88,8 @@ export { Item as SectionListItem } from './section-list'; +export { TagItem, TagList, TagItemContainer } from './tags'; + export { Actions as ActionsIcon, Affinity as AffinityIcon, diff --git a/packages/ui-toolkit/src/tags/container.js b/packages/ui-toolkit/src/tags/container.js new file mode 100644 index 00000000..9df288fa --- /dev/null +++ b/packages/ui-toolkit/src/tags/container.js @@ -0,0 +1,7 @@ +import styled from 'styled-components'; + +export default styled.div` + display: flex; + align-items: center; + flex-grow: 1; +`; diff --git a/packages/ui-toolkit/src/tags/index.js b/packages/ui-toolkit/src/tags/index.js new file mode 100644 index 00000000..ab3a022f --- /dev/null +++ b/packages/ui-toolkit/src/tags/index.js @@ -0,0 +1,3 @@ +export { default as TagItem } from './item'; +export { default as TagList } from './list'; +export { default as TagItemContainer } from './container'; diff --git a/packages/ui-toolkit/src/tags/item.js b/packages/ui-toolkit/src/tags/item.js new file mode 100644 index 00000000..a5801d3f --- /dev/null +++ b/packages/ui-toolkit/src/tags/item.js @@ -0,0 +1,13 @@ +import styled from 'styled-components'; +import remcalc from 'remcalc'; + +export default styled.li` + border: ${remcalc(1)} solid ${props => props.theme.grey}; + box-sizing: border-box; + border-radius: ${remcalc(2)}; + font-size: ${remcalc(13)}; + padding: ${remcalc(6)} ${remcalc(12)}; + display: flex; + align-items: center; + flex-grow: 1; +`; diff --git a/packages/ui-toolkit/src/tags/list.js b/packages/ui-toolkit/src/tags/list.js new file mode 100644 index 00000000..4770ccb1 --- /dev/null +++ b/packages/ui-toolkit/src/tags/list.js @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export default styled.ul` + margin: 0; + padding: 0; + display: flex; + list-style: none; + flex-wrap: wrap; +`; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3c3f80d1..e9d4044c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10163,6 +10163,20 @@ styled-components-spacing@^2.1.3: react-create-component-from-tag-prop "^1.2.1" styled-components-breakpoint "^1.0.0-preview.3" +styled-components@2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2" + dependencies: + buffer "^5.0.3" + css-to-react-native "^2.0.3" + fbjs "^0.8.9" + hoist-non-react-statics "^1.2.0" + is-function "^1.0.1" + is-plain-object "^2.0.1" + prop-types "^15.5.4" + stylis "^3.4.0" + supports-color "^3.2.3" + styled-components@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.2.4.tgz#dd87fd3dafd359e7a0d570aec1bd07d691c0b5a2" @@ -10177,7 +10191,14 @@ styled-components@^2.2.3: stylis "^3.4.0" supports-color "^3.2.3" -styled-is@^1.1.0: +styled-flex-component@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/styled-flex-component/-/styled-flex-component-1.1.0.tgz#f897bffecb7650045443481941bfb42ef16d2404" + dependencies: + styled-components "2.2.4" + styled-is "1.1.0" + +styled-is@1.1.0, styled-is@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/styled-is/-/styled-is-1.1.0.tgz#0cf8d32098fe6559eb0ec889790cc6c84f1f497f"