mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): track & indicate workflow saved status
This commit is contained in:
parent
4627a7c75f
commit
6d176601cc
@ -171,7 +171,8 @@
|
|||||||
"created": "Created",
|
"created": "Created",
|
||||||
"prevPage": "Previous Page",
|
"prevPage": "Previous Page",
|
||||||
"nextPage": "Next Page",
|
"nextPage": "Next Page",
|
||||||
"unknownError": "Unknown Error"
|
"unknownError": "Unknown Error",
|
||||||
|
"unsaved": "Unsaved"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { Text } from '@chakra-ui/layout';
|
import { Text } from '@chakra-ui/layout';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const TopCenterPanel = () => {
|
const TopCenterPanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const name = useAppSelector(
|
const name = useAppSelector(
|
||||||
(state) => state.workflow.name || 'Untitled Workflow'
|
(state) => state.workflow.name || 'Untitled Workflow'
|
||||||
);
|
);
|
||||||
|
const isTouched = useAppSelector((state) => state.workflow.isTouched);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
@ -18,6 +21,7 @@ const TopCenterPanel = () => {
|
|||||||
opacity={0.8}
|
opacity={0.8}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
{isTouched ? ` (${t('common.unsaved')})` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { RootState } from 'app/store/store';
|
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
||||||
|
import { omit } from 'lodash-es';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
export const useWorkflow = () => {
|
export const useWorkflow = () => {
|
||||||
const nodes_ = useAppSelector((state: RootState) => state.nodes.nodes);
|
const nodes_ = useAppSelector((state) => state.nodes.nodes);
|
||||||
const edges_ = useAppSelector((state: RootState) => state.nodes.edges);
|
const edges_ = useAppSelector((state) => state.nodes.edges);
|
||||||
const workflow_ = useAppSelector((state: RootState) => state.workflow);
|
const workflow_ = useAppSelector((state) => state.workflow);
|
||||||
const [nodes] = useDebounce(nodes_, 300);
|
const [nodes] = useDebounce(nodes_, 300);
|
||||||
const [edges] = useDebounce(edges_, 300);
|
const [edges] = useDebounce(edges_, 300);
|
||||||
const [workflow] = useDebounce(workflow_, 300);
|
const [workflow] = useDebounce(workflow_, 300);
|
||||||
const builtWorkflow = useMemo(
|
const builtWorkflow = useMemo(
|
||||||
() => buildWorkflow({ nodes, edges, workflow }),
|
() =>
|
||||||
|
buildWorkflow({ nodes, edges, workflow: omit(workflow, 'isTouched') }),
|
||||||
[nodes, edges, workflow]
|
[nodes, edges, workflow]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||||
import {
|
import {
|
||||||
@ -936,4 +936,42 @@ export const {
|
|||||||
edgeAdded,
|
edgeAdded,
|
||||||
} = nodesSlice.actions;
|
} = nodesSlice.actions;
|
||||||
|
|
||||||
|
// This is used for tracking `state.workflow.isTouched`
|
||||||
|
export const isAnyNodeOrEdgeMutation = isAnyOf(
|
||||||
|
connectionEnded,
|
||||||
|
connectionMade,
|
||||||
|
edgeDeleted,
|
||||||
|
edgesChanged,
|
||||||
|
edgesDeleted,
|
||||||
|
edgeUpdated,
|
||||||
|
fieldBoardValueChanged,
|
||||||
|
fieldBooleanValueChanged,
|
||||||
|
fieldColorValueChanged,
|
||||||
|
fieldControlNetModelValueChanged,
|
||||||
|
fieldEnumModelValueChanged,
|
||||||
|
fieldImageValueChanged,
|
||||||
|
fieldIPAdapterModelValueChanged,
|
||||||
|
fieldT2IAdapterModelValueChanged,
|
||||||
|
fieldLabelChanged,
|
||||||
|
fieldLoRAModelValueChanged,
|
||||||
|
fieldMainModelValueChanged,
|
||||||
|
fieldNumberValueChanged,
|
||||||
|
fieldRefinerModelValueChanged,
|
||||||
|
fieldSchedulerValueChanged,
|
||||||
|
fieldStringValueChanged,
|
||||||
|
fieldVaeModelValueChanged,
|
||||||
|
nodeAdded,
|
||||||
|
nodeReplaced,
|
||||||
|
nodeIsIntermediateChanged,
|
||||||
|
nodeIsOpenChanged,
|
||||||
|
nodeLabelChanged,
|
||||||
|
nodeNotesChanged,
|
||||||
|
nodesChanged,
|
||||||
|
nodesDeleted,
|
||||||
|
nodeUseCacheChanged,
|
||||||
|
notesNodeValueChanged,
|
||||||
|
selectionPasted,
|
||||||
|
edgeAdded
|
||||||
|
);
|
||||||
|
|
||||||
export default nodesSlice.reducer;
|
export default nodesSlice.reducer;
|
||||||
|
@ -41,4 +41,6 @@ export type NodesState = {
|
|||||||
selectionMode: SelectionMode;
|
selectionMode: SelectionMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'>;
|
export type WorkflowsState = Omit<WorkflowV2, 'nodes' | 'edges'> & {
|
||||||
|
isTouched: boolean;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||||
import { nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
import {
|
||||||
|
nodeEditorReset,
|
||||||
|
nodesDeleted,
|
||||||
|
isAnyNodeOrEdgeMutation,
|
||||||
|
} from 'features/nodes/store/nodesSlice';
|
||||||
import { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
import { WorkflowsState as WorkflowState } from 'features/nodes/store/types';
|
||||||
import { FieldIdentifier } from 'features/nodes/types/field';
|
import { FieldIdentifier } from 'features/nodes/types/field';
|
||||||
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
||||||
@ -15,6 +19,7 @@ export const initialWorkflowState: WorkflowState = {
|
|||||||
notes: '',
|
notes: '',
|
||||||
exposedFields: [],
|
exposedFields: [],
|
||||||
meta: { version: '2.0.0', category: 'user' },
|
meta: { version: '2.0.0', category: 'user' },
|
||||||
|
isTouched: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowSlice = createSlice({
|
const workflowSlice = createSlice({
|
||||||
@ -29,6 +34,7 @@ const workflowSlice = createSlice({
|
|||||||
state.exposedFields.concat(action.payload),
|
state.exposedFields.concat(action.payload),
|
||||||
(field) => `${field.nodeId}-${field.fieldName}`
|
(field) => `${field.nodeId}-${field.fieldName}`
|
||||||
);
|
);
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowExposedFieldRemoved: (
|
workflowExposedFieldRemoved: (
|
||||||
state,
|
state,
|
||||||
@ -37,37 +43,48 @@ const workflowSlice = createSlice({
|
|||||||
state.exposedFields = state.exposedFields.filter(
|
state.exposedFields = state.exposedFields.filter(
|
||||||
(field) => !isEqual(field, action.payload)
|
(field) => !isEqual(field, action.payload)
|
||||||
);
|
);
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.name = action.payload;
|
state.name = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.description = action.payload;
|
state.description = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowTagsChanged: (state, action: PayloadAction<string>) => {
|
workflowTagsChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.tags = action.payload;
|
state.tags = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
|
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.author = action.payload;
|
state.author = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowNotesChanged: (state, action: PayloadAction<string>) => {
|
workflowNotesChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.notes = action.payload;
|
state.notes = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowVersionChanged: (state, action: PayloadAction<string>) => {
|
workflowVersionChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.version = action.payload;
|
state.version = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowContactChanged: (state, action: PayloadAction<string>) => {
|
workflowContactChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.contact = action.payload;
|
state.contact = action.payload;
|
||||||
|
state.isTouched = true;
|
||||||
},
|
},
|
||||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||||
state.id = action.payload;
|
state.id = action.payload;
|
||||||
},
|
},
|
||||||
workflowReset: () => cloneDeep(initialWorkflowState),
|
workflowReset: () => cloneDeep(initialWorkflowState),
|
||||||
|
workflowSaved: (state) => {
|
||||||
|
state.isTouched = false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(workflowLoaded, (state, action) => {
|
builder.addCase(workflowLoaded, (state, action) => {
|
||||||
const { nodes: _nodes, edges: _edges, ...workflow } = action.payload;
|
const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||||
return cloneDeep(workflow);
|
return { ...cloneDeep(workflowExtra), isTouched: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(nodesDeleted, (state, action) => {
|
builder.addCase(nodesDeleted, (state, action) => {
|
||||||
@ -79,6 +96,10 @@ const workflowSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
|
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
|
||||||
|
|
||||||
|
builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
|
||||||
|
state.isTouched = true;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,6 +115,7 @@ export const {
|
|||||||
workflowContactChanged,
|
workflowContactChanged,
|
||||||
workflowIDChanged,
|
workflowIDChanged,
|
||||||
workflowReset,
|
workflowReset,
|
||||||
|
workflowSaved,
|
||||||
} = workflowSlice.actions;
|
} = workflowSlice.actions;
|
||||||
|
|
||||||
export default workflowSlice.reducer;
|
export default workflowSlice.reducer;
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { ToastId, useToast } from '@chakra-ui/react';
|
import { ToastId, useToast } from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import {
|
||||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
workflowIDChanged,
|
||||||
|
workflowSaved,
|
||||||
|
} from 'features/nodes/store/workflowSlice';
|
||||||
|
import { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
useCreateWorkflowMutation,
|
useCreateWorkflowMutation,
|
||||||
useUpdateWorkflowMutation,
|
useUpdateWorkflowMutation,
|
||||||
} from 'services/api/endpoints/workflows';
|
} from 'services/api/endpoints/workflows';
|
||||||
|
import { O } from 'ts-toolbelt';
|
||||||
|
|
||||||
type UseSaveLibraryWorkflowReturn = {
|
type UseSaveLibraryWorkflowReturn = {
|
||||||
saveWorkflow: () => Promise<void>;
|
saveWorkflow: () => Promise<void>;
|
||||||
@ -18,6 +22,10 @@ type UseSaveLibraryWorkflowReturn = {
|
|||||||
|
|
||||||
type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn;
|
type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn;
|
||||||
|
|
||||||
|
const isWorkflowWithID = (
|
||||||
|
workflow: WorkflowV2
|
||||||
|
): workflow is O.Required<WorkflowV2, 'id'> => Boolean(workflow.id);
|
||||||
|
|
||||||
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -34,15 +42,13 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
|||||||
isClosable: false,
|
isClosable: false,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (workflow.id) {
|
if (isWorkflowWithID(workflow)) {
|
||||||
const data = await updateWorkflow(workflow).unwrap();
|
await updateWorkflow(workflow).unwrap();
|
||||||
const updatedWorkflow = zWorkflowV2.parse(data.workflow);
|
|
||||||
dispatch(workflowLoaded(updatedWorkflow));
|
|
||||||
} else {
|
} else {
|
||||||
const data = await createWorkflow(workflow).unwrap();
|
const data = await createWorkflow(workflow).unwrap();
|
||||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
dispatch(workflowIDChanged(data.workflow.id));
|
||||||
dispatch(workflowLoaded(createdWorkflow));
|
|
||||||
}
|
}
|
||||||
|
dispatch(workflowSaved());
|
||||||
toast.update(toastRef.current, {
|
toast.update(toastRef.current, {
|
||||||
title: t('workflows.workflowSaved'),
|
title: t('workflows.workflowSaved'),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { ToastId, useToast } from '@chakra-ui/react';
|
import { ToastId, useToast } from '@chakra-ui/react';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import {
|
||||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
workflowIDChanged,
|
||||||
|
workflowNameChanged,
|
||||||
|
workflowSaved,
|
||||||
|
} from 'features/nodes/store/workflowSlice';
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows';
|
import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows';
|
||||||
@ -40,8 +43,9 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
|||||||
workflow.id = undefined;
|
workflow.id = undefined;
|
||||||
workflow.name = newName;
|
workflow.name = newName;
|
||||||
const data = await createWorkflow(workflow).unwrap();
|
const data = await createWorkflow(workflow).unwrap();
|
||||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
dispatch(workflowIDChanged(data.workflow.id));
|
||||||
dispatch(workflowLoaded(createdWorkflow));
|
dispatch(workflowNameChanged(data.workflow.name));
|
||||||
|
dispatch(workflowSaved());
|
||||||
onSuccess && onSuccess();
|
onSuccess && onSuccess();
|
||||||
toast.update(toastRef.current, {
|
toast.update(toastRef.current, {
|
||||||
title: t('workflows.workflowSaved'),
|
title: t('workflows.workflowSaved'),
|
||||||
|
Loading…
Reference in New Issue
Block a user