Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin UI: Improvements to Update workflow #2871

Merged
merged 5 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-adults-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystonejs/app-admin-ui': patch
---

Improved Update workflow.
Vultraz marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion packages/app-admin-ui/client/components/Nav/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ const ActionItems = ({ mouseIsOverNav }) => {
...(ENABLE_DEV_FEATURES
? [
{
label: 'GraphiQL Playground',
label: 'GraphQL Playground',
Copy link
Contributor Author

@Vultraz Vultraz May 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, I just learned that GraphiQL =/= GraphQL Playground.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

¯_(ツ)_/¯ OK

to: graphiqlPath,
icon: TerminalIcon,
target: '_blank',
Expand Down
320 changes: 155 additions & 165 deletions packages/app-admin-ui/client/components/UpdateManyItemsModal.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import { Component, Fragment, useMemo, useCallback, Suspense } from 'react';
import { Fragment, useMemo, useCallback, Suspense, useState } from 'react';
import { useMutation } from '@apollo/react-hooks';
import { useToasts } from 'react-toast-notifications';
import { omit, arrayToObject, countArrays } from '@keystonejs/utils';

import { Button, LoadingButton } from '@arch-ui/button';
import Drawer from '@arch-ui/drawer';
import { FieldContainer, FieldLabel, FieldInput } from '@arch-ui/fields';
import Select from '@arch-ui/select';
import { omit, arrayToObject, countArrays } from '@keystonejs/utils';
import { LoadingIndicator } from '@arch-ui/loading';
import Select from '@arch-ui/select';

import { validateFields } from '../util';
import { validateFields, handleCreateUpdateMutationError } from '../util';
import CreateItemModal from './CreateItemModal';

let Render = ({ children }) => children();

class UpdateManyModal extends Component {
constructor(props) {
super(props);
const { list } = props;
const selectedFields = [];
const item = list.getInitialItemData();
const validationErrors = {};
const validationWarnings = {};

this.state = { item, selectedFields, validationErrors, validationWarnings };
}
onUpdate = async () => {
const { updateItem, isLoading, items } = this.props;
const { item, selectedFields, validationErrors, validationWarnings } = this.state;
if (isLoading) return;
if (countArrays(validationErrors)) {
const Render = ({ children }) => children();

const UpdateManyModal = ({ list, items, isOpen, onUpdate, onClose }) => {
const { addToast } = useToasts();
const [updateItem, { loading }] = useMutation(list.updateManyMutation, {
errorPolicy: 'all',
onError: error => handleCreateUpdateMutationError({ error, addToast }),
});

const [item, setItem] = useState({});
const [selectedFields, setSelectedFields] = useState([]);
const [validationErrors, setValidationErrors] = useState({});
const [validationWarnings, setValidationWarnings] = useState({});

const handleUpdate = async () => {
if (loading || countArrays(validationErrors)) {
return;
}

Expand All @@ -39,171 +39,161 @@ class UpdateManyModal extends Component {
const { errors, warnings } = await validateFields(selectedFields, item, data);

if (countArrays(errors) + countArrays(warnings) > 0) {
this.setState(() => ({
validationErrors: errors,
validationWarnings: warnings,
}));
setValidationErrors(errors);
setValidationWarnings(warnings);

return;
}
}

updateItem({
const result = await updateItem({
variables: {
data: items.map(id => ({ id, data })),
},
}).then(() => {
this.props.onUpdate();
this.resetState();
});

// Result will be undefined if a GraphQL error occurs (such as failed validation)
// Leave the modal open in that case
if (!result) return;

resetState();
onUpdate();
};

resetState = () => {
this.setState({ item: this.props.list.getInitialItemData({}), selectedFields: [] });
const resetState = () => {
setItem(list.getInitialItemData({}));
setSelectedFields([]);
};
onClose = () => {
const { isLoading } = this.props;
if (isLoading) return;
this.resetState();
this.props.onClose();

const handleClose = () => {
if (loading) return;
resetState();
onClose();
};
onKeyDown = event => {

const onKeyDown = event => {
if (event.defaultPrevented) return;
switch (event.key) {
case 'Escape':
return this.onClose();
return handleClose();
case 'Enter':
return this.onUpdate();
return handleUpdate();
}
};
handleSelect = selected => {
const { list } = this.props;
const selectedFields = selected.map(({ path, value }) => {
return list.fields
.filter(({ isPrimaryKey }) => !isPrimaryKey)
.find(f => f.path === path || f.path === value);
});
this.setState({ selectedFields });
};
getOptionValue = option => {
return option.path || option.value;

const handleSelect = selected => {
setSelectedFields(
selected
? selected.map(({ path, value }) => {
return list.fields
.filter(({ isPrimaryKey }) => !isPrimaryKey)
.find(f => f.path === path || f.path === value);
})
: []
);
};
getOptionValue = option => {

const getOptionValue = option => {
return option.path || option.value;
};
getOptions = () => {
const { list } = this.props;

const options = useMemo(
// remove the `options` key from select type fields
return list.fields.filter(({ isPrimaryKey }) => !isPrimaryKey).map(f => omit(f, ['options']));
};
render() {
const { isLoading, isOpen, items, list } = this.props;
const { item, selectedFields, validationErrors, validationWarnings } = this.state;
const options = this.getOptions();

const hasWarnings = countArrays(validationWarnings);
const hasErrors = countArrays(validationErrors);

return (
<Drawer
isOpen={isOpen}
onClose={this.onClose}
closeOnBlanketClick
heading={`Update ${list.formatCount(items)}`}
onKeyDown={this.onKeyDown}
slideInFrom="left"
footer={
<Fragment>
<LoadingButton
appearance={hasWarnings && !hasErrors ? 'warning' : 'primary'}
isDisabled={hasErrors}
isLoading={isLoading}
onClick={this.onUpdate}
>
{hasWarnings && !hasErrors ? 'Ignore Warnings and Update' : 'Update'}
</LoadingButton>
<Button appearance="warning" variant="subtle" onClick={this.onClose}>
Cancel
</Button>
</Fragment>
}
>
<FieldContainer>
<FieldLabel field={{ label: 'Fields', config: { isRequired: false } }} />
<FieldInput>
<Select
autoFocus
isMulti
menuPosition="fixed"
onChange={this.handleSelect}
options={options}
tabSelectsValue={false}
value={selectedFields}
getOptionValue={this.getOptionValue}
filterOption={this.filterOption}
/>
</FieldInput>
</FieldContainer>
{selectedFields.map((field, i) => {
return (
<Suspense
fallback={<LoadingIndicator css={{ height: '3em' }} size={12} />}
key={field.path}
>
<Render>
{() => {
let [Field] = field.adminMeta.readViews([field.views.Field]);
let onChange = useCallback(
value => {
this.setState(({ item }) => ({
item: {
...item,
[field.path]: value,
},
validationErrors: {},
validationWarnings: {},
}));
},
[field]
);
return useMemo(
() => (
<Field
autoFocus={!i}
field={field}
value={item[field.path]}
// Explicitly pass undefined here as it doesn't make
// sense to pass in any one 'saved' value
savedValue={undefined}
errors={validationErrors[field.path] || []}
warnings={validationWarnings[field.path] || []}
onChange={onChange}
renderContext="dialog"
CreateItemModal={CreateItemModal}
/>
),
[
i,
field,
item[field.path],
validationErrors[field.path],
validationWarnings[field.path],
onChange,
]
);
}}
</Render>
</Suspense>
);
})}
</Drawer>
);
}
}
() => list.fields.filter(({ isPrimaryKey }) => !isPrimaryKey).map(f => omit(f, ['options'])),
[]
);

export default function UpdateManyModalWithMutation(props) {
const { list } = props;
const [updateItem, { loading }] = useMutation(list.updateManyMutation);
const hasWarnings = countArrays(validationWarnings);
const hasErrors = countArrays(validationErrors);

return (
<Drawer
isOpen={isOpen}
onClose={handleClose}
closeOnBlanketClick
heading={`Update ${list.formatCount(items)}`}
onKeyDown={onKeyDown}
slideInFrom="left"
footer={
<Fragment>
<LoadingButton
appearance={hasWarnings && !hasErrors ? 'warning' : 'primary'}
isDisabled={hasErrors}
isLoading={loading}
onClick={handleUpdate}
>
{hasWarnings && !hasErrors ? 'Ignore Warnings and Update' : 'Update'}
</LoadingButton>
<Button appearance="warning" variant="subtle" onClick={handleClose}>
Cancel
</Button>
</Fragment>
}
>
<FieldContainer>
<FieldLabel field={{ label: 'Fields', config: { isRequired: false } }} />
<FieldInput>
<Select
autoFocus
isMulti
menuPosition="fixed"
onChange={handleSelect}
options={options}
tabSelectsValue={false}
value={selectedFields}
getOptionValue={getOptionValue}
/>
</FieldInput>
</FieldContainer>
{selectedFields.map((field, i) => {
return (
<Suspense
fallback={<LoadingIndicator css={{ height: '3em' }} size={12} />}
key={field.path}
>
<Render>
{() => {
const [Field] = field.adminMeta.readViews([field.views.Field]);
const onChange = useCallback(
value => {
setItem(prev => ({ ...prev, [field.path]: value }));
setValidationErrors({});
setValidationWarnings({});
},
[field]
);
return useMemo(
() => (
<Field
autoFocus={!i}
field={field}
value={item[field.path]}
// Explicitly pass undefined here as it doesn't make
// sense to pass in any one 'saved' value
savedValue={undefined}
errors={validationErrors[field.path] || []}
warnings={validationWarnings[field.path] || []}
onChange={onChange}
renderContext="dialog"
CreateItemModal={CreateItemModal}
/>
),
[
i,
field,
item[field.path],
validationErrors[field.path],
validationWarnings[field.path],
onChange,
]
);
}}
</Render>
</Suspense>
);
})}
</Drawer>
);
};

return <UpdateManyModal updateItem={updateItem} isLoading={loading} {...props} />;
}
export default UpdateManyModal;
Loading