Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -8,14 +9,15 @@ import { PiFloppyDiskBold } from 'react-icons/pi';
const SaveWorkflowButton = () => {
const { t } = useTranslation();
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();

return (
<IconButton
tooltip={t('workflows.saveWorkflow')}
aria-label={t('workflows.saveWorkflow')}
icon={<PiFloppyDiskBold />}
isDisabled={!doesWorkflowHaveUnsavedChanges}
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
onClick={saveOrSaveAsWorkflow}
pointerEvents="auto"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<IconButton
tooltip={t('workflows.saveWorkflow')}
aria-label={t('workflows.saveWorkflow')}
icon={<PiFloppyDiskBold />}
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
onClick={saveOrSaveAsWorkflow}
pointerEvents="auto"
variant="ghost"
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,11 +10,12 @@ const SaveWorkflowMenuItem = () => {
const { t } = useTranslation();
const saveOrSaveAsWorkflow = useSaveOrSaveAsWorkflow();
const doesWorkflowHaveUnsavedChanges = useDoesWorkflowHaveUnsavedChanges();
const isCurrentWorkflowOwner = useIsCurrentWorkflowOwner();

return (
<MenuItem
as="button"
isDisabled={!doesWorkflowHaveUnsavedChanges}
isDisabled={!doesWorkflowHaveUnsavedChanges || !isCurrentWorkflowOwner}
icon={<PiFloppyDiskBold />}
onClick={saveOrSaveAsWorkflow}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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]);
};