|
|
@ -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} |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</> |
|
|
|
} |
|
|
|
} |