diff --git a/resources/style/controls/popover.scss b/resources/style/controls/popover.scss index 4707430a..9a3d7d01 100644 --- a/resources/style/controls/popover.scss +++ b/resources/style/controls/popover.scss @@ -180,7 +180,7 @@ dialog[role='alertdialog'] { } } -#tag-remove-overview { +.tag-overview { overflow-y: auto; max-height: 300px; } \ No newline at end of file diff --git a/src/frontend/components/RemovalAlert.tsx b/src/frontend/components/RemovalAlert.tsx index 77f2c6ef..6a322f32 100644 --- a/src/frontend/components/RemovalAlert.tsx +++ b/src/frontend/components/RemovalAlert.tsx @@ -75,7 +75,7 @@ export const TagRemoval = observer((props: IRemovalProps) => { information="Deleting tags or collections will permanently remove them from Allusion." body={ tagsToRemove.length > 0 && ( -
+

Selected Tags

{tagsToRemove}
diff --git a/src/frontend/containers/ContentView/menu-items.tsx b/src/frontend/containers/ContentView/menu-items.tsx index 4e069fc8..76115c2f 100644 --- a/src/frontend/containers/ContentView/menu-items.tsx +++ b/src/frontend/containers/ContentView/menu-items.tsx @@ -308,6 +308,7 @@ export const ExternalAppMenuItems = observer(({ file }: { file: ClientFile }) => export const FileTagMenuItems = observer(({ file, tag }: { file?: ClientFile; tag: ClientTag }) => { const { uiStore } = useStore(); + const ctxTags = uiStore.getTagContextItems(tag.id); return ( <> + uiStore.openTagMovePanel(tag)} + text="Move Tag To" + icon={IconSet.TAG_GROUP} + /> + uiStore.openTagMergePanel(tag)} + text="Merge Tag With" + icon={IconSet.TAG_GROUP} + disabled={ctxTags.some((tag) => tag.subTags.length > 0)} + /> file && file.removeTag(tag)} - text="Unassign Tag from File" + text="Unassign Tag From File" icon={IconSet.TAG_BLANCO} /> @@ -338,6 +350,7 @@ export const EditorTagSummaryItems = ({ beforeSelect: () => void; }) => { const { uiStore } = useStore(); + const ctxTags = uiStore.getTagContextItems(tag.id); return ( <> + uiStore.openTagMovePanel(tag)} + text="Move To" + icon={IconSet.TAG_GROUP} + /> + uiStore.openTagMergePanel(tag)} + text="Merge With" + icon={IconSet.TAG_GROUP} + disabled={ctxTags.some((tag) => tag.subTags.length > 0)} + /> ); }; diff --git a/src/frontend/containers/Outliner/TagsPanel/ContextMenu.tsx b/src/frontend/containers/Outliner/TagsPanel/ContextMenu.tsx index ebadc194..f4448af6 100644 --- a/src/frontend/containers/Outliner/TagsPanel/ContextMenu.tsx +++ b/src/frontend/containers/Outliner/TagsPanel/ContextMenu.tsx @@ -140,8 +140,8 @@ export const TagItemContextMenu = observer((props: IContextMenuProps) => { onClick={tag.toggleHidden} /> dispatch(Factory.confirmMerge(tag))} - text="Merge with" + onClick={() => uiStore.openTagMergePanel(tag)} + text="Merge With" icon={IconSet.TAG_GROUP} disabled={ctxTags.some((tag) => tag.subTags.length > 0)} /> @@ -186,7 +186,7 @@ export const TagItemContextMenu = observer((props: IContextMenuProps) => { disabled={pos === tag.parent.subTags.length} /> dispatch(Factory.confirmMove(tag))} + onClick={() => uiStore.openTagMovePanel(tag)} text="Move To" icon={IconSet.TAG_GROUP} /> diff --git a/src/frontend/containers/Outliner/TagsPanel/TagMerge.tsx b/src/frontend/containers/Outliner/TagsPanel/TagMerge.tsx index ccf3ddeb..c1fd9f3b 100644 --- a/src/frontend/containers/Outliner/TagsPanel/TagMerge.tsx +++ b/src/frontend/containers/Outliner/TagsPanel/TagMerge.tsx @@ -1,37 +1,37 @@ import { observer } from 'mobx-react-lite'; import React, { useEffect, useState } from 'react'; -import { Button, Checkbox, IconSet, Tag } from 'widgets'; +import { Button, IconSet, Tag } from 'widgets'; import { Dialog } from 'widgets/popovers'; import { useStore } from '../../../contexts/StoreContext'; import { ClientTag } from '../../../entities/Tag'; import { Placement } from '@floating-ui/core'; import { TagSelector } from 'src/frontend/components/TagSelector'; -import { Menu, MenuCheckboxItem } from 'widgets/menus'; - -interface TagMergeProps { - tag: ClientTag; - onClose: () => void; -} +import { MenuCheckboxItem } from 'widgets/menus'; const ADD_AS_ALIAS_ID = 'merge-add-as-alias'; const FALLBACK_PLACEMENTS: Placement[] = ['bottom']; /** this component is only shown when all tags in the context do not have child-tags */ -export const TagMerge = observer(({ tag, onClose }: TagMergeProps) => { +export const TagMerge = observer(() => { const { tagStore, uiStore } = useStore(); + const [selectedTag, setSelectedTag] = useState(); const [addAsAliasEnabled, setAddAsAlias] = useState( localStorage.getItem(ADD_AS_ALIAS_ID) == 'true', ); + const tag = uiStore.tagToMerge; + const isOpen = tag !== undefined; + const ctxTags = uiStore.getTagContextItems(tag?.id); + + const onClose = () => { + uiStore.closeTagMergePanel(); + }; + useEffect(() => { localStorage.setItem(ADD_AS_ALIAS_ID, JSON.stringify(addAsAliasEnabled)); }, [addAsAliasEnabled]); - const ctxTags = uiStore.getTagContextItems(tag.id); - - const [selectedTag, setSelectedTag] = useState(); - const mergingWithSelf = ctxTags.some((t) => t.id === selectedTag?.id); const merge = () => { @@ -45,9 +45,13 @@ export const TagMerge = observer(({ tag, onClose }: TagMergeProps) => { const plur = ctxTags.length === 1 ? '' : 's'; + if (!isOpen) { + return null; + } + return ( {

- {ctxTags.map((tag) => ( - - ))} +
+ {ctxTags.map((tag) => ( + + ))} +

@@ -75,7 +81,7 @@ export const TagMerge = observer(({ tag, onClose }: TagMergeProps) => {
void; -} - const FALLBACK_PLACEMENTS: Placement[] = ['bottom']; -export const TagsMoveTo = observer(({ tag, onClose }: TagMoveToProps) => { +export const TagsMoveTo = observer(() => { const { uiStore } = useStore(); const [selectedTag, setSelectedTag] = useState(); - const ctxTags = uiStore.getTagContextItems(tag.id); + const tag = uiStore.tagToMove; + const ctxTags = uiStore.getTagContextItems(tag?.id); + const isOpen = tag !== undefined; const isMulti = ctxTags.length > 1; const plur = isMulti ? 's' : ''; + const onClose = () => { + uiStore.closeTagMovePanel(); + }; + const handleMove = () => { let count = 0; if (selectedTag !== undefined) { @@ -47,9 +48,13 @@ export const TagsMoveTo = observer(({ tag, onClose }: TagMoveToProps) => { onClose(); }; + if (!isOpen) { + return null; + } + return ( {

- {ctxTags.map((tag) => ( - - ))} +
+ {ctxTags.map((tag) => ( + + ))} +

) => { expansion: {}, editableNode: undefined, deletableNode: undefined, - mergableNode: undefined, - movableNode: undefined, }); const dndData = useTagDnD(); const vTreeRef = useRef(null); @@ -859,14 +855,6 @@ const TagsTree = observer((props: Partial) => { onClose={() => dispatch(Factory.abortDeletion())} /> )} - - {state.mergableNode && ( - dispatch(Factory.abortMerge())} /> - )} - - {state.movableNode && ( - dispatch(Factory.abortMove())} /> - )} ); }); diff --git a/src/frontend/containers/Outliner/TagsPanel/index.tsx b/src/frontend/containers/Outliner/TagsPanel/index.tsx index 498d3808..06bf0a02 100644 --- a/src/frontend/containers/Outliner/TagsPanel/index.tsx +++ b/src/frontend/containers/Outliner/TagsPanel/index.tsx @@ -9,6 +9,9 @@ import { ClientTagSearchCriteria } from '../../../entities/SearchCriteria'; import { useAction } from '../../../hooks/mobx'; import { comboMatches, getKeyCombo, parseKeyCombo } from '../../../hotkeyParser'; import TagsTree from './TagsTree'; +import { TagPropertiesEditor } from './TagPropertiesEditor'; +import { TagsMoveTo } from './TagsMoveTo'; +import { TagMerge } from './TagMerge'; // Tooltip info const enum TooltipInfo { @@ -86,7 +89,14 @@ const TagsPanel = (props: Partial) => { } }); - return ; + return ( + <> + + + + + + ); }; export default TagsPanel; diff --git a/src/frontend/containers/Outliner/TagsPanel/state.ts b/src/frontend/containers/Outliner/TagsPanel/state.ts index 891b0179..384ba77f 100644 --- a/src/frontend/containers/Outliner/TagsPanel/state.ts +++ b/src/frontend/containers/Outliner/TagsPanel/state.ts @@ -11,10 +11,6 @@ export const enum Flag { ExpandNode, ConfirmDeletion, AbortDeletion, - ConfirmMerge, - AbortMerge, - ConfirmMove, - AbortMove, } export interface ActionData { @@ -25,11 +21,8 @@ export interface ActionData { export type Action = | IAction> | IAction> - | IAction> - | IAction< - Flag.DisableEditing | Flag.AbortDeletion | Flag.AbortMerge | Flag.AbortMove, - ActionData - > + | IAction> + | IAction> | IAction< Flag.Expansion, ActionData IExpansionState)> @@ -71,30 +64,12 @@ export const Factory = { flag: Flag.AbortDeletion, data: { source: undefined, data: undefined }, }), - confirmMerge: (data: ClientTag): Action => ({ - flag: Flag.ConfirmMerge, - data: { source: undefined, data }, - }), - abortMerge: (): Action => ({ - flag: Flag.AbortMerge, - data: { source: undefined, data: undefined }, - }), - confirmMove: (data: ClientTag): Action => ({ - flag: Flag.ConfirmMove, - data: { source: undefined, data }, - }), - abortMove: (): Action => ({ - flag: Flag.AbortMove, - data: { source: undefined, data: undefined }, - }), }; export type State = { expansion: IExpansionState; editableNode: ID | undefined; deletableNode: ClientTag | undefined; - mergableNode: ClientTag | undefined; - movableNode: ClientTag | undefined; }; export function reducer(state: State, action: Action): State { @@ -149,20 +124,6 @@ export function reducer(state: State, action: Action): State { deletableNode: action.data.data, }; - case Flag.ConfirmMerge: - case Flag.AbortMerge: - return { - ...state, - mergableNode: action.data.data, - }; - - case Flag.ConfirmMove: - case Flag.AbortMove: - return { - ...state, - movableNode: action.data.data, - }; - default: return state; } diff --git a/src/frontend/containers/Outliner/index.tsx b/src/frontend/containers/Outliner/index.tsx index 3294880a..3b7e775a 100644 --- a/src/frontend/containers/Outliner/index.tsx +++ b/src/frontend/containers/Outliner/index.tsx @@ -7,7 +7,6 @@ import LocationsPanel from './LocationsPanel'; import SavedSearchesPanel from './SavedSearchesPanel'; import TagsPanel, { OutlinerActionBar } from './TagsPanel'; import FileEditorsPanel from './FileEditorsPanel'; -import { TagPropertiesEditor } from './TagPropertiesEditor'; const Outliner = () => { const { uiStore } = useStore(); @@ -37,7 +36,6 @@ const Outliner = () => {
- ); }; diff --git a/src/frontend/stores/FileStore.ts b/src/frontend/stores/FileStore.ts index 5c7af178..fc2daa39 100644 --- a/src/frontend/stores/FileStore.ts +++ b/src/frontend/stores/FileStore.ts @@ -345,13 +345,55 @@ class FileStore { } } + @action.bound private async getAllowedFilesFromTaggingService( + files: string[], + ): Promise | undefined> { + const taggingServiceURL = this.rootStore.uiStore.taggingServiceURL + '/allowed-files/'; + const response = await fetch(taggingServiceURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ files: files }), + }).catch((error) => { + throw new Error(`Error while requesting allowed files: ${error}`); + }); + + if (response.ok) { + try { + const responseData = await response.json(); + if (responseData.allowed && Array.isArray(responseData.allowed)) { + const allowed = new Set(); + responseData.allowed.forEach((path: unknown) => { + if (typeof path === 'string') { + allowed.add(path); + } + }); + return allowed; + } else { + console.error('Allowed files response error: no "allowed" key found.'); + } + } catch (error) { + console.error('Allowed files error: response must be JSON.', error); + } + } else { + let errorBody; + try { + errorBody = await response.clone().json(); + } catch { + errorBody = await response.text(); + } + console.error('Allowed files request failed:', response.status, errorBody); + } + } + @action.bound async tagSelectedFilesUsingTaggingService(): Promise { if (this.isTaggingWithService) { return; } this.isTaggingWithService = true; const taggingServiceURL = this.rootStore.uiStore.taggingServiceURL; - const files = Array.from(this.rootStore.uiStore.fileSelection); + let files = Array.from(this.rootStore.uiStore.fileSelection); const numFiles = files.length; const isMulti = numFiles > 1; let successCount = 0; @@ -383,6 +425,13 @@ class FileStore { }; showProgressToaster(0); + + // Try to get the Allowed files, in case of the service API does some filtering to skip unnecessary requests + const allowedFiles = await this.getAllowedFilesFromTaggingService( + files.map((f) => f.absolutePath), + ).catch((e) => console.error(e)); + files = + allowedFiles === undefined ? files : files.filter((f) => allowedFiles.has(f.absolutePath)); // Process files with only N jobs in parallel and a progress + cancel callback const N = this.rootStore.uiStore.taggingServiceParallelRequests; await promiseAllLimit( diff --git a/src/frontend/stores/UiStore.ts b/src/frontend/stores/UiStore.ts index ccf619f5..cd70cdcd 100644 --- a/src/frontend/stores/UiStore.ts +++ b/src/frontend/stores/UiStore.ts @@ -228,8 +228,11 @@ class UiStore { @observable isMoveFilesToTrashOpen: boolean = false; /** Dialog to warn the user when he tries to open too many files externally */ @observable isManyExternalFilesOpen: boolean = false; - /** the tag selected to edit in a Dialog */ + + /* Tags selected to use in a tag operation dialog panel */ @observable tagToEdit: ClientTag | undefined = undefined; + @observable tagToMerge: ClientTag | undefined = undefined; + @observable tagToMove: ClientTag | undefined = undefined; // Usage preferences @observable isClearTagSelectorsOnSelectEnabled: boolean = false; @@ -660,6 +663,22 @@ class UiStore { } } + @action.bound openTagMergePanel(tag: ClientTag): void { + this.tagToMerge = tag; + } + + @action.bound closeTagMergePanel(): void { + this.tagToMerge = undefined; + } + + @action.bound openTagMovePanel(tag: ClientTag): void { + this.tagToMove = tag; + } + + @action.bound closeTagMovePanel(): void { + this.tagToMove = undefined; + } + @action.bound closeManyExternalFiles(): void { this.isManyExternalFilesOpen = false; }