Skip to content

Commit b37bc8d

Browse files
Copilotlstein
andauthored
Disable Save when editing another user's shared workflow in multiuser mode (#120)
* Disable Save when editing another user's shared workflow in multiuser mode Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
1 parent d965d60 commit b37bc8d

4 files changed

Lines changed: 59 additions & 2 deletions

File tree

invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IconButton } from '@invoke-ai/ui-library';
22
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
3+
import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
34
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
45
import { memo } from 'react';
56
import { useTranslation } from 'react-i18next';
@@ -8,14 +9,15 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
89
const SaveWorkflowButton = () => {
910
const { t } = useTranslation();
1011
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
12+
const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
1113
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
1214

1315
return (
1416
<IconButton
1517
tooltip={t('workflows.saveWorkflow')}
1618
aria-label={t('workflows.saveWorkflow')}
1719
icon={<PiFloppyDiskBold />}
18-
isDisabled={!doesWorkflowHaveUnsavedChanges}
20+
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
1921
onClick={saveOrSaveAsWorkflow}
2022
pointerEvents="auto"
2123
/>

invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { IconButton } from '@invoke-ai/ui-library';
2+
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
3+
import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
24
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
35
import { memo } from 'react';
46
import { useTranslation } from 'react-i18next';
@@ -7,12 +9,15 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
79
const SaveWorkflowButton = () => {
810
const { t } = useTranslation();
911
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
12+
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
13+
const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
1014

1115
return (
1216
<IconButton
1317
tooltip={t('workflows.saveWorkflow')}
1418
aria-label={t('workflows.saveWorkflow')}
1519
icon={<PiFloppyDiskBold />}
20+
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
1621
onClick={saveOrSaveAsWorkflow}
1722
pointerEvents="auto"
1823
variant="ghost"

invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MenuItem } from '@invoke-ai/ui-library';
22
import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
3+
import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner';
34
import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow';
45
import { memo } from 'react';
56
import { useTranslation } from 'react-i18next';
@@ -9,11 +10,12 @@ const SaveWorkflowMenuItem = () => {
910
const { t } = useTranslation();
1011
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
1112
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
13+
const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
1214

1315
return (
1416
<MenuItem
1517
as="button"
16-
isDisabled={!doesWorkflowHaveUnsavedChanges}
18+
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
1719
icon={<PiFloppyDiskBold />}
1820
onClick={saveOrSaveAsWorkflow}
1921
>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { skipToken } from '@reduxjs/toolkit/query';
2+
import { useAppSelector } from 'app/store/storeHooks';
3+
import { selectCurrentUser } from 'features/auth/store/authSlice';
4+
import { selectWorkflowId } from 'features/nodes/store/selectors';
5+
import { useMemo } from 'react';
6+
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
7+
import { useGetWorkflowQuery } from 'services/api/endpoints/workflows';
8+
9+
/**
10+
* Returns true if the current user can save the currently-loaded workflow directly (not as a copy).
11+
*
12+
* In single-user mode, this always returns true.
13+
* In multiuser mode, returns true when:
14+
* - The workflow has no ID (new, unsaved workflow — will open Save As)
15+
* - The current user is the owner of the workflow
16+
* - The current user is an admin
17+
*/
18+
export const useIsCurrentWorkflowOwner = (): boolean => {
19+
const workflowId = useAppSelector(selectWorkflowId);
20+
const currentUser = useAppSelector(selectCurrentUser);
21+
const { data: setupStatus } = useGetSetupStatusQuery();
22+
const { data: workflowData } = useGetWorkflowQuery(workflowId ?? skipToken);
23+
24+
return useMemo(() => {
25+
// In single-user mode there is no concept of ownership, so saving is always allowed.
26+
if (!setupStatus?.multiuser_enabled) {
27+
return true;
28+
}
29+
30+
// No authenticated user — be permissive.
31+
if (!currentUser) {
32+
return true;
33+
}
34+
35+
// No workflow ID means this is a new/unsaved workflow. Clicking "Save" will open the
36+
// Save As dialog, so we should not block it.
37+
if (!workflowId) {
38+
return true;
39+
}
40+
41+
// API data not yet available — be permissive to avoid incorrect disabling during loading.
42+
if (!workflowData) {
43+
return true;
44+
}
45+
46+
return workflowData.user_id === currentUser.user_id || currentUser.is_admin;
47+
}, [setupStatus?.multiuser_enabled, workflowId, workflowData, currentUser]);
48+
};

0 commit comments

Comments
 (0)