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",
|
||||
"prevPage": "Previous Page",
|
||||
"nextPage": "Next Page",
|
||||
"unknownError": "Unknown Error"
|
||||
"unknownError": "Unknown Error",
|
||||
"unsaved": "Unsaved"
|
||||
},
|
||||
"controlnet": {
|
||||
"controlAdapter_one": "Control Adapter",
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { Text } from '@chakra-ui/layout';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
const name = useAppSelector(
|
||||
(state) => state.workflow.name || 'Untitled Workflow'
|
||||
);
|
||||
const isTouched = useAppSelector((state) => state.workflow.isTouched);
|
||||
|
||||
return (
|
||||
<Text
|
||||
@ -18,6 +21,7 @@ const TopCenterPanel = () => {
|
||||
opacity={0.8}
|
||||
>
|
||||
{name}
|
||||
{isTouched ? ` (${t('common.unsaved')})` : ''}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { buildWorkflow } from 'features/nodes/util/workflow/buildWorkflow';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useMemo } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const nodes_ = useAppSelector((state: RootState) => state.nodes.nodes);
|
||||
const edges_ = useAppSelector((state: RootState) => state.nodes.edges);
|
||||
const workflow_ = useAppSelector((state: RootState) => state.workflow);
|
||||
const nodes_ = useAppSelector((state) => state.nodes.nodes);
|
||||
const edges_ = useAppSelector((state) => state.nodes.edges);
|
||||
const workflow_ = useAppSelector((state) => state.workflow);
|
||||
const [nodes] = useDebounce(nodes_, 300);
|
||||
const [edges] = useDebounce(edges_, 300);
|
||||
const [workflow] = useDebounce(workflow_, 300);
|
||||
const builtWorkflow = useMemo(
|
||||
() => buildWorkflow({ nodes, edges, workflow }),
|
||||
() =>
|
||||
buildWorkflow({ nodes, edges, workflow: omit(workflow, 'isTouched') }),
|
||||
[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 { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||
import {
|
||||
@ -936,4 +936,42 @@ export const {
|
||||
edgeAdded,
|
||||
} = 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;
|
||||
|
@ -41,4 +41,6 @@ export type NodesState = {
|
||||
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 { 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 { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import { cloneDeep, isEqual, uniqBy } from 'lodash-es';
|
||||
@ -15,6 +19,7 @@ export const initialWorkflowState: WorkflowState = {
|
||||
notes: '',
|
||||
exposedFields: [],
|
||||
meta: { version: '2.0.0', category: 'user' },
|
||||
isTouched: true,
|
||||
};
|
||||
|
||||
const workflowSlice = createSlice({
|
||||
@ -29,6 +34,7 @@ const workflowSlice = createSlice({
|
||||
state.exposedFields.concat(action.payload),
|
||||
(field) => `${field.nodeId}-${field.fieldName}`
|
||||
);
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowExposedFieldRemoved: (
|
||||
state,
|
||||
@ -37,37 +43,48 @@ const workflowSlice = createSlice({
|
||||
state.exposedFields = state.exposedFields.filter(
|
||||
(field) => !isEqual(field, action.payload)
|
||||
);
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowNameChanged: (state, action: PayloadAction<string>) => {
|
||||
state.name = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowDescriptionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.description = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowTagsChanged: (state, action: PayloadAction<string>) => {
|
||||
state.tags = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowAuthorChanged: (state, action: PayloadAction<string>) => {
|
||||
state.author = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowNotesChanged: (state, action: PayloadAction<string>) => {
|
||||
state.notes = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowVersionChanged: (state, action: PayloadAction<string>) => {
|
||||
state.version = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowContactChanged: (state, action: PayloadAction<string>) => {
|
||||
state.contact = action.payload;
|
||||
state.isTouched = true;
|
||||
},
|
||||
workflowIDChanged: (state, action: PayloadAction<string>) => {
|
||||
state.id = action.payload;
|
||||
},
|
||||
workflowReset: () => cloneDeep(initialWorkflowState),
|
||||
workflowSaved: (state) => {
|
||||
state.isTouched = false;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(workflowLoaded, (state, action) => {
|
||||
const { nodes: _nodes, edges: _edges, ...workflow } = action.payload;
|
||||
return cloneDeep(workflow);
|
||||
const { nodes: _nodes, edges: _edges, ...workflowExtra } = action.payload;
|
||||
return { ...cloneDeep(workflowExtra), isTouched: true };
|
||||
});
|
||||
|
||||
builder.addCase(nodesDeleted, (state, action) => {
|
||||
@ -79,6 +96,10 @@ const workflowSlice = createSlice({
|
||||
});
|
||||
|
||||
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
|
||||
|
||||
builder.addMatcher(isAnyNodeOrEdgeMutation, (state) => {
|
||||
state.isTouched = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -94,6 +115,7 @@ export const {
|
||||
workflowContactChanged,
|
||||
workflowIDChanged,
|
||||
workflowReset,
|
||||
workflowSaved,
|
||||
} = workflowSlice.actions;
|
||||
|
||||
export default workflowSlice.reducer;
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { ToastId, useToast } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import {
|
||||
workflowIDChanged,
|
||||
workflowSaved,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useCreateWorkflowMutation,
|
||||
useUpdateWorkflowMutation,
|
||||
} from 'services/api/endpoints/workflows';
|
||||
import { O } from 'ts-toolbelt';
|
||||
|
||||
type UseSaveLibraryWorkflowReturn = {
|
||||
saveWorkflow: () => Promise<void>;
|
||||
@ -18,6 +22,10 @@ type UseSaveLibraryWorkflowReturn = {
|
||||
|
||||
type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn;
|
||||
|
||||
const isWorkflowWithID = (
|
||||
workflow: WorkflowV2
|
||||
): workflow is O.Required<WorkflowV2, 'id'> => Boolean(workflow.id);
|
||||
|
||||
export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
@ -34,15 +42,13 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => {
|
||||
isClosable: false,
|
||||
});
|
||||
try {
|
||||
if (workflow.id) {
|
||||
const data = await updateWorkflow(workflow).unwrap();
|
||||
const updatedWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(updatedWorkflow));
|
||||
if (isWorkflowWithID(workflow)) {
|
||||
await updateWorkflow(workflow).unwrap();
|
||||
} else {
|
||||
const data = await createWorkflow(workflow).unwrap();
|
||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(createdWorkflow));
|
||||
dispatch(workflowIDChanged(data.workflow.id));
|
||||
}
|
||||
dispatch(workflowSaved());
|
||||
toast.update(toastRef.current, {
|
||||
title: t('workflows.workflowSaved'),
|
||||
status: 'success',
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { ToastId, useToast } from '@chakra-ui/react';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import {
|
||||
workflowIDChanged,
|
||||
workflowNameChanged,
|
||||
workflowSaved,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows';
|
||||
@ -40,8 +43,9 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => {
|
||||
workflow.id = undefined;
|
||||
workflow.name = newName;
|
||||
const data = await createWorkflow(workflow).unwrap();
|
||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(createdWorkflow));
|
||||
dispatch(workflowIDChanged(data.workflow.id));
|
||||
dispatch(workflowNameChanged(data.workflow.name));
|
||||
dispatch(workflowSaved());
|
||||
onSuccess && onSuccess();
|
||||
toast.update(toastRef.current, {
|
||||
title: t('workflows.workflowSaved'),
|
||||
|
Loading…
Reference in New Issue
Block a user