feat: Collection Icons (#1281)
* wip: Working for creation, and display * feat: IconPicker * fix * feat: Invert collection icon color when dark in dark mode * Improve readability of dropdown menus in dark mode Suggest icon based on collection name * Add additional icons Tweaks and final polish * fix: Write default icon as empty icon column * feat: Improve icon selection logic add more keywords Improve icon coloring when selected and in dark mode * lint * lint
This commit is contained in:
parent
f3ea02fdd0
commit
d864e228e7
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { getLuminance } from 'polished';
|
||||
import { PrivateCollectionIcon, CollectionIcon } from 'outline-icons';
|
||||
import Collection from 'models/Collection';
|
||||
import { icons } from 'components/IconPicker';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
type Props = {
|
||||
collection: Collection,
|
||||
expanded?: boolean,
|
||||
size?: number,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
function ResolvedCollectionIcon({ collection, expanded, size, ui }: Props) {
|
||||
// If the chosen icon color is very dark then we invert it in dark mode
|
||||
// otherwise it will be impossible to see against the dark background.
|
||||
const color =
|
||||
ui.resolvedTheme === 'dark'
|
||||
? getLuminance(collection.color) > 0.12
|
||||
? collection.color
|
||||
: 'currentColor'
|
||||
: collection.color;
|
||||
|
||||
if (collection.icon && collection.icon !== 'collection') {
|
||||
try {
|
||||
const Component = icons[collection.icon].component;
|
||||
return <Component color={color} size={size} />;
|
||||
} catch (error) {
|
||||
console.warn('Failed to render custom icon ' + collection.icon);
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.private) {
|
||||
return (
|
||||
<PrivateCollectionIcon color={color} expanded={expanded} size={size} />
|
||||
);
|
||||
}
|
||||
|
||||
return <CollectionIcon color={color} expanded={expanded} size={size} />;
|
||||
}
|
||||
|
||||
export default inject('ui')(observer(ResolvedCollectionIcon));
|
|
@ -1,106 +0,0 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { TwitterPicker } from 'react-color';
|
||||
import styled from 'styled-components';
|
||||
import Fade from 'components/Fade';
|
||||
import { LabelText } from 'components/Input';
|
||||
|
||||
const colors = [
|
||||
'#4E5C6E',
|
||||
'#19B7FF',
|
||||
'#7F6BFF',
|
||||
'#FC7419',
|
||||
'#FC2D2D',
|
||||
'#FFE100',
|
||||
'#14CF9F',
|
||||
'#00D084',
|
||||
'#EE84F0',
|
||||
'#2F362F',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
onChange: (color: string) => void,
|
||||
value?: string,
|
||||
};
|
||||
|
||||
@observer
|
||||
class ColorPicker extends React.Component<Props> {
|
||||
@observable isOpen: boolean = false;
|
||||
node: ?HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.isOpen = true;
|
||||
};
|
||||
|
||||
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
|
||||
// $FlowFixMe
|
||||
if (ev.target && this.node && this.node.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Wrapper ref={ref => (this.node = ref)}>
|
||||
<label>
|
||||
<LabelText>Color</LabelText>
|
||||
</label>
|
||||
<Swatch
|
||||
role="button"
|
||||
onClick={this.isOpen ? this.handleClose : this.handleOpen}
|
||||
color={this.props.value}
|
||||
/>
|
||||
<Floating>
|
||||
{this.isOpen && (
|
||||
<Fade>
|
||||
<TwitterPicker
|
||||
colors={colors}
|
||||
color={this.props.value}
|
||||
onChange={color => this.props.onChange(color.hex)}
|
||||
triangle="top-right"
|
||||
/>
|
||||
</Fade>
|
||||
)}
|
||||
</Floating>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`;
|
||||
const Floating = styled('div')`
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const Swatch = styled('div')`
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 32px;
|
||||
border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')};
|
||||
border-radius: 4px;
|
||||
background: ${({ color }) => color};
|
||||
`;
|
||||
|
||||
export default ColorPicker;
|
|
@ -253,6 +253,10 @@ const Menu = styled.div`
|
|||
animation: ${fadeAndScaleIn} 200ms ease;
|
||||
transform-origin: ${props => (props.left !== undefined ? '25%' : '75%')} 0;
|
||||
background: ${props => props.theme.menuBackground};
|
||||
${props =>
|
||||
props.theme.menuBorder
|
||||
? `border: 1px solid ${props.theme.menuBorder}`
|
||||
: ''};
|
||||
border-radius: 2px;
|
||||
padding: 0.5em 0;
|
||||
min-width: 180px;
|
||||
|
@ -261,6 +265,10 @@ const Menu = styled.div`
|
|||
box-shadow: ${props => props.theme.menuShadow};
|
||||
pointer-events: all;
|
||||
|
||||
hr {
|
||||
margin: 0.5em 12px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { TwitterPicker } from 'react-color';
|
||||
import {
|
||||
CollectionIcon,
|
||||
CoinsIcon,
|
||||
AcademicCapIcon,
|
||||
BeakerIcon,
|
||||
BuildingBlocksIcon,
|
||||
CloudIcon,
|
||||
CodeIcon,
|
||||
EditIcon,
|
||||
EyeIcon,
|
||||
LeafIcon,
|
||||
LightBulbIcon,
|
||||
MoonIcon,
|
||||
NotepadIcon,
|
||||
PadlockIcon,
|
||||
PaletteIcon,
|
||||
QuestionMarkIcon,
|
||||
SunIcon,
|
||||
VehicleIcon,
|
||||
} from 'outline-icons';
|
||||
import styled from 'styled-components';
|
||||
import { LabelText } from 'components/Input';
|
||||
import { DropdownMenu } from 'components/DropdownMenu';
|
||||
import NudeButton from 'components/NudeButton';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
export const icons = {
|
||||
collection: {
|
||||
component: CollectionIcon,
|
||||
keywords: 'collection',
|
||||
},
|
||||
coins: {
|
||||
component: CoinsIcon,
|
||||
keywords: 'coins money finance sales income revenue cash',
|
||||
},
|
||||
academicCap: {
|
||||
component: AcademicCapIcon,
|
||||
keywords: 'learn teach lesson guide tutorial onboarding training',
|
||||
},
|
||||
beaker: {
|
||||
component: BeakerIcon,
|
||||
keywords: 'lab research experiment test',
|
||||
},
|
||||
buildingBlocks: {
|
||||
component: BuildingBlocksIcon,
|
||||
keywords: 'app blocks product prototype',
|
||||
},
|
||||
cloud: {
|
||||
component: CloudIcon,
|
||||
keywords: 'cloud service aws infrastructure',
|
||||
},
|
||||
code: {
|
||||
component: CodeIcon,
|
||||
keywords: 'developer api code development engineering programming',
|
||||
},
|
||||
eye: {
|
||||
component: EyeIcon,
|
||||
keywords: 'eye view',
|
||||
},
|
||||
leaf: {
|
||||
component: LeafIcon,
|
||||
keywords: 'leaf plant outdoors nature ecosystem climate',
|
||||
},
|
||||
lightbulb: {
|
||||
component: LightBulbIcon,
|
||||
keywords: 'lightbulb idea',
|
||||
},
|
||||
moon: {
|
||||
component: MoonIcon,
|
||||
keywords: 'night moon dark',
|
||||
},
|
||||
notepad: {
|
||||
component: NotepadIcon,
|
||||
keywords: 'journal notepad write notes',
|
||||
},
|
||||
padlock: {
|
||||
component: PadlockIcon,
|
||||
keywords: 'padlock private security authentication authorization auth',
|
||||
},
|
||||
palette: {
|
||||
component: PaletteIcon,
|
||||
keywords: 'design palette art brand',
|
||||
},
|
||||
pencil: {
|
||||
component: EditIcon,
|
||||
keywords: 'copy writing post blog',
|
||||
},
|
||||
question: {
|
||||
component: QuestionMarkIcon,
|
||||
keywords: 'question help support faq',
|
||||
},
|
||||
sun: {
|
||||
component: SunIcon,
|
||||
keywords: 'day sun weather',
|
||||
},
|
||||
vehicle: {
|
||||
component: VehicleIcon,
|
||||
keywords: 'truck car travel transport',
|
||||
},
|
||||
};
|
||||
|
||||
const colors = [
|
||||
'#4E5C6E',
|
||||
'#0366d6',
|
||||
'#7F6BFF',
|
||||
'#E76F51',
|
||||
'#FC2D2D',
|
||||
'#FFBE0B',
|
||||
'#2A9D8F',
|
||||
'#00D084',
|
||||
'#EE84F0',
|
||||
'#2F362F',
|
||||
];
|
||||
|
||||
type Props = {
|
||||
onOpen?: () => void,
|
||||
onChange: (color: string, icon: string) => void,
|
||||
icon: string,
|
||||
color: string,
|
||||
};
|
||||
|
||||
function preventEventBubble(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@observer
|
||||
class IconPicker extends React.Component<Props> {
|
||||
@observable isOpen: boolean = false;
|
||||
node: ?HTMLElement;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleClickOutside);
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.isOpen = false;
|
||||
};
|
||||
|
||||
handleOpen = () => {
|
||||
this.isOpen = true;
|
||||
|
||||
if (this.props.onOpen) {
|
||||
this.props.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
handleClickOutside = (ev: SyntheticMouseEvent<>) => {
|
||||
// $FlowFixMe
|
||||
if (ev.target && this.node && this.node.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
const Component = icons[this.props.icon || 'collection'].component;
|
||||
|
||||
return (
|
||||
<Wrapper ref={ref => (this.node = ref)}>
|
||||
<label>
|
||||
<LabelText>Icon</LabelText>
|
||||
</label>
|
||||
<DropdownMenu
|
||||
label={
|
||||
<LabelButton>
|
||||
<Component role="button" color={this.props.color} size={30} />
|
||||
</LabelButton>
|
||||
}
|
||||
>
|
||||
<Icons onClick={preventEventBubble}>
|
||||
{Object.keys(icons).map(name => {
|
||||
const Component = icons[name].component;
|
||||
return (
|
||||
<IconButton
|
||||
key={name}
|
||||
onClick={() => this.props.onChange(this.props.color, name)}
|
||||
style={{ width: 30, height: 30 }}
|
||||
>
|
||||
<Component color={this.props.color} size={30} />
|
||||
</IconButton>
|
||||
);
|
||||
})}
|
||||
</Icons>
|
||||
<Flex onClick={preventEventBubble}>
|
||||
<ColorPicker
|
||||
color={this.props.color}
|
||||
onChange={color =>
|
||||
this.props.onChange(color.hex, this.props.icon)
|
||||
}
|
||||
colors={colors}
|
||||
triangle="hide"
|
||||
/>
|
||||
</Flex>
|
||||
</DropdownMenu>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const Icons = styled.div`
|
||||
padding: 15px 9px 9px 15px;
|
||||
width: 276px;
|
||||
`;
|
||||
|
||||
const LabelButton = styled(NudeButton)`
|
||||
border: 1px solid ${props => props.theme.inputBorder};
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
`;
|
||||
|
||||
const IconButton = styled(NudeButton)`
|
||||
border-radius: 4px;
|
||||
margin: 0px 6px 6px 0px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
`;
|
||||
|
||||
const ColorPicker = styled(TwitterPicker)`
|
||||
box-shadow: none !important;
|
||||
background: transparent !important;
|
||||
`;
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export default IconPicker;
|
|
@ -2,12 +2,13 @@
|
|||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { GoToIcon, CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
|
||||
import { GoToIcon } from 'outline-icons';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import Collection from 'models/Collection';
|
||||
import type { DocumentPath } from 'stores/CollectionsStore';
|
||||
import CollectionIcon from 'components/CollectionIcon';
|
||||
|
||||
type Props = {
|
||||
result: DocumentPath,
|
||||
|
@ -41,12 +42,7 @@ class PathToDocument extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<Component ref={ref} onClick={this.handleClick} href="" selectable>
|
||||
{collection &&
|
||||
(collection.private ? (
|
||||
<PrivateCollectionIcon color={collection.color} />
|
||||
) : (
|
||||
<CollectionIcon color={collection.color} />
|
||||
))}
|
||||
{collection && <CollectionIcon collection={collection} />}
|
||||
{result.path
|
||||
.map(doc => <Title key={doc.id}>{doc.title}</Title>)
|
||||
.reduce((prev, curr) => [prev, <StyledGoToIcon />, curr])}
|
||||
|
|
|
@ -84,7 +84,7 @@ class MainSidebar extends React.Component<Props> {
|
|||
<Section>
|
||||
<SidebarLink
|
||||
to="/home"
|
||||
icon={<HomeIcon />}
|
||||
icon={<HomeIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Home"
|
||||
/>
|
||||
|
@ -93,19 +93,19 @@ class MainSidebar extends React.Component<Props> {
|
|||
pathname: '/search',
|
||||
state: { fromMenu: true },
|
||||
}}
|
||||
icon={<SearchIcon />}
|
||||
icon={<SearchIcon color="currentColor" />}
|
||||
label="Search"
|
||||
exact={false}
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/starred"
|
||||
icon={<StarredIcon />}
|
||||
icon={<StarredIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Starred"
|
||||
/>
|
||||
<SidebarLink
|
||||
to="/drafts"
|
||||
icon={<EditIcon />}
|
||||
icon={<EditIcon color="currentColor" />}
|
||||
label={
|
||||
<Drafts align="center">
|
||||
Drafts{draftDocumentsCount > 0 && (
|
||||
|
@ -127,7 +127,7 @@ class MainSidebar extends React.Component<Props> {
|
|||
<Section>
|
||||
<SidebarLink
|
||||
to="/archive"
|
||||
icon={<ArchiveIcon />}
|
||||
icon={<ArchiveIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Archive"
|
||||
active={
|
||||
|
@ -138,7 +138,7 @@ class MainSidebar extends React.Component<Props> {
|
|||
/>
|
||||
<SidebarLink
|
||||
to="/trash"
|
||||
icon={<TrashIcon />}
|
||||
icon={<TrashIcon color="currentColor" />}
|
||||
exact={false}
|
||||
label="Trash"
|
||||
active={
|
||||
|
@ -149,7 +149,7 @@ class MainSidebar extends React.Component<Props> {
|
|||
<SidebarLink
|
||||
to="/settings/people"
|
||||
onClick={this.handleInviteModalOpen}
|
||||
icon={<PlusIcon />}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label="Invite people…"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { observable } from 'mobx';
|
||||
import { CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
|
||||
import Collection from 'models/Collection';
|
||||
import Document from 'models/Document';
|
||||
import CollectionMenu from 'menus/CollectionMenu';
|
||||
|
@ -10,6 +9,7 @@ import UiStore from 'stores/UiStore';
|
|||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import SidebarLink from './SidebarLink';
|
||||
import DocumentLink from './DocumentLink';
|
||||
import CollectionIcon from 'components/CollectionIcon';
|
||||
import DropToImport from 'components/DropToImport';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
|
@ -44,16 +44,7 @@ class CollectionLink extends React.Component<Props> {
|
|||
<SidebarLink
|
||||
key={collection.id}
|
||||
to={collection.url}
|
||||
icon={
|
||||
collection.private ? (
|
||||
<PrivateCollectionIcon
|
||||
expanded={expanded}
|
||||
color={collection.color}
|
||||
/>
|
||||
) : (
|
||||
<CollectionIcon expanded={expanded} color={collection.color} />
|
||||
)
|
||||
}
|
||||
icon={<CollectionIcon collection={collection} expanded={expanded} />}
|
||||
iconColor={collection.color}
|
||||
expanded={expanded}
|
||||
hideDisclosure
|
||||
|
|
|
@ -70,7 +70,7 @@ class Collections extends React.Component<Props> {
|
|||
<SidebarLink
|
||||
to="/collections"
|
||||
onClick={this.props.onCreateCollection}
|
||||
icon={<PlusIcon />}
|
||||
icon={<PlusIcon color="currentColor" />}
|
||||
label="New collection…"
|
||||
exact
|
||||
/>
|
||||
|
|
|
@ -3,13 +3,14 @@ import * as React from 'react';
|
|||
import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { PlusIcon, CollectionIcon, PrivateCollectionIcon } from 'outline-icons';
|
||||
import { PlusIcon } from 'outline-icons';
|
||||
|
||||
import { newDocumentUrl } from 'utils/routeHelpers';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
import Button from 'components/Button';
|
||||
import CollectionIcon from 'components/CollectionIcon';
|
||||
|
||||
type Props = {
|
||||
label?: React.Node,
|
||||
|
@ -64,12 +65,7 @@ class NewDocumentMenu extends React.Component<Props> {
|
|||
onClick={() => this.handleNewDocument(collection.id)}
|
||||
disabled={!can.update}
|
||||
>
|
||||
{collection.private ? (
|
||||
<PrivateCollectionIcon color={collection.color} />
|
||||
) : (
|
||||
<CollectionIcon color={collection.color} />
|
||||
)}{' '}
|
||||
{collection.name}
|
||||
<CollectionIcon collection={collection} /> {collection.name}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -13,6 +13,7 @@ export default class Collection extends BaseModel {
|
|||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
private: boolean;
|
||||
type: 'atlas' | 'journal';
|
||||
|
@ -101,7 +102,14 @@ export default class Collection extends BaseModel {
|
|||
}
|
||||
|
||||
toJS = () => {
|
||||
return pick(this, ['id', 'name', 'color', 'description', 'private']);
|
||||
return pick(this, [
|
||||
'id',
|
||||
'name',
|
||||
'color',
|
||||
'description',
|
||||
'icon',
|
||||
'private',
|
||||
]);
|
||||
};
|
||||
|
||||
export = () => {
|
||||
|
|
|
@ -5,13 +5,7 @@ import { observer, inject } from 'mobx-react';
|
|||
import { Redirect, Link, Switch, Route } from 'react-router-dom';
|
||||
|
||||
import styled, { withTheme } from 'styled-components';
|
||||
import {
|
||||
CollectionIcon,
|
||||
PrivateCollectionIcon,
|
||||
NewDocumentIcon,
|
||||
PlusIcon,
|
||||
PinIcon,
|
||||
} from 'outline-icons';
|
||||
import { NewDocumentIcon, PlusIcon, PinIcon } from 'outline-icons';
|
||||
import RichMarkdownEditor from 'rich-markdown-editor';
|
||||
|
||||
import { newDocumentUrl, collectionUrl } from 'utils/routeHelpers';
|
||||
|
@ -42,6 +36,7 @@ import CollectionMembers from 'scenes/CollectionMembers';
|
|||
import Tabs from 'components/Tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import PaginatedDocumentList from 'components/PaginatedDocumentList';
|
||||
import CollectionIcon from 'components/CollectionIcon';
|
||||
|
||||
type Props = {
|
||||
ui: UiStore,
|
||||
|
@ -210,19 +205,7 @@ class CollectionScene extends React.Component<Props> {
|
|||
) : (
|
||||
<React.Fragment>
|
||||
<Heading>
|
||||
{collection.private ? (
|
||||
<PrivateCollectionIcon
|
||||
color={collection.color}
|
||||
size={40}
|
||||
expanded
|
||||
/>
|
||||
) : (
|
||||
<CollectionIcon
|
||||
color={collection.color}
|
||||
size={40}
|
||||
expanded
|
||||
/>
|
||||
)}{' '}
|
||||
<CollectionIcon collection={collection} size={40} expanded />{' '}
|
||||
{collection.name}
|
||||
</Heading>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import Button from 'components/Button';
|
|||
import Switch from 'components/Switch';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import HelpText from 'components/HelpText';
|
||||
import ColorPicker from 'components/ColorPicker';
|
||||
import IconPicker from 'components/IconPicker';
|
||||
import Collection from 'models/Collection';
|
||||
import UiStore from 'stores/UiStore';
|
||||
|
||||
|
@ -22,6 +22,7 @@ type Props = {
|
|||
class CollectionEdit extends React.Component<Props> {
|
||||
@observable name: string;
|
||||
@observable description: string = '';
|
||||
@observable icon: string = '';
|
||||
@observable color: string = '#4E5C6E';
|
||||
@observable isSaving: boolean;
|
||||
@observable private: boolean = false;
|
||||
|
@ -29,6 +30,7 @@ class CollectionEdit extends React.Component<Props> {
|
|||
componentDidMount() {
|
||||
this.name = this.props.collection.name;
|
||||
this.description = this.props.collection.description;
|
||||
this.icon = this.props.collection.icon;
|
||||
this.color = this.props.collection.color;
|
||||
this.private = this.props.collection.private;
|
||||
}
|
||||
|
@ -41,6 +43,7 @@ class CollectionEdit extends React.Component<Props> {
|
|||
await this.props.collection.save({
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
icon: this.icon,
|
||||
color: this.color,
|
||||
private: this.private,
|
||||
});
|
||||
|
@ -61,8 +64,9 @@ class CollectionEdit extends React.Component<Props> {
|
|||
this.name = ev.target.value;
|
||||
};
|
||||
|
||||
handleColor = (color: string) => {
|
||||
handleChange = (color: string, icon: string) => {
|
||||
this.color = color;
|
||||
this.icon = icon;
|
||||
};
|
||||
|
||||
handlePrivateChange = (ev: SyntheticInputEvent<*>) => {
|
||||
|
@ -87,7 +91,12 @@ class CollectionEdit extends React.Component<Props> {
|
|||
autoFocus
|
||||
flex
|
||||
/>
|
||||
<ColorPicker onChange={this.handleColor} value={this.color} />
|
||||
|
||||
<IconPicker
|
||||
onChange={this.handleChange}
|
||||
color={this.color}
|
||||
icon={this.icon}
|
||||
/>
|
||||
</Flex>
|
||||
<InputRich
|
||||
id={this.props.collection.id}
|
||||
|
|
|
@ -3,11 +3,12 @@ import * as React from 'react';
|
|||
import { withRouter, type RouterHistory } from 'react-router-dom';
|
||||
import { observable } from 'mobx';
|
||||
import { inject, observer } from 'mobx-react';
|
||||
import { intersection } from 'lodash';
|
||||
import Button from 'components/Button';
|
||||
import Switch from 'components/Switch';
|
||||
import Input from 'components/Input';
|
||||
import InputRich from 'components/InputRich';
|
||||
import ColorPicker from 'components/ColorPicker';
|
||||
import IconPicker, { icons } from 'components/IconPicker';
|
||||
import HelpText from 'components/HelpText';
|
||||
import Flex from 'shared/components/Flex';
|
||||
|
||||
|
@ -26,9 +27,11 @@ type Props = {
|
|||
class CollectionNew extends React.Component<Props> {
|
||||
@observable name: string = '';
|
||||
@observable description: string = '';
|
||||
@observable icon: string = '';
|
||||
@observable color: string = '#4E5C6E';
|
||||
@observable private: boolean = false;
|
||||
@observable isSaving: boolean;
|
||||
hasOpenedIconPicker: boolean = false;
|
||||
|
||||
handleSubmit = async (ev: SyntheticEvent<>) => {
|
||||
ev.preventDefault();
|
||||
|
@ -37,6 +40,7 @@ class CollectionNew extends React.Component<Props> {
|
|||
{
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
icon: this.icon,
|
||||
color: this.color,
|
||||
private: this.private,
|
||||
},
|
||||
|
@ -56,6 +60,29 @@ class CollectionNew extends React.Component<Props> {
|
|||
|
||||
handleNameChange = (ev: SyntheticInputEvent<*>) => {
|
||||
this.name = ev.target.value;
|
||||
|
||||
// If the user hasn't picked an icon yet, go ahead and suggest one based on
|
||||
// the name of the collection. It's the little things sometimes.
|
||||
if (!this.hasOpenedIconPicker) {
|
||||
const keys = Object.keys(icons);
|
||||
for (const key of keys) {
|
||||
const icon = icons[key];
|
||||
const keywords = icon.keywords.split(' ');
|
||||
const namewords = this.name.toLowerCase().split(' ');
|
||||
const matches = intersection(namewords, keywords);
|
||||
|
||||
if (matches.length > 0) {
|
||||
this.icon = key;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.icon = 'collection';
|
||||
}
|
||||
};
|
||||
|
||||
handleIconPickerOpen = () => {
|
||||
this.hasOpenedIconPicker = true;
|
||||
};
|
||||
|
||||
handleDescriptionChange = getValue => {
|
||||
|
@ -66,8 +93,9 @@ class CollectionNew extends React.Component<Props> {
|
|||
this.private = ev.target.checked;
|
||||
};
|
||||
|
||||
handleColor = (color: string) => {
|
||||
handleChange = (color: string, icon: string) => {
|
||||
this.color = color;
|
||||
this.icon = icon;
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -88,7 +116,13 @@ class CollectionNew extends React.Component<Props> {
|
|||
autoFocus
|
||||
flex
|
||||
/>
|
||||
<ColorPicker onChange={this.handleColor} value={this.color} />
|
||||
|
||||
<IconPicker
|
||||
onOpen={this.handleIconPickerOpen}
|
||||
onChange={this.handleChange}
|
||||
color={this.color}
|
||||
icon={this.icon}
|
||||
/>
|
||||
</Flex>
|
||||
<InputRich
|
||||
label="Description"
|
||||
|
|
|
@ -121,11 +121,11 @@
|
|||
"mobx-react": "^5.4.2",
|
||||
"natural-sort": "^1.0.0",
|
||||
"nodemailer": "^4.4.0",
|
||||
"outline-icons": "^1.16.0",
|
||||
"outline-icons": "^1.18.0",
|
||||
"oy-vey": "^0.10.0",
|
||||
"pg": "^6.1.5",
|
||||
"pg-hstore": "2.3.2",
|
||||
"polished": "1.2.1",
|
||||
"polished": "3.6.4",
|
||||
"query-string": "^4.3.4",
|
||||
"randomstring": "1.1.5",
|
||||
"raw-loader": "^0.5.1",
|
||||
|
|
|
@ -30,7 +30,7 @@ const { authorize } = policy;
|
|||
const router = new Router();
|
||||
|
||||
router.post('collections.create', auth(), async ctx => {
|
||||
const { name, color, description, type } = ctx.body;
|
||||
const { name, color, description, icon, type } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
|
||||
|
@ -44,6 +44,7 @@ router.post('collections.create', auth(), async ctx => {
|
|||
let collection = await Collection.create({
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
color,
|
||||
type: type || 'atlas',
|
||||
teamId: user.teamId,
|
||||
|
@ -445,7 +446,7 @@ router.post('collections.export_all', auth(), async ctx => {
|
|||
});
|
||||
|
||||
router.post('collections.update', auth(), async ctx => {
|
||||
const { id, name, description, color } = ctx.body;
|
||||
const { id, name, description, icon, color } = ctx.body;
|
||||
const isPrivate = ctx.body.private;
|
||||
ctx.assertPresent(name, 'name is required');
|
||||
|
||||
|
@ -480,6 +481,7 @@ router.post('collections.update', auth(), async ctx => {
|
|||
|
||||
collection.name = name;
|
||||
collection.description = description;
|
||||
collection.icon = icon;
|
||||
collection.color = color;
|
||||
collection.private = isPrivate;
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('collections', 'icon', {
|
||||
type: Sequelize.TEXT,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('collections', 'icon');
|
||||
}
|
||||
};
|
|
@ -19,6 +19,7 @@ const Collection = sequelize.define(
|
|||
urlId: { type: DataTypes.STRING, unique: true },
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.STRING,
|
||||
icon: DataTypes.STRING,
|
||||
color: DataTypes.STRING,
|
||||
private: DataTypes.BOOLEAN,
|
||||
maintainerApprovalRequired: DataTypes.BOOLEAN,
|
||||
|
@ -46,6 +47,12 @@ const Collection = sequelize.define(
|
|||
}
|
||||
);
|
||||
|
||||
Collection.addHook('beforeSave', async model => {
|
||||
if (model.icon === 'collection') {
|
||||
model.icon = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Class methods
|
||||
|
||||
Collection.associate = models => {
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function present(collection: Collection) {
|
|||
url: collection.url,
|
||||
name: collection.name,
|
||||
description: collection.description,
|
||||
icon: collection.icon,
|
||||
color: collection.color || '#4E5C6E',
|
||||
type: collection.type,
|
||||
private: collection.private,
|
||||
|
|
|
@ -4,19 +4,14 @@ import { observer, inject } from 'mobx-react';
|
|||
import breakpoint from 'styled-components-breakpoint';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
CollectionIcon,
|
||||
PrivateCollectionIcon,
|
||||
PadlockIcon,
|
||||
GoToIcon,
|
||||
MoreIcon,
|
||||
} from 'outline-icons';
|
||||
import { PadlockIcon, GoToIcon, MoreIcon } from 'outline-icons';
|
||||
|
||||
import Document from 'models/Document';
|
||||
import CollectionsStore from 'stores/CollectionsStore';
|
||||
import { collectionUrl } from 'utils/routeHelpers';
|
||||
import Flex from 'shared/components/Flex';
|
||||
import BreadcrumbMenu from './BreadcrumbMenu';
|
||||
import CollectionIcon from 'components/CollectionIcon';
|
||||
|
||||
type Props = {
|
||||
document: Document,
|
||||
|
@ -56,11 +51,7 @@ const Breadcrumb = observer(({ document, collections, onlyText }: Props) => {
|
|||
return (
|
||||
<Wrapper justify="flex-start" align="center">
|
||||
<CollectionName to={collectionUrl(collection.id)}>
|
||||
{collection.private ? (
|
||||
<PrivateCollectionIcon color={collection.color} expanded />
|
||||
) : (
|
||||
<CollectionIcon color={collection.color} expanded />
|
||||
)}{' '}
|
||||
<CollectionIcon collection={collection} expanded />{' '}
|
||||
<span>{collection.name}</span>
|
||||
</CollectionName>
|
||||
{isNestedDocument && (
|
||||
|
|
|
@ -155,6 +155,7 @@ export const dark = {
|
|||
sidebarText: colors.slate,
|
||||
shadow: 'rgba(0, 0, 0, 0.6)',
|
||||
|
||||
menuBorder: lighten(0.1, colors.almostBlack),
|
||||
menuBackground: lighten(0.015, colors.almostBlack),
|
||||
menuShadow:
|
||||
'0 0 0 1px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.08), inset 0 0 1px rgba(255,255,255,.2)',
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -122,6 +122,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.9.2":
|
||||
version "7.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.1.tgz#b6eb75cac279588d3100baecd1b9894ea2840822"
|
||||
integrity sha512-nQbbCbQc9u/rpg1XCxoMYQTbSMVZjCDxErQ1ClCn9Pvcmv1lGads19ep0a2VsEiIJeHqjZley6EQGEC3Yo1xMA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/template@^7.8.3":
|
||||
version "7.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
|
||||
|
@ -7159,10 +7166,10 @@ osenv@^0.1.4:
|
|||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
outline-icons@^1.15.0, outline-icons@^1.16.0:
|
||||
version "1.16.0"
|
||||
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.16.0.tgz#0a71d2fe32170f0e00b8775681f0339f4fc8777a"
|
||||
integrity sha512-6bAk5rBGVtYiFP6AONx7NdmpFP+daKUJEev7PAjdfTVmE3bPXeSzzaGY51Y1XM8UdP5XqJICWncktRHeSfn1Pw==
|
||||
outline-icons@^1.15.0, outline-icons@^1.18.0:
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/outline-icons/-/outline-icons-1.18.0.tgz#2f128d668b0874725b5c0656a04fd73a7f8fe418"
|
||||
integrity sha512-odEYBLN+zFcTDhfs4af2RHUGneQBYbVgaBuI/I30BLaJQsKT2jBw2Shjie/HR8MYpZtgMwixP51XPPlGBSIqgw==
|
||||
|
||||
oy-vey@^0.10.0:
|
||||
version "0.10.0"
|
||||
|
@ -7549,10 +7556,12 @@ pn@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
|
||||
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
|
||||
|
||||
polished@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-1.2.1.tgz#83c18a85bf9d7023477cfc7049763b657d50f0f7"
|
||||
integrity sha1-g8GKhb+dcCNHfPxwSXY7ZX1Q8Pc=
|
||||
polished@3.6.4:
|
||||
version "3.6.4"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-3.6.4.tgz#cec6bc0fbffc5d6ce5799c85bcc1bca5e63f1dee"
|
||||
integrity sha512-21moJXCm/7EvjeKQz5w89QDDKNPCoimc83CqwZZGJluFdMXsFlMQl9lPA/OMRkoceZ19kU0anKlMgZmY7LJSJw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.9.2"
|
||||
|
||||
popper.js@^1.14.7:
|
||||
version "1.16.1"
|
||||
|
|
Reference in New Issue