diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx index 91c6c1dae38..fe4b889f540 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx @@ -1,5 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher'; +import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner'; import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,6 +9,7 @@ import { PiFloppyDiskBold } from 'react-icons/pi'; const SaveWorkflowButton = () => { const { t } = useTranslation(); const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges(); + const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner(); const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow(); return ( @@ -15,7 +17,7 @@ const SaveWorkflowButton = () => { tooltip={t('workflows.saveWorkflow')} aria-label={t('workflows.saveWorkflow')} icon={} - isDisabled={!doesWorkflowHaveUnsavedChanges} + isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner} onClick={saveOrSaveAsWorkflow} pointerEvents="auto" /> diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx index 39a93e4a382..779d6f018ee 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/SaveWorkflowButton.tsx @@ -1,4 +1,6 @@ import { IconButton } from '@invoke-ai/ui-library'; +import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher'; +import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner'; import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,12 +9,15 @@ import { PiFloppyDiskBold } from 'react-icons/pi'; const SaveWorkflowButton = () => { const { t } = useTranslation(); const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow(); + const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges(); + const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner(); return ( } + isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner} onClick={saveOrSaveAsWorkflow} pointerEvents="auto" variant="ghost" diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx index 6f5acc431ed..e683cfdbefd 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx @@ -1,5 +1,6 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useDoesWorkflowHaveUnsavedChanges } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher'; +import { useIsCurrentWorkflowOwner } from 'features/workflowLibrary/hooks/useIsCurrentWorkflowOwner'; import { useSaveOrSaveAsWorkflow } from 'features/workflowLibrary/hooks/useSaveOrSaveAsWorkflow'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,11 +10,12 @@ const SaveWorkflowMenuItem = () => { const { t } = useTranslation(); const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow(); const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges(); + const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner(); return ( } onClick={saveOrSaveAsWorkflow} > diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts new file mode 100644 index 00000000000..5183c9050b7 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useIsCurrentWorkflowOwner.ts @@ -0,0 +1,48 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectCurrentUser } from 'features/auth/store/authSlice'; +import { selectWorkflowId } from 'features/nodes/store/selectors'; +import { useMemo } from 'react'; +import { useGetSetupStatusQuery } from 'services/api/endpoints/auth'; +import { useGetWorkflowQuery } from 'services/api/endpoints/workflows'; + +/** + * Returns true if the current user can save the currently-loaded workflow directly (not as a copy). + * + * In single-user mode, this always returns true. + * In multiuser mode, returns true when: + * - The workflow has no ID (new, unsaved workflow — will open Save As) + * - The current user is the owner of the workflow + * - The current user is an admin + */ +export const useIsCurrentWorkflowOwner = (): boolean => { + const workflowId = useAppSelector(selectWorkflowId); + const currentUser = useAppSelector(selectCurrentUser); + const { data: setupStatus } = useGetSetupStatusQuery(); + const { data: workflowData } = useGetWorkflowQuery(workflowId ?? skipToken); + + return useMemo(() => { + // In single-user mode there is no concept of ownership, so saving is always allowed. + if (!setupStatus?.multiuser_enabled) { + return true; + } + + // No authenticated user — be permissive. + if (!currentUser) { + return true; + } + + // No workflow ID means this is a new/unsaved workflow. Clicking "Save" will open the + // Save As dialog, so we should not block it. + if (!workflowId) { + return true; + } + + // API data not yet available — be permissive to avoid incorrect disabling during loading. + if (!workflowData) { + return true; + } + + return workflowData.user_id === currentUser.user_id || currentUser.is_admin; + }, [setupStatus?.multiuser_enabled, workflowId, workflowData, currentUser]); +};