Skip to content

Commit

Permalink
Merge pull request keystonejs#68 from keystonejs/file-upload-ui
Browse files Browse the repository at this point in the history
File upload UI
  • Loading branch information
JedWatson authored May 22, 2018
2 parents 80244f3 + 3003c11 commit e4220b3
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 57 deletions.
19 changes: 13 additions & 6 deletions packages/admin-ui/client/pages/Item/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styled from 'react-emotion';
import raf from 'raf-schd';
import { Button } from '@keystonejs/ui/src/primitives/buttons';
import { Button, LoadingButton } from '@keystonejs/ui/src/primitives/buttons';
import { colors } from '@keystonejs/ui/src/theme';

const Wrapper = styled.div({
Expand Down Expand Up @@ -101,7 +101,7 @@ export default class Footer extends Component {
};

render() {
const { onSave, onReset, onDelete } = this.props;
const { onSave, onReset, onDelete, updateInProgress } = this.props;
const { height, position, top, width } = this.state;

const wrapperStyle = { height };
Expand All @@ -111,25 +111,32 @@ export default class Footer extends Component {
<Wrapper innerRef={this.getWrapper} style={wrapperStyle} key="wrapper">
<Toolbar innerRef={this.getToolbar} style={footerStyle} key="footer">
<div>
<Button
<LoadingButton
appearance="primary"
isDisabled={updateInProgress}
isLoading={updateInProgress}
onClick={onSave}
style={{ marginRight: 8 }}
type="submit"
>
Save Changes
</Button>
</LoadingButton>
<Button
appearance="warning"
isDisabled={!onReset || updateInProgress}
variant="subtle"
onClick={onReset}
isDisabled={!onReset}
>
Reset Changes
</Button>
</div>
<div>
<Button appearance="danger" variant="subtle" onClick={onDelete}>
<Button
appearance="danger"
isDisabled={updateInProgress}
variant="subtle"
onClick={onDelete}
>
Delete
</Button>
</div>
Expand Down
46 changes: 32 additions & 14 deletions packages/admin-ui/client/pages/Item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import { Container, FlexGroup } from '@keystonejs/ui/src/primitives/layout';
import { A11yText, Title } from '@keystonejs/ui/src/primitives/typography';
import { Button, IconButton } from '@keystonejs/ui/src/primitives/buttons';
import { Dialog } from '@keystonejs/ui/src/primitives/modals';
import { colors } from '@keystonejs/ui/src/theme';
import { Alert } from '@keystonejs/ui/src/primitives/alert';
import { colors, gridSize } from '@keystonejs/ui/src/theme';

import { resolveAllKeys } from '@keystonejs/utils';

Expand Down Expand Up @@ -208,14 +209,24 @@ const ItemDetails = withRouter(
};

render() {
const { adminPath, list } = this.props;
const {
adminPath,
list,
updateInProgress,
updateErrorMessage,
} = this.props;
const { copyText, item, itemHasChanged } = this.state;
const isCopied = copyText === item.id;
const CopyIcon = isCopied ? CheckIcon : ClippyIcon;
const listHref = `${adminPath}/${list.path}`;

return (
<Fragment>
{updateErrorMessage ? (
<Alert appearance="danger" css={{ marginTop: gridSize * 3 }}>
{updateErrorMessage}
</Alert>
) : null}
<FlexGroup align="center" justify="space-between">
<Title>
<Link to={listHref}>{list.label}</Link>: {item.name}
Expand Down Expand Up @@ -258,6 +269,7 @@ const ItemDetails = withRouter(
onSave={this.onSave}
onDelete={this.showDeleteModal}
onReset={itemHasChanged ? this.showConfirmResetModal : undefined}
updateInProgress={updateInProgress}
/>
<FooterNavigation>
<IconButton
Expand Down Expand Up @@ -339,18 +351,24 @@ const ItemPage = ({ list, itemId, adminPath, getListByKey }) => {
const item = data[list.itemQueryName];
return item ? (
<Mutation mutation={list.updateMutation}>
{(updateItem, { loading: updateInProgress }) => (
<ItemDetails
adminPath={adminPath}
item={item}
key={itemId}
list={list}
getListByKey={getListByKey}
onUpdate={refetch}
updateInProgress={updateInProgress}
updateItem={updateItem}
/>
)}
{(
updateItem,
{ loading: updateInProgress, error: updateError }
) => {
return (
<ItemDetails
adminPath={adminPath}
item={item}
key={itemId}
list={list}
getListByKey={getListByKey}
onUpdate={refetch}
updateInProgress={updateInProgress}
updateErrorMessage={updateError && updateError.message}
updateItem={updateItem}
/>
);
}}
</Mutation>
) : (
<ItemNotFound adminPath={adminPath} itemId={itemId} list={list} />
Expand Down
2 changes: 1 addition & 1 deletion packages/core/auth/Twitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class TwitterAuthStrategy {
this.config = {
idField: 'twitterId',
usernameField: 'twitterUsername',
sessionListKey: 'TwitterSessions',
sessionListKey: 'TwitterSession',
...config,
};

Expand Down
176 changes: 168 additions & 8 deletions packages/fields/types/CloudinaryImage/views/Field.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,206 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';

import {
FieldContainer,
FieldLabel,
FieldInput,
} from '@keystonejs/ui/src/primitives/fields';
// TODO: Upload component?
import { Input } from '@keystonejs/ui/src/primitives/forms';
import { HiddenInput } from '@keystonejs/ui/src/primitives/forms';
import { LoadingButton } from '@keystonejs/ui/src/primitives/buttons';
import { borderRadius, colors, gridSize } from '@keystonejs/ui/src/theme';

function buttonLabelFn({ hasValue }) {
return hasValue ? 'Change Image' : 'Upload Image';
}

export default class FileField extends Component {
static propTypes = {
buttonLabel: PropTypes.func,
disabled: PropTypes.bool,
field: PropTypes.object,
onChange: PropTypes.func,
};
static defaultProps = {
buttonLabel: buttonLabelFn,
};
state = {
dataURI: null,
errorMessage: false,
isLoading: false,
};

onChange = ({
target: {
validity,
files: [file],
},
}) => {
if (!file) return; // bail if the user cancels from the file browser

const { field, onChange } = this.props;

// basic validity check
if (!validity.valid) {
// TODO - show error state
this.setState({
errorMessage: 'Something went wrong, please reload and try again.',
});
return;
}

// check if the file is actually an image
if (!file.type.includes('image')) {
this.setState({
errorMessage: 'Only image files are allowed. Please try again.',
});
return;
} else if (this.state.errorMessage) {
this.setState({ errorMessage: null });
}

onChange(field, file);
this.getDataURI(file);
};
openFileBrowser = () => {
if (this.inputRef) this.inputRef.click();
};

getDataURI = file => {
const reader = new FileReader();

reader.readAsDataURL(file);
reader.onloadstart = () => {
this.setState({ isLoading: true });
};
reader.onerror = err => {
console.error('Error with Cloudinary preview', err);
this.setState({
errorMessage: 'Something went wrong, please reload and try again.',
});
};
reader.onloadend = upload => {
this.setState({ isLoading: false, dataURI: upload.target.result });
};
};
getImagePath = () => {
const { field, item } = this.props;
const { dataURI } = this.state;
const file = item[field.path];

return (
(file && file.publicUrlTransformed && file.publicUrlTransformed.url) ||
dataURI
);
};
getInputRef = ref => {
this.inputRef = ref;
};

render() {
const { autoFocus, field, item } = this.props;
const { autoFocus, buttonLabel, field, item } = this.props;
const { errorMessage, isLoading } = this.state;

const file = item[field.path];
const imagePath = this.getImagePath();
const button = (
<LoadingButton onClick={this.openFileBrowser} isLoading={isLoading}>
{buttonLabel({ hasValue: imagePath })}
</LoadingButton>
);

return (
<FieldContainer>
<FieldLabel>{field.label}</FieldLabel>
<FieldInput>
<Input
autocomplete="off"
{imagePath ? (
<Wrapper>
<Image src={imagePath} alt={field.path} />
<Content>
<div>{button}</div>
{errorMessage ? (
<ErrorInfo>{errorMessage}</ErrorInfo>
) : file ? (
<MetaInfo>{file.filename || file.name}</MetaInfo>
) : null}
{}
</Content>
</Wrapper>
) : (
button
)}

<HiddenInput
autoComplete="off"
autoFocus={autoFocus}
innerRef={this.getInputRef}
type="file"
name={field.path}
onChange={this.onChange}
/>
</FieldInput>
{file && file.publicUrlTransformed && file.publicUrlTransformed.url ? (
<img src={file.publicUrlTransformed.url} alt={field.path} />
) : null}
</FieldContainer>
);
}
}

const Wrapper = props => (
<div css={{ alignItems: 'flex-start', display: 'flex' }} {...props} />
);
const Content = props => <div css={{ flex: 1 }} {...props} />;
const Image = props => (
<div
css={{
backgroundColor: 'white',
borderRadius,
border: `1px solid ${colors.N20}`,
flexShrink: 0,
lineHeight: 0,
marginRight: gridSize,
maxWidth: 100,
padding: 4,
}}
>
<img
css={{
height: 'auto',
maxWidth: '100%',
}}
{...props}
/>
</div>
);
const Info = ({ styles, ...props }) => (
<div
css={{
borderRadius,
border: '1px solid transparent',
display: 'inline-block',
fontSize: '0.85em',
marginTop: gridSize,
padding: `${gridSize / 2}px ${gridSize}px`,
...styles,
}}
{...props}
/>
);
const MetaInfo = props => (
<Info
styles={{
backgroundColor: colors.N05,
borderColor: colors.N10,
color: colors.N60,
}}
{...props}
/>
);
const ErrorInfo = props => (
<Info
styles={{
backgroundColor: colors.R.L90,
borderColor: colors.R.L80,
color: colors.R.D20,
}}
{...props}
/>
);
Loading

0 comments on commit e4220b3

Please sign in to comment.