|
|
|
@ -14,13 +14,14 @@ import { useHistory } from 'react-router'; |
|
|
|
|
import { NotLoggedInError, handleNotLoggedIn } from '../../../lib/backend/request'; |
|
|
|
|
import { useAuth } from '../../../lib/useAuth'; |
|
|
|
|
import * as serverApi from '../../../api/api'; |
|
|
|
|
import { Id, QueryResponseTagDetails, Tag, Name } from '../../../api/api'; |
|
|
|
|
var _ = require('lodash'); |
|
|
|
|
|
|
|
|
|
export interface ManageTagsWindowState extends WindowState { |
|
|
|
|
// Tags are indexed by a string ID. This can be a stringified MuDBase ID integer,
|
|
|
|
|
// or a UID for tags which only exist in the front-end and haven't been committed
|
|
|
|
|
// to the database.
|
|
|
|
|
fetchedTags: Record<string, any> | null, |
|
|
|
|
fetchedTags: Record<string, ManagedTag> | null, |
|
|
|
|
pendingChanges: TagChange[], |
|
|
|
|
alert: ReactFragment | null, // For notifications such as errors
|
|
|
|
|
} |
|
|
|
@ -61,36 +62,50 @@ export function ManageTagsWindowReducer(state: ManageTagsWindowState, action: an |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function organiseTags(allTags: Record<string, any>, fromId: string | null): any[] { |
|
|
|
|
const base = Object.values(allTags).filter((tag: any) => { |
|
|
|
|
var par: any = ("proposedParent" in tag) ? tag.proposedParent : tag.parentId; |
|
|
|
|
export function organiseTags(allTags: Record<string, ManagedTag>, fromId: string | null):
|
|
|
|
|
ManagedTag[] { |
|
|
|
|
const base = Object.values(allTags).filter((tag: ManagedTag) => { |
|
|
|
|
var par = ("proposedParent" in tag) ? tag.proposedParent : tag.parentId; |
|
|
|
|
|
|
|
|
|
return (fromId === null && !par) || |
|
|
|
|
(par && par === fromId) |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
return base.map((tag: any) => { |
|
|
|
|
return base.map((tag: ManagedTag) => { |
|
|
|
|
return { |
|
|
|
|
...tag, |
|
|
|
|
children: organiseTags(allTags, tag.tagId), |
|
|
|
|
children: organiseTags(allTags, tag.strTagId), |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function getAllTags() { |
|
|
|
|
// Work with strings so we can have temporary randomized IDs.
|
|
|
|
|
type ManagedTag = (Tag & Name & { |
|
|
|
|
id?: number, |
|
|
|
|
strTagId: string, |
|
|
|
|
strParentId: string | null, |
|
|
|
|
strChildIds: string[], |
|
|
|
|
proposeDelete?: boolean, |
|
|
|
|
proposedName?: string, |
|
|
|
|
proposedParent?: string | null, |
|
|
|
|
proposedMergeInto?: string, |
|
|
|
|
isNewTag?: boolean, |
|
|
|
|
children?: ManagedTag[], |
|
|
|
|
}); |
|
|
|
|
export async function getAllTags(): Promise<Record<string, ManagedTag>> { |
|
|
|
|
return (async () => { |
|
|
|
|
var retval: Record<string, any> = {}; |
|
|
|
|
const tags: any = await queryTags( |
|
|
|
|
var retval: Record<string, ManagedTag> = {}; |
|
|
|
|
const tags: QueryResponseTagDetails[] = await queryTags( |
|
|
|
|
undefined, 0, -1, serverApi.QueryResponseType.Details, |
|
|
|
|
); |
|
|
|
|
// Convert numeric IDs to string IDs because that is
|
|
|
|
|
// what we work with within this component.
|
|
|
|
|
tags.forEach((tag: any) => { |
|
|
|
|
retval[tag.tagId.toString()] = { |
|
|
|
|
) as QueryResponseTagDetails[]; |
|
|
|
|
tags.forEach((tag: QueryResponseTagDetails) => { |
|
|
|
|
retval[tag.id.toString()] = { |
|
|
|
|
...tag, |
|
|
|
|
tagId: tag.tagId && tag.tagId.toString(), |
|
|
|
|
parentId: tag.parentId && tag.parentId.toString(), |
|
|
|
|
childIds: tag.childIds && tag.childIds.map((c: number) => c.toString()), |
|
|
|
|
strTagId: tag.id.toString(), |
|
|
|
|
strParentId: tag.parentId?.toString() || null, |
|
|
|
|
strChildIds: tags |
|
|
|
|
.filter((t: QueryResponseTagDetails) => t.parentId == tag.id) |
|
|
|
|
.map((t: QueryResponseTagDetails) => t.id.toString() ) |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
return retval; |
|
|
|
@ -116,14 +131,14 @@ export function CreateTagButton(props: any) { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function SingleTag(props: { |
|
|
|
|
tag: any, |
|
|
|
|
tag: ManagedTag, |
|
|
|
|
prependElems: any[], |
|
|
|
|
dispatch: (action: any) => void, |
|
|
|
|
state: ManageTagsWindowState, |
|
|
|
|
changedTags: any[], |
|
|
|
|
changedTags: ManagedTag[], |
|
|
|
|
}) { |
|
|
|
|
const tag = props.tag; |
|
|
|
|
const hasChildren = 'children' in tag && tag.children.length > 0; |
|
|
|
|
const hasChildren = tag.children && tag.children.length > 0; |
|
|
|
|
|
|
|
|
|
const [menuPos, setMenuPos] = React.useState<null | number[]>(null); |
|
|
|
|
const [expanded, setExpanded] = useState<boolean>(true); |
|
|
|
@ -163,9 +178,9 @@ export function SingleTag(props: { |
|
|
|
|
{props.prependElems} |
|
|
|
|
<TagChip transparent={tag.proposeDelete} label={tagLabel} /> |
|
|
|
|
</Box> |
|
|
|
|
{hasChildren && expanded && tag.children |
|
|
|
|
.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
|
|
|
|
.map((child: any) => <SingleTag |
|
|
|
|
{expanded && tag.children && tag.children |
|
|
|
|
.sort((a: ManagedTag, b: ManagedTag) => a.name.localeCompare(b.name)) |
|
|
|
|
.map((child: ManagedTag) => <SingleTag |
|
|
|
|
tag={child} |
|
|
|
|
prependElems={[...props.prependElems, |
|
|
|
|
<TagChip transparent={true} label={tagLabel} />, |
|
|
|
@ -179,7 +194,7 @@ export function SingleTag(props: { |
|
|
|
|
open={menuPos !== null} |
|
|
|
|
onClose={onCloseMenu} |
|
|
|
|
onOpenTag={() => { |
|
|
|
|
history.push('/tag/' + tag.tagId); |
|
|
|
|
history.push('/tag/' + tag.strTagId); |
|
|
|
|
}} |
|
|
|
|
onRename={(s: string) => { |
|
|
|
|
props.dispatch({ |
|
|
|
@ -189,7 +204,7 @@ export function SingleTag(props: { |
|
|
|
|
{ |
|
|
|
|
type: TagChangeType.Rename, |
|
|
|
|
name: s, |
|
|
|
|
id: tag.tagId, |
|
|
|
|
id: tag.strTagId, |
|
|
|
|
} |
|
|
|
|
] |
|
|
|
|
}) |
|
|
|
@ -205,7 +220,7 @@ export function SingleTag(props: { |
|
|
|
|
...props.state.pendingChanges, |
|
|
|
|
{ |
|
|
|
|
type: TagChangeType.Delete, |
|
|
|
|
id: tag.tagId, |
|
|
|
|
id: tag.strTagId, |
|
|
|
|
} |
|
|
|
|
] |
|
|
|
|
}) |
|
|
|
@ -221,7 +236,7 @@ export function SingleTag(props: { |
|
|
|
|
...props.state.pendingChanges, |
|
|
|
|
{ |
|
|
|
|
type: TagChangeType.MoveTo, |
|
|
|
|
id: tag.tagId, |
|
|
|
|
id: tag.strTagId, |
|
|
|
|
parent: to, |
|
|
|
|
} |
|
|
|
|
] |
|
|
|
@ -238,7 +253,7 @@ export function SingleTag(props: { |
|
|
|
|
...props.state.pendingChanges, |
|
|
|
|
{ |
|
|
|
|
type: TagChangeType.MergeTo, |
|
|
|
|
id: tag.tagId, |
|
|
|
|
id: tag.strTagId, |
|
|
|
|
into: into, |
|
|
|
|
} |
|
|
|
|
] |
|
|
|
@ -254,13 +269,14 @@ export function SingleTag(props: { |
|
|
|
|
</> |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[]) { |
|
|
|
|
var retval: Record<string, any> = _.cloneDeep(tags); |
|
|
|
|
function annotateTagsWithChanges(tags: Record<string, ManagedTag>, changes: TagChange[]) |
|
|
|
|
: Record<string, ManagedTag> { |
|
|
|
|
var retval: Record<string, ManagedTag> = _.cloneDeep(tags); |
|
|
|
|
|
|
|
|
|
const applyDelete = (id: string) => { |
|
|
|
|
retval[id].proposeDelete = true; |
|
|
|
|
Object.values(tags).filter((t: any) => t.parentId === id) |
|
|
|
|
.forEach((child: any) => applyDelete(child.tagId)); |
|
|
|
|
Object.values(tags).filter((t: ManagedTag) => t.strParentId === id) |
|
|
|
|
.forEach((child: ManagedTag) => applyDelete(child.strTagId)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
changes.forEach((change: TagChange) => { |
|
|
|
@ -278,15 +294,20 @@ function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[] |
|
|
|
|
retval[change.id].proposedMergeInto = change.into; |
|
|
|
|
break; |
|
|
|
|
case TagChangeType.Create: |
|
|
|
|
if (!change.name) { |
|
|
|
|
throw new Error("Trying to create a tag without a name"); |
|
|
|
|
} |
|
|
|
|
retval[change.id] = { |
|
|
|
|
mbApi_typename: 'tag', |
|
|
|
|
isNewTag: true, |
|
|
|
|
name: change.name, |
|
|
|
|
parentId: change.parent, |
|
|
|
|
tagId: change.id, |
|
|
|
|
strTagId: change.id, |
|
|
|
|
strParentId: change.parent || null, |
|
|
|
|
strChildIds: [], |
|
|
|
|
} |
|
|
|
|
if (change.parent) { |
|
|
|
|
retval[change.parent].childIds = |
|
|
|
|
[...retval[change.parent].childIds, change.id] |
|
|
|
|
retval[change.parent].strChildIds = |
|
|
|
|
[...retval[change.parent].strChildIds, change.id] |
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
default: |
|
|
|
@ -296,12 +317,12 @@ function annotateTagsWithChanges(tags: Record<string, any>, changes: TagChange[] |
|
|
|
|
return retval; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function applyTagsChanges(tags: Record<string, any>, changes: TagChange[]) { |
|
|
|
|
function applyTagsChanges(tags: Record<string, ManagedTag>, changes: TagChange[]) { |
|
|
|
|
var retval = _.cloneDeep(tags); |
|
|
|
|
|
|
|
|
|
const applyDelete = (id: string) => { |
|
|
|
|
Object.values(tags).filter((t: any) => t.parentId === id) |
|
|
|
|
.forEach((child: any) => applyDelete(child.tagId)); |
|
|
|
|
Object.values(tags).filter((t: ManagedTag) => t.strParentId === id) |
|
|
|
|
.forEach((child: ManagedTag) => applyDelete(child.strTagId)); |
|
|
|
|
delete retval[id].proposeDelete; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -447,8 +468,8 @@ export function ManageTagsWindowControlled(props: { |
|
|
|
|
width="80%" |
|
|
|
|
> |
|
|
|
|
{tags && tags.length && tags |
|
|
|
|
.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
|
|
|
|
.map((tag: any) => { |
|
|
|
|
.sort((a: ManagedTag, b: ManagedTag) => a.name.localeCompare(b.name)) |
|
|
|
|
.map((tag: ManagedTag) => { |
|
|
|
|
return <SingleTag |
|
|
|
|
tag={tag} |
|
|
|
|
prependElems={[]} |
|
|
|
|