color editing
This commit is contained in:
@ -32,7 +32,7 @@ const Wrapper = styled.div`
|
||||
|
||||
`;
|
||||
|
||||
const Outline = styled(Flex)`
|
||||
export const Outline = styled(Flex)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 0 ${size.large};
|
||||
@ -48,7 +48,7 @@ const Outline = styled(Flex)`
|
||||
}
|
||||
`;
|
||||
|
||||
const LabelText = styled.div`
|
||||
export const LabelText = styled.div`
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
|
@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
import Input from './Input';
|
||||
import Input, { LabelText, Outline } from './Input';
|
||||
export default Input;
|
||||
export { LabelText, Outline };
|
||||
|
@ -100,7 +100,7 @@ type Props = {
|
||||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={<CollectionIcon expanded={expanded} />}
|
||||
icon={<CollectionIcon expanded={expanded} color={collection.color} />}
|
||||
>
|
||||
<Flex justify="space-between">
|
||||
{collection.name}
|
||||
|
@ -19,6 +19,7 @@ class Collection extends BaseModel {
|
||||
description: ?string;
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
type: 'atlas' | 'journal';
|
||||
documents: Array<NavigationNode>;
|
||||
updatedAt: string;
|
||||
@ -57,19 +58,21 @@ class Collection extends BaseModel {
|
||||
if (this.isSaving) return this;
|
||||
this.isSaving = true;
|
||||
|
||||
const params = {
|
||||
name: this.name,
|
||||
color: this.color,
|
||||
description: this.description,
|
||||
};
|
||||
|
||||
try {
|
||||
let res;
|
||||
if (this.id) {
|
||||
res = await client.post('/collections.update', {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
res = await client.post('/collections.create', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
});
|
||||
res = await client.post('/collections.create', params);
|
||||
}
|
||||
runInAction('Collection#save', () => {
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
|
@ -5,7 +5,7 @@ import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import stores from 'stores';
|
||||
import ErrorsStore from 'stores/ErrorsStore';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
import parseTitle from '../../shared/utils/parseTitle.js';
|
||||
|
||||
import type { User } from 'types';
|
||||
import BaseModel from './BaseModel';
|
||||
|
@ -7,6 +7,7 @@ import Button from 'components/Button';
|
||||
import Input from 'components/Input';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import HelpText from 'components/HelpText';
|
||||
import ColorPicker from 'components/ColorPicker';
|
||||
import Collection from 'models/Collection';
|
||||
|
||||
type Props = {
|
||||
@ -18,6 +19,7 @@ type Props = {
|
||||
@observer class CollectionEdit extends Component {
|
||||
props: Props;
|
||||
@observable name: string;
|
||||
@observable color: string = '';
|
||||
@observable isSaving: boolean;
|
||||
|
||||
componentWillMount() {
|
||||
@ -28,7 +30,7 @@ type Props = {
|
||||
ev.preventDefault();
|
||||
this.isSaving = true;
|
||||
|
||||
this.props.collection.updateData({ name: this.name });
|
||||
this.props.collection.updateData({ name: this.name, color: this.color });
|
||||
const success = await this.props.collection.save();
|
||||
|
||||
if (success) {
|
||||
@ -42,6 +44,10 @@ type Props = {
|
||||
this.name = ev.target.value;
|
||||
};
|
||||
|
||||
handleColor = (color: string) => {
|
||||
this.color = color;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Flex column>
|
||||
@ -58,6 +64,10 @@ type Props = {
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<ColorPicker
|
||||
onSelect={this.handleColor}
|
||||
value={this.props.collection.color}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={this.isSaving || !this.props.collection.name}
|
||||
|
@ -5,6 +5,7 @@ import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import Button from 'components/Button';
|
||||
import Input from 'components/Input';
|
||||
import ColorPicker from 'components/ColorPicker';
|
||||
import HelpText from 'components/HelpText';
|
||||
|
||||
import Collection from 'models/Collection';
|
||||
@ -20,6 +21,7 @@ type Props = {
|
||||
props: Props;
|
||||
@observable collection: Collection;
|
||||
@observable name: string = '';
|
||||
@observable color: string = '';
|
||||
@observable isSaving: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
@ -30,7 +32,7 @@ type Props = {
|
||||
handleSubmit = async (ev: SyntheticEvent) => {
|
||||
ev.preventDefault();
|
||||
this.isSaving = true;
|
||||
this.collection.updateData({ name: this.name });
|
||||
this.collection.updateData({ name: this.name, color: this.color });
|
||||
const success = await this.collection.save();
|
||||
|
||||
if (success) {
|
||||
@ -46,6 +48,10 @@ type Props = {
|
||||
this.name = ev.target.value;
|
||||
};
|
||||
|
||||
handleColor = (color: string) => {
|
||||
this.color = color;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
@ -61,6 +67,7 @@ type Props = {
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
<ColorPicker onSelect={this.handleColor} />
|
||||
<Button type="submit" disabled={this.isSaving || !this.name}>
|
||||
{this.isSaving ? 'Creating…' : 'Create'}
|
||||
</Button>
|
||||
|
184
frontend/components/ColorPicker/ColorPicker.js
Normal file
184
frontend/components/ColorPicker/ColorPicker.js
Normal file
@ -0,0 +1,184 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import { observable, computed, action } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import Flex from 'components/Flex';
|
||||
import { LabelText, Outline } from 'components/Input';
|
||||
import { color, fonts, fontWeight } from 'styles/constants';
|
||||
import { validateColorHex } from '../../../shared/utils/color';
|
||||
|
||||
const colors = [
|
||||
'#4E5C6E',
|
||||
'#19B7FF',
|
||||
'#7F6BFF',
|
||||
'#FC7419',
|
||||
'#FC2D2D',
|
||||
'#FFE100',
|
||||
'#14CF9F',
|
||||
'#EE84F0',
|
||||
'#2F362F',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
onSelect: string => void,
|
||||
value?: string,
|
||||
};
|
||||
|
||||
@observer class ColorPicker extends React.Component {
|
||||
props: Props;
|
||||
|
||||
@observable selectedColor: string = colors[0];
|
||||
@observable customColorValue: string = '';
|
||||
@observable customColorSelected: boolean;
|
||||
|
||||
componentWillMount() {
|
||||
const { value } = this.props;
|
||||
if (value && colors.includes(value)) {
|
||||
this.selectedColor = value;
|
||||
} else if (value) {
|
||||
this.customColorSelected = true;
|
||||
this.customColorValue = value.replace('#', '');
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fireCallback();
|
||||
}
|
||||
|
||||
fireCallback = () => {
|
||||
this.props.onSelect(
|
||||
this.customColorSelected ? this.customColor : this.selectedColor
|
||||
);
|
||||
};
|
||||
|
||||
@computed get customColor(): string {
|
||||
return this.customColorValue &&
|
||||
validateColorHex(`#${this.customColorValue}`)
|
||||
? `#${this.customColorValue}`
|
||||
: colors[0];
|
||||
}
|
||||
|
||||
@action setColor = (color: string) => {
|
||||
this.selectedColor = color;
|
||||
this.customColorSelected = false;
|
||||
this.fireCallback();
|
||||
};
|
||||
|
||||
@action focusOnCustomColor = (event: SyntheticEvent) => {
|
||||
this.selectedColor = '';
|
||||
this.customColorSelected = true;
|
||||
this.fireCallback();
|
||||
};
|
||||
|
||||
@action setCustomColor = (event: SyntheticEvent) => {
|
||||
let target = event.target;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
const color = target.value;
|
||||
this.customColorValue = color.replace('#', '');
|
||||
this.fireCallback();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Flex column>
|
||||
<LabelText>Color</LabelText>
|
||||
<StyledOutline justify="space-between">
|
||||
<Flex>
|
||||
{colors.map(color => (
|
||||
<Swatch
|
||||
key={color}
|
||||
color={color}
|
||||
active={
|
||||
color === this.selectedColor && !this.customColorSelected
|
||||
}
|
||||
onClick={() => this.setColor(color)}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex justify="flex-end">
|
||||
<strong>Custom color:</strong>
|
||||
<HexHash>#</HexHash>
|
||||
<CustomColorInput
|
||||
placeholder="FFFFFF"
|
||||
onFocus={this.focusOnCustomColor}
|
||||
onChange={this.setCustomColor}
|
||||
value={this.customColorValue}
|
||||
maxLength={6}
|
||||
/>
|
||||
<Swatch
|
||||
color={this.customColor}
|
||||
active={this.customColorSelected}
|
||||
/>
|
||||
</Flex>
|
||||
</StyledOutline>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type SwatchProps = {
|
||||
onClick?: Function,
|
||||
color?: string,
|
||||
active?: boolean,
|
||||
};
|
||||
|
||||
const Swatch = ({ onClick, ...props }: SwatchProps) => (
|
||||
<SwatchOutset onClick={onClick} {...props}>
|
||||
<SwatchInset {...props} />
|
||||
</SwatchOutset>
|
||||
);
|
||||
|
||||
const SwatchOutset = styled(Flex)`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 5px;
|
||||
border: 2px solid ${({ active, color }) => (active ? color : 'transparent')};
|
||||
border-radius: 2px;
|
||||
background: ${({ color }) => color};
|
||||
${({ onClick }) => onClick && `cursor: pointer;`}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const SwatchInset = styled(Flex)`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')};
|
||||
border-radius: 2px;
|
||||
background: ${({ color }) => color};
|
||||
`;
|
||||
|
||||
const StyledOutline = styled(Outline)`
|
||||
padding: 5px;
|
||||
`;
|
||||
|
||||
const HexHash = styled.div`
|
||||
margin-left: 12px;
|
||||
padding-bottom: 0;
|
||||
font-weight: ${fontWeight.medium};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const CustomColorInput = styled.input`
|
||||
border: 0;
|
||||
flex: 1;
|
||||
width: 65px;
|
||||
margin-right: 12px;
|
||||
padding-bottom: 0;
|
||||
outline: none;
|
||||
background: none;
|
||||
font-family: ${fonts.monospace};
|
||||
font-weight: ${fontWeight.medium};
|
||||
|
||||
&::placeholder {
|
||||
color: ${color.slate};
|
||||
font-family: ${fonts.monospace};
|
||||
font-weight: ${fontWeight.medium};
|
||||
}
|
||||
`;
|
||||
|
||||
export default ColorPicker;
|
3
frontend/components/ColorPicker/index.js
Normal file
3
frontend/components/ColorPicker/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import ColorPicker from './ColorPicker';
|
||||
export default ColorPicker;
|
@ -11,14 +11,17 @@ import { Collection } from '../models';
|
||||
const router = new Router();
|
||||
|
||||
router.post('collections.create', auth(), async ctx => {
|
||||
const { name, description, type } = ctx.body;
|
||||
const { name, color, description, type } = ctx.body;
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
if (color)
|
||||
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
|
||||
|
||||
const user = ctx.state.user;
|
||||
|
||||
const collection = await Collection.create({
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
type: type || 'atlas',
|
||||
teamId: user.teamId,
|
||||
creatorId: user.id,
|
||||
@ -30,11 +33,14 @@ router.post('collections.create', auth(), async ctx => {
|
||||
});
|
||||
|
||||
router.post('collections.update', auth(), async ctx => {
|
||||
const { id, name } = ctx.body;
|
||||
const { id, name, color } = ctx.body;
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
if (color)
|
||||
ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)');
|
||||
|
||||
const collection = await Collection.findById(id);
|
||||
collection.name = name;
|
||||
collection.color = color;
|
||||
await collection.save();
|
||||
|
||||
ctx.body = {
|
||||
|
@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import apiError from '../../errors';
|
||||
import validator from 'validator';
|
||||
import { validateColorHex } from '../../../shared/utils/color';
|
||||
|
||||
export default function validation() {
|
||||
return function validationMiddleware(ctx: Object, next: Function) {
|
||||
@ -28,6 +29,12 @@ export default function validation() {
|
||||
}
|
||||
};
|
||||
|
||||
ctx.assertHexColor = (value, message) => {
|
||||
if (!validateColorHex(value)) {
|
||||
throw apiError(400, 'validation_error', message);
|
||||
}
|
||||
};
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
11
server/migrations/20171023064220-collection-color.js
Normal file
11
server/migrations/20171023064220-collection-color.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('collections', 'color', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('collections', 'color');
|
||||
},
|
||||
};
|
@ -22,6 +22,7 @@ const Collection = sequelize.define(
|
||||
urlId: { type: DataTypes.STRING, unique: true },
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.STRING,
|
||||
color: DataTypes.STRING,
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
validate: { isIn: allowedCollectionTypes },
|
||||
|
@ -5,7 +5,7 @@ import randomstring from 'randomstring';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
import parseTitle from '../../shared/utils/parseTitle.js';
|
||||
import Revision from './Revision';
|
||||
|
||||
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
|
||||
|
@ -11,6 +11,7 @@ async function present(ctx: Object, collection: Collection) {
|
||||
url: collection.getUrl(),
|
||||
name: collection.name,
|
||||
description: collection.description,
|
||||
color: collection.color || '#4E5C6E',
|
||||
type: collection.type,
|
||||
createdAt: collection.createdAt,
|
||||
updatedAt: collection.updatedAt,
|
||||
|
@ -35,6 +35,13 @@ export const fontWeight = {
|
||||
heavy: 800,
|
||||
};
|
||||
|
||||
export const fonts = {
|
||||
regular: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;`,
|
||||
monospace: `'Atlas Typewriter', 'Source Code Pro', Menlo, Consolas,
|
||||
'Liberation Mono', monospace;`,
|
||||
};
|
||||
|
||||
export const color = {
|
||||
text: '#171B35',
|
||||
|
||||
|
4
shared/utils/color.js
Normal file
4
shared/utils/color.js
Normal file
@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export const validateColorHex = (color: string) =>
|
||||
/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
|
Reference in New Issue
Block a user