diff --git a/client/package-lock.json b/client/package-lock.json index 7d63a89..b8c35fe 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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": { "version": "13.0.10", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.10.tgz", diff --git a/client/package.json b/client/package.json index 03718a5..1fb3ac0 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@types/react-dom": "^16.9.0", "@types/react-router": "^5.1.8", "@types/react-router-dom": "^5.1.5", + "@types/uuid": "^8.3.0", "jsurl": "^0.1.5", "lodash": "^4.17.20", "material-table": "^1.69.0", diff --git a/client/src/components/common/MenuEditText.tsx b/client/src/components/common/MenuEditText.tsx new file mode 100644 index 0000000..5939000 --- /dev/null +++ b/client/src/components/common/MenuEditText.tsx @@ -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 setInput(e.target.value)} + onKeyDown={(e: any) => { + if (e.key === 'Enter') { + // User submitted free-form value. + props.onSubmit(input); + e.preventDefault(); + } + }} + /> +} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/ManageTagMenu.tsx b/client/src/components/windows/manage_tags/ManageTagMenu.tsx index 19a0fef..f1dde4c 100644 --- a/client/src/components/windows/manage_tags/ManageTagMenu.tsx +++ b/client/src/components/windows/manage_tags/ManageTagMenu.tsx @@ -1,29 +1,7 @@ import React, { useState } from 'react'; import { Menu, MenuItem, TextField, Input } from '@material-ui/core'; import NestedMenuItem from "material-ui-nested-menu-item"; - -export function MenuEditText(props: { - label: string, - onSubmit: (s: string) => void, -}) { - const [input, setInput] = useState(""); - - return 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); - } - }} - /> -} +import MenuEditText from '../../common/MenuEditText'; export function PickTag(props: { tags: any[] diff --git a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx index 08380f6..d2ed83b 100644 --- a/client/src/components/windows/manage_tags/ManageTagsWindow.tsx +++ b/client/src/components/windows/manage_tags/ManageTagsWindow.tsx @@ -1,13 +1,14 @@ import React, { useEffect, useState } from 'react'; import { WindowState } from '../Windows'; -import { Box, Typography, Chip, IconButton, useTheme } from '@material-ui/core'; -import * as serverApi from '../../../api'; +import { Box, Typography, Chip, IconButton, useTheme, Button } from '@material-ui/core'; import LoyaltyIcon from '@material-ui/icons/Loyalty'; import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; import ManageTagMenu from './ManageTagMenu'; import ControlTagChanges, { TagChange, TagChangeType } from './TagChange'; import { queryTags } from '../../../lib/query/Backend'; +import NewTagMenu from './NewTagMenu'; +import { v4 as genUuid } from 'uuid'; var _ = require('lodash'); 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 ? + props.onSetExpanded(false)}> : + props.onSetExpanded(true)}>; +} + +export function CreateTagButton(props: any) { + return + + { }} /> + + + +} + export function SingleTag(props: { tag: any, prependElems: any[], @@ -89,7 +108,7 @@ export function SingleTag(props: { const hasChildren = 'children' in tag && tag.children.length > 0; const [menuPos, setMenuPos] = React.useState(null); - const [expanded, setExpanded] = useState(false); + const [expanded, setExpanded] = useState(false); const theme = useTheme(); const onOpenMenu = (e: any) => { @@ -106,10 +125,6 @@ export function SingleTag(props: { tagLabel = <>{tag.name}; } - const expandArrow = expanded ? - setExpanded(false)}> : - setExpanded(true)}>; - const TagChip = (props: any) => @@ -123,7 +138,7 @@ export function SingleTag(props: { return <> - {expandArrow} + {props.prependElems} @@ -189,7 +204,7 @@ export function SingleTag(props: { } function annotateTagsWithChanges(tags: Record, changes: TagChange[]) { - var retval: Record = tags; + var retval: Record = _.cloneDeep(tags); const applyDelete = (id: string) => { retval[id].proposeDelete = true; @@ -208,6 +223,18 @@ function annotateTagsWithChanges(tags: Record, changes: TagChange[] case TagChangeType.MoveTo: retval[change.id].proposedParent = change.parent; 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: throw new Error("Unimplemented tag change") } @@ -236,6 +263,18 @@ function applyTagsChanges(tags: Record, changes: TagChange[]) { retval[change.id].parentId = change.parent; if (change.parent === null) { delete retval[change.id].parentId; } 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: throw new Error("Unimplemented tag change") } @@ -248,6 +287,15 @@ export default function ManageTagsWindow(props: { dispatch: (action: any) => void, mainDispatch: (action: any) => void, }) { + const [newTagMenuPos, setNewTagMenuPos] = React.useState(null); + + const onOpenNewTagMenu = (e: any) => { + setNewTagMenuPos([e.clientX, e.clientY]) + }; + const onCloseNewTagMenu = () => { + setNewTagMenuPos(null); + }; + useEffect(() => { if (props.state.fetchedTags !== null) { return; @@ -269,50 +317,73 @@ export default function ManageTagsWindow(props: { null); const tags = organiseTags(tagsWithChanges, null); - return - - - - - Manage Tags - - {props.state.pendingChanges.length > 0 && - props.dispatch({ - type: ManageTagsWindowActions.SetPendingChanges, - value: [], + return <> + + + + + + Manage Tags + + {props.state.pendingChanges.length > 0 && + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [], + })} + onSave={() => { }} + getTagDetails={(id: string) => tagsWithChanges[id]} + /> + } + + {tags && tags.length && tags.map((tag: any) => { + return ; })} - onSave={() => { }} - getTagDetails={(id: string) => tagsWithChanges[id]} - /> - } - - {tags && tags.length && tags.map((tag: any) => { - return ; - })} + { onOpenNewTagMenu(e) }} /> + - + { + props.dispatch({ + type: ManageTagsWindowActions.SetPendingChanges, + value: [ + ...props.state.pendingChanges, + { + type: TagChangeType.Create, + id: genUuid(), + parent: parentId, + name: name, + } + ] + }) + }} + onClose={onCloseNewTagMenu} + changedTags={changedTags} + /> + } \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/NewTagMenu.tsx b/client/src/components/windows/manage_tags/NewTagMenu.tsx new file mode 100644 index 0000000..3d16315 --- /dev/null +++ b/client/src/components/windows/manage_tags/NewTagMenu.tsx @@ -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 <> + { + props.onCreate(s, props.parentId); + }} + /> + {props.tags.map((tag: any) => { + return + + + })} + +} + +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 + { + props.onClose(); + props.onCreate(n, v); + }} /> + +} \ No newline at end of file diff --git a/client/src/components/windows/manage_tags/TagChange.tsx b/client/src/components/windows/manage_tags/TagChange.tsx index 3b17e46..f5c2838 100644 --- a/client/src/components/windows/manage_tags/TagChange.tsx +++ b/client/src/components/windows/manage_tags/TagChange.tsx @@ -26,7 +26,7 @@ export function TagChangeDisplay(props: { getTagDetails: (id: string) => any, }) { 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 MakeTag = (props: { name: string }) => @@ -50,6 +50,10 @@ export function TagChangeDisplay(props: { : ; return Move {MainTag} from {OldParent} to {NewParent} + case TagChangeType.Create: + return props.change.parent ? + Create {MainTag} under : + Create {MainTag} default: throw new Error("Unhandled tag change type") }