Able to create new tag

pull/24/head
Sander Vocke 5 years ago
parent 52865497cd
commit c8a39c9224
  1. 5
      client/package-lock.json
  2. 1
      client/package.json
  3. 23
      client/src/components/common/MenuEditText.tsx
  4. 24
      client/src/components/windows/manage_tags/ManageTagMenu.tsx
  5. 177
      client/src/components/windows/manage_tags/ManageTagsWindow.tsx
  6. 54
      client/src/components/windows/manage_tags/NewTagMenu.tsx
  7. 6
      client/src/components/windows/manage_tags/TagChange.tsx

@ -2066,6 +2066,11 @@
} }
} }
}, },
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ=="
},
"@types/yargs": { "@types/yargs": {
"version": "13.0.10", "version": "13.0.10",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz",

@ -15,6 +15,7 @@
"@types/react-dom": "^16.9.0", "@types/react-dom": "^16.9.0",
"@types/react-router": "^5.1.8", "@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5", "@types/react-router-dom": "^5.1.5",
"@types/uuid": "^8.3.0",
"jsurl": "^0.1.5", "jsurl": "^0.1.5",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"material-table": "^1.69.0", "material-table": "^1.69.0",

@ -0,0 +1,23 @@
import React, { useState } from 'react';
import { TextField } from '@material-ui/core';
export default function MenuEditText(props: {
label: string,
onSubmit: (s: string) => void,
}) {
const [input, setInput] = useState("");
return <TextField
label={props.label}
variant="outlined"
value={input}
onChange={(e: any) => setInput(e.target.value)}
onKeyDown={(e: any) => {
if (e.key === 'Enter') {
// User submitted free-form value.
props.onSubmit(input);
e.preventDefault();
}
}}
/>
}

@ -1,29 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; import { Menu, MenuItem, TextField, Input } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item"; import NestedMenuItem from "material-ui-nested-menu-item";
import MenuEditText from '../../common/MenuEditText';
export function MenuEditText(props: {
label: string,
onSubmit: (s: string) => void,
}) {
const [input, setInput] = useState("");
return <TextField
label={props.label}
variant="outlined"
value={input}
onChange={(e: any) => setInput(e.target.value)}
onKeyDown={(e: any) => {
// Prevent the event from propagating, because
// that would trigger keyboard navigation of the menu.
e.stopPropagation();
if (e.key === 'Enter') {
// User submitted free-form value.
props.onSubmit(input);
}
}}
/>
}
export function PickTag(props: { export function PickTag(props: {
tags: any[] tags: any[]

@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { WindowState } from '../Windows'; import { WindowState } from '../Windows';
import { Box, Typography, Chip, IconButton, useTheme } from '@material-ui/core'; import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core';
import * as serverApi from '../../../api';
import LoyaltyIcon from '@material-ui/icons/Loyalty'; import LoyaltyIcon from '@material-ui/icons/Loyalty';
import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import ArrowRightIcon from '@material-ui/icons/ArrowRight';
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
import ManageTagMenu from './ManageTagMenu'; import ManageTagMenu from './ManageTagMenu';
import ControlTagChanges, { TagChange, TagChangeType } from './TagChange'; import ControlTagChanges, { TagChange, TagChangeType } from './TagChange';
import { queryTags } from '../../../lib/query/Backend'; import { queryTags } from '../../../lib/query/Backend';
import NewTagMenu from './NewTagMenu';
import { v4 as genUuid } from 'uuid';
var _ = require('lodash'); var _ = require('lodash');
export interface ManageTagsWindowState extends WindowState { export interface ManageTagsWindowState extends WindowState {
@ -78,6 +79,24 @@ export async function getAllTags() {
})(); })();
} }
export function ExpandArrow(props: {
expanded: boolean,
onSetExpanded: (v: boolean) => void,
}) {
return props.expanded ?
<IconButton size="small" onClick={() => props.onSetExpanded(false)}><ArrowDropDownIcon /></IconButton> :
<IconButton size="small" onClick={() => props.onSetExpanded(true)}><ArrowRightIcon /></IconButton>;
}
export function CreateTagButton(props: any) {
return <Box display="flex">
<Box visibility='hidden'>
<ExpandArrow expanded={false} onSetExpanded={(v: boolean) => { }} />
</Box>
<Button style={{ textTransform: 'none' }} variant="outlined" {...props}>New Tag...</Button>
</Box>
}
export function SingleTag(props: { export function SingleTag(props: {
tag: any, tag: any,
prependElems: any[], prependElems: any[],
@ -89,7 +108,7 @@ export function SingleTag(props: {
const hasChildren = 'children' in tag && tag.children.length > 0; const hasChildren = 'children' in tag && tag.children.length > 0;
const [menuPos, setMenuPos] = React.useState<null | number[]>(null); const [menuPos, setMenuPos] = React.useState<null | number[]>(null);
const [expanded, setExpanded] = useState<Boolean>(false); const [expanded, setExpanded] = useState<boolean>(false);
const theme = useTheme(); const theme = useTheme();
const onOpenMenu = (e: any) => { const onOpenMenu = (e: any) => {
@ -106,10 +125,6 @@ export function SingleTag(props: {
tagLabel = <><del style={{ color: theme.palette.text.secondary }}>{tag.name}</del></>; tagLabel = <><del style={{ color: theme.palette.text.secondary }}>{tag.name}</del></>;
} }
const expandArrow = expanded ?
<IconButton size="small" onClick={() => setExpanded(false)}><ArrowDropDownIcon /></IconButton> :
<IconButton size="small" onClick={() => setExpanded(true)}><ArrowRightIcon /></IconButton>;
const TagChip = (props: any) => <Box const TagChip = (props: any) => <Box
style={{ opacity: props.transparent ? 0.5 : 1.0 }} style={{ opacity: props.transparent ? 0.5 : 1.0 }}
> >
@ -123,7 +138,7 @@ export function SingleTag(props: {
return <> return <>
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
<Box visibility={hasChildren ? 'visible' : 'hidden'}> <Box visibility={hasChildren ? 'visible' : 'hidden'}>
{expandArrow} <ExpandArrow expanded={expanded} onSetExpanded={setExpanded} />
</Box> </Box>
{props.prependElems} {props.prependElems}
<TagChip transparent={tag.proposeDelete} label={tagLabel} /> <TagChip transparent={tag.proposeDelete} label={tagLabel} />
@ -189,7 +204,7 @@ export function SingleTag(props: {
} }
function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[]) { function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[]) {
var retval: Record<string, any> = tags; var retval: Record<string, any> = _.cloneDeep(tags);
const applyDelete = (id: string) => { const applyDelete = (id: string) => {
retval[id].proposeDelete = true; retval[id].proposeDelete = true;
@ -208,6 +223,18 @@ function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[]
case TagChangeType.MoveTo: case TagChangeType.MoveTo:
retval[change.id].proposedParent = change.parent; retval[change.id].proposedParent = change.parent;
break; break;
case TagChangeType.Create:
retval[change.id] = {
isNewTag: true,
name: change.name,
parentId: change.parent,
tagId: change.id,
}
if (change.parent) {
retval[change.parent].childIds =
[...retval[change.parent].childIds, change.id]
}
break;
default: default:
throw new Error("Unimplemented tag change") throw new Error("Unimplemented tag change")
} }
@ -236,6 +263,18 @@ function applyTagsChanges(tags: Record<string, any>, changes: TagChange[]) {
retval[change.id].parentId = change.parent; retval[change.id].parentId = change.parent;
if (change.parent === null) { delete retval[change.id].parentId; } if (change.parent === null) { delete retval[change.id].parentId; }
break; break;
case TagChangeType.Create:
retval[change.id] = {
name: change.name,
tagId: change.id,
parentId: change.parent,
isNewTag: true,
}
if (change.parent) {
retval[change.parent].childIds =
[...retval[change.parent].childIds, change.id]
}
break;
default: default:
throw new Error("Unimplemented tag change") throw new Error("Unimplemented tag change")
} }
@ -248,6 +287,15 @@ export default function ManageTagsWindow(props: {
dispatch: (action: any) => void, dispatch: (action: any) => void,
mainDispatch: (action: any) => void, mainDispatch: (action: any) => void,
}) { }) {
const [newTagMenuPos, setNewTagMenuPos] = React.useState<null | number[]>(null);
const onOpenNewTagMenu = (e: any) => {
setNewTagMenuPos([e.clientX, e.clientY])
};
const onCloseNewTagMenu = () => {
setNewTagMenuPos(null);
};
useEffect(() => { useEffect(() => {
if (props.state.fetchedTags !== null) { if (props.state.fetchedTags !== null) {
return; return;
@ -269,50 +317,73 @@ export default function ManageTagsWindow(props: {
null); null);
const tags = organiseTags(tagsWithChanges, null); const tags = organiseTags(tagsWithChanges, null);
return <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap"> return <>
<Box <Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
m={1} <Box
mt={4} m={1}
width="80%" mt={4}
> width="80%"
<LoyaltyIcon style={{ fontSize: 80 }} /> >
</Box> <LoyaltyIcon style={{ fontSize: 80 }} />
<Box </Box>
m={1} <Box
mt={4} m={1}
width="80%" mt={4}
> width="80%"
<Typography variant="h4">Manage Tags</Typography> >
</Box> <Typography variant="h4">Manage Tags</Typography>
{props.state.pendingChanges.length > 0 && <Box </Box>
m={1} {props.state.pendingChanges.length > 0 && <Box
mt={4} m={1}
width="80%" mt={4}
> width="80%"
<ControlTagChanges >
changes={props.state.pendingChanges} <ControlTagChanges
onDiscard={() => props.dispatch({ changes={props.state.pendingChanges}
type: ManageTagsWindowActions.SetPendingChanges, onDiscard={() => props.dispatch({
value: [], type: ManageTagsWindowActions.SetPendingChanges,
value: [],
})}
onSave={() => { }}
getTagDetails={(id: string) => tagsWithChanges[id]}
/>
</Box>}
<Box
m={1}
mt={4}
width="80%"
>
{tags && tags.length && tags.map((tag: any) => {
return <SingleTag
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
state={props.state}
changedTags={changedTags}
/>;
})} })}
onSave={() => { }} <Box mt={3}><CreateTagButton onClick={(e: any) => { onOpenNewTagMenu(e) }} /></Box>
getTagDetails={(id: string) => tagsWithChanges[id]} </Box>
/>
</Box>}
<Box
m={1}
mt={4}
width="80%"
>
{tags && tags.length && tags.map((tag: any) => {
return <SingleTag
tag={tag}
prependElems={[]}
dispatch={props.dispatch}
state={props.state}
changedTags={changedTags}
/>;
})}
</Box> </Box>
</Box> <NewTagMenu
position={newTagMenuPos}
open={newTagMenuPos !== null}
onCreate={(name: string, parentId: string | null) => {
props.dispatch({
type: ManageTagsWindowActions.SetPendingChanges,
value: [
...props.state.pendingChanges,
{
type: TagChangeType.Create,
id: genUuid(),
parent: parentId,
name: name,
}
]
})
}}
onClose={onCloseNewTagMenu}
changedTags={changedTags}
/>
</>
} }

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Menu, MenuItem, TextField, Input } from '@material-ui/core';
import NestedMenuItem from "material-ui-nested-menu-item";
import MenuEditText from '../../common/MenuEditText';
export function PickCreateTag(props: {
tags: any[],
open: boolean,
parentId: string | null,
onCreate: (name: string, parentId: string | null) => void,
}) {
return <>
<MenuEditText
label="Name"
onSubmit={(s: string) => {
props.onCreate(s, props.parentId);
}}
/>
{props.tags.map((tag: any) => {
return <NestedMenuItem
parentMenuOpen={props.open}
label={tag.name}
>
<PickCreateTag tags={tag.children} open={props.open} parentId={tag.tagId} onCreate={props.onCreate} />
</NestedMenuItem>
})}
</>
}
export default function NewTagMenu(props: {
position: null | number[],
open: boolean,
onCreate: (name: string, parentId: string | null) => void,
onClose: () => void,
changedTags: any[], // Tags organized hierarchically with "children" fields
}) {
const pos = props.open && props.position ?
{ left: props.position[0], top: props.position[1] }
: { left: 0, top: 0 }
return <Menu
open={props.open}
anchorReference="anchorPosition"
anchorPosition={pos}
keepMounted
onClose={props.onClose}
>
<PickCreateTag tags={props.changedTags} open={props.open} parentId={null} onCreate={(n: string, v: string | null) => {
props.onClose();
props.onCreate(n, v);
}} />
</Menu>
}

@ -26,7 +26,7 @@ export function TagChangeDisplay(props: {
getTagDetails: (id: string) => any, getTagDetails: (id: string) => any,
}) { }) {
const tag = props.getTagDetails(props.change.id); const tag = props.getTagDetails(props.change.id);
const oldParent = tag.parentId ? props.getTagDetails(tag.parentId.toString()) : null; const oldParent = tag.parentId ? props.getTagDetails(tag.parentId) : null;
const newParent = props.change.parent ? props.getTagDetails(props.change.parent) : null; const newParent = props.change.parent ? props.getTagDetails(props.change.parent) : null;
const MakeTag = (props: { name: string }) => <Chip label={props.name} /> const MakeTag = (props: { name: string }) => <Chip label={props.name} />
@ -50,6 +50,10 @@ export function TagChangeDisplay(props: {
<MakeTag name={newParent ? newParent.name : "/"} /> : <MakeTag name={newParent ? newParent.name : "/"} /> :
<CircularProgress />; <CircularProgress />;
return <Typography>Move {MainTag} from {OldParent} to {NewParent}</Typography> return <Typography>Move {MainTag} from {OldParent} to {NewParent}</Typography>
case TagChangeType.Create:
return props.change.parent ?
<Typography>Create {MainTag} under <MakeTag name={newParent.name}/></Typography> :
<Typography>Create {MainTag}</Typography>
default: default:
throw new Error("Unhandled tag change type") throw new Error("Unhandled tag change type")
} }

Loading…
Cancel
Save