This commit is contained in:
psychedelicious 2023-11-30 19:19:58 +11:00
parent 11085783ef
commit 46905175a9
19 changed files with 191 additions and 53 deletions

View File

@ -34,7 +34,7 @@ async def get_workflow(
"/i/{workflow_id}",
operation_id="update_workflow",
responses={
200: {"model": Workflow},
200: {"model": WorkflowRecordDTO},
},
)
async def update_workflow(

View File

@ -48,7 +48,7 @@ WorkflowWithoutIDValidator = TypeAdapter(WorkflowWithoutID)
class Workflow(WorkflowWithoutID):
workflow_id: str = Field(default_factory=uuid_string, description="The id of the workflow.")
id: str = Field(default_factory=uuid_string, description="The id of the workflow.")
WorkflowValidator = TypeAdapter(Workflow)

View File

@ -58,7 +58,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
VALUES (?, ?);
""",
(
workflow_with_id.workflow_id,
workflow_with_id.id,
workflow_with_id.model_dump_json(),
),
)
@ -68,7 +68,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
raise
finally:
self._lock.release()
return self.get(workflow_with_id.workflow_id)
return self.get(workflow_with_id.id)
def update(self, workflow: Workflow) -> WorkflowRecordDTO:
try:
@ -79,7 +79,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
SET workflow = ?
WHERE workflow_id = ?;
""",
(workflow.model_dump_json(), workflow.workflow_id),
(workflow.model_dump_json(), workflow.id),
)
self._conn.commit()
except Exception:
@ -87,7 +87,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
raise
finally:
self._lock.release()
return self.get(workflow.workflow_id)
return self.get(workflow.id)
def delete(self, workflow_id: str) -> None:
try:

View File

@ -1613,6 +1613,8 @@
"systemCategory": "System",
"loadWorkflow": "$t(nodes.loadWorkflow)",
"deleteWorkflow": "Delete Workflow",
"unnamedWorkflow": "Unnamed Workflow"
"unnamedWorkflow": "Unnamed Workflow",
"downloadWorkflow": "Download Workflow",
"saveWorkflow": "Save Workflow"
}
}

View File

@ -14,7 +14,7 @@ import { useTranslation } from 'react-i18next';
import { FaDownload, FaPlus } from 'react-icons/fa';
import { useBoardName } from 'services/api/hooks/useBoardName';
import { BoardDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
import GalleryBoardContextMenuItems from './GalleryBoardContextMenuItems';
import NoBoardContextMenuItems from './NoBoardContextMenuItems';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
@ -94,7 +94,7 @@ const BoardContextMenu = ({
() => (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
motionProps={MENU_LIST_MOTION_PROPS}
onContextMenu={skipEvent}
>
<MenuGroup title={boardName}>

View File

@ -46,7 +46,7 @@ import {
useLazyGetImageWorkflowQuery,
} from 'services/api/endpoints/images';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import { menuListMotionProps } from 'theme/components/menu';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
const currentImageButtonsSelector = createSelector(
[stateSelector, activeTabNameSelector],
@ -248,7 +248,7 @@ const CurrentImageButtons = () => {
isDisabled={!imageDTO}
icon={<FaEllipsis />}
/>
<MenuList motionProps={menuListMotionProps}>
<MenuList motionProps={MENU_LIST_MOTION_PROPS}>
{imageDTO && <SingleSelectionMenuItems imageDTO={imageDTO} />}
</MenuList>
</Menu>

View File

@ -5,7 +5,7 @@ import {
} from 'common/components/IAIContextMenu';
import { MouseEvent, memo, useCallback } from 'react';
import { ImageDTO } from 'services/api/types';
import { menuListMotionProps } from 'theme/components/menu';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
import SingleSelectionMenuItems from './SingleSelectionMenuItems';
import { createSelector } from '@reduxjs/toolkit';
import { stateSelector } from 'app/store/store';
@ -44,7 +44,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
motionProps={MENU_LIST_MOTION_PROPS}
onContextMenu={skipEvent}
>
<MultipleSelectionMenuItems />
@ -55,7 +55,7 @@ const ImageContextMenu = ({ imageDTO, children }: Props) => {
return (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
motionProps={MENU_LIST_MOTION_PROPS}
onContextMenu={skipEvent}
>
<SingleSelectionMenuItems imageDTO={imageDTO} />

View File

@ -11,13 +11,10 @@ const PER_PAGE = 10;
const WorkflowLibraryContent = () => {
const [page, setPage] = useState(0);
const [category, setCategory] = useState<WorkflowCategory>('user');
const { data } = useListWorkflowsQuery(
{
page,
per_page: PER_PAGE,
},
{ refetchOnMountOrArgChange: true }
);
const { data } = useListWorkflowsQuery({
page,
per_page: PER_PAGE,
});
if (!data) {
return null;

View File

@ -16,7 +16,7 @@ import {
} from 'features/nodes/store/nodesSlice';
import { MouseEvent, ReactNode, memo, useCallback, useMemo } from 'react';
import { FaMinus, FaPlus } from 'react-icons/fa';
import { menuListMotionProps } from 'theme/components/menu';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
import { useTranslation } from 'react-i18next';
type Props = {
@ -110,7 +110,7 @@ const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => {
!menuItems.length ? null : (
<MenuList
sx={{ visibility: 'visible !important' }}
motionProps={menuListMotionProps}
motionProps={MENU_LIST_MOTION_PROPS}
onContextMenu={skipEvent}
>
<MenuGroup

View File

@ -0,0 +1,30 @@
import { FileButton } from '@mantine/core';
import IAIIconButton from 'common/components/IAIIconButton';
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { FaSave, FaUpload } from 'react-icons/fa';
const LoadWorkflowButton = () => {
const { t } = useTranslation();
const resetRef = useRef<() => void>(null);
const loadWorkflowFromFile = useLoadWorkflowFromFile(resetRef);
return (
<FileButton
resetRef={resetRef}
accept="application/json"
onChange={loadWorkflowFromFile}
>
{(props) => (
<IAIIconButton
icon={<FaSave />}
tooltip={t('nodes.loadWorkflow')}
aria-label={t('nodes.loadWorkflow')}
{...props}
/>
)}
</FileButton>
);
};
export default memo(LoadWorkflowButton);

View File

@ -3,6 +3,7 @@ import { memo } from 'react';
import DownloadWorkflowButton from './DownloadWorkflowButton';
import LoadWorkflowButton from './LoadWorkflowButton';
import ResetWorkflowButton from './ResetWorkflowButton';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopCenterPanel/SaveWorkflowButton';
const TopCenterPanel = () => {
return (
@ -17,6 +18,7 @@ const TopCenterPanel = () => {
>
<DownloadWorkflowButton />
<LoadWorkflowButton />
<SaveWorkflowButton />
<ResetWorkflowButton />
</Flex>
);

View File

@ -1,13 +1,53 @@
import { Flex } from '@chakra-ui/layout';
import { memo } from 'react';
import WorkflowEditorSettings from './WorkflowEditorSettings';
import {
Flex,
Menu,
MenuButton,
MenuGroup,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import IAIIconButton from 'common/components/IAIIconButton';
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
import { memo } from 'react';
import { FaEllipsis } from 'react-icons/fa6';
import WorkflowEditorSettings from './WorkflowEditorSettings';
import { MENU_LIST_MOTION_PROPS as MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
import { useTranslation } from 'react-i18next';
import { useDownloadWorkflow } from 'features/nodes/hooks/useDownloadWorkflow';
import { FaDownload, FaSave } from 'react-icons/fa';
import { useSaveWorkflow } from 'features/nodes/hooks/useSaveWorkflow';
const TopRightPanel = () => {
const { t } = useTranslation();
const downloadWorkflow = useDownloadWorkflow();
const saveWorkflow = useSaveWorkflow();
return (
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
<WorkflowEditorSettings />
<WorkflowLibraryButton />
<Menu>
<MenuButton as={IAIIconButton} icon={<FaEllipsis />} />
<MenuList motionProps={MENU_LIST_MOTION_PROPS}>
<MenuItem onClick={saveWorkflow} icon={<FaSave />}>
{t('workflows.saveWorkflow')}
</MenuItem>
<MenuItem onClick={downloadWorkflow} icon={<FaDownload />}>
{t('workflows.downloadWorkflow')}
</MenuItem>
{/* <MenuGroup title={t('common.settingsLabel')}>
<HotkeysModal>
<MenuItem as="button" icon={<FaKeyboard />}>
{t('common.hotkeysLabel')}
</MenuItem>
</HotkeysModal>
<SettingsModal>
<MenuItem as="button" icon={<FaCog />}>
{t('common.settingsLabel')}
</MenuItem>
</SettingsModal>
</MenuGroup> */}
</MenuList>
</Menu>
</Flex>
);
};

View File

@ -0,0 +1,17 @@
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { useCallback } from 'react';
export const useDownloadWorkflow = () => {
const workflow = useWorkflow();
const downloadWorkflow = useCallback(() => {
const blob = new Blob([JSON.stringify(workflow, null, 2)]);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${workflow.name || 'My Workflow'}.json`;
document.body.appendChild(a);
a.click();
a.remove();
}, [workflow]);
return downloadWorkflow;
};

View File

@ -0,0 +1,46 @@
import { useAppToaster } from 'app/components/Toaster';
import { useAppDispatch } from 'app/store/storeHooks';
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
import { zWorkflowV2 } from 'features/nodes/types/workflow';
import { useCallback } from 'react';
import {
useCreateWorkflowMutation,
useUpdateWorkflowMutation,
} from 'services/api/endpoints/workflows';
export const useSaveWorkflow = () => {
const dispatch = useAppDispatch();
const workflow = useWorkflow();
const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation();
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
const toaster = useAppToaster();
const saveWorkflow = useCallback(async () => {
try {
if (workflow.id) {
console.log('update workflow');
const data = await updateWorkflow(workflow).unwrap();
const updatedWorkflow = zWorkflowV2.parse(data.workflow);
dispatch(workflowLoaded(updatedWorkflow));
} else {
console.log('create workflow');
const data = await createWorkflow(workflow).unwrap();
const createdWorkflow = zWorkflowV2.parse(data.workflow);
dispatch(workflowLoaded(createdWorkflow));
}
toaster({
title: 'Workflow saved',
status: 'success',
duration: 3000,
});
} catch (e) {
toaster({
title: 'Failed to save workflow',
// description: e.message,
status: 'error',
duration: 3000,
});
}
}, [workflow, toaster, updateWorkflow, dispatch, createWorkflow]);
return saveWorkflow;
};

View File

@ -711,6 +711,9 @@ const nodesSlice = createSlice({
workflowContactChanged: (state, action: PayloadAction<string>) => {
state.workflow.contact = action.payload;
},
workflowIDChanged: (state, action: PayloadAction<string>) => {
state.workflow.id = action.payload;
},
workflowLoaded: (state, action: PayloadAction<WorkflowV2>) => {
const { nodes, edges, ...workflow } = action.payload;
state.workflow = workflow;
@ -1003,6 +1006,7 @@ export const {
workflowNotesChanged,
workflowTagsChanged,
workflowVersionChanged,
workflowIDChanged,
edgeAdded,
} = nodesSlice.actions;

View File

@ -18,7 +18,7 @@ import {
FaGithub,
FaKeyboard,
} from 'react-icons/fa';
import { menuListMotionProps } from 'theme/components/menu';
import { MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import HotkeysModal from './HotkeysModal/HotkeysModal';
import InvokeAILogoComponent from './InvokeAILogoComponent';
@ -54,7 +54,7 @@ const SiteHeader = () => {
icon={<FaBars />}
sx={{ boxSize: 8 }}
/>
<MenuList motionProps={menuListMotionProps}>
<MenuList motionProps={MENU_LIST_MOTION_PROPS}>
<MenuGroup title={t('common.communityLabel')}>
{isGithubLinkEnabled && (
<MenuItem

View File

@ -18,11 +18,14 @@ export const workflowsApi = api.injectEndpoints({
url: `workflows/i/${workflow_id}`,
method: 'DELETE',
}),
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
invalidatesTags: (result, error, workflow_id) => [
{ type: 'Workflow', id: LIST_TAG },
{ type: 'Workflow', id: workflow_id },
],
}),
createWorkflow: build.mutation<
paths['/api/v1/workflows/']['post']['responses']['200']['content']['application/json'],
WorkflowV2
paths['/api/v1/workflows/']['post']['requestBody']['content']['application/json']['workflow']
>({
query: (workflow) => ({
url: 'workflows',
@ -33,16 +36,16 @@ export const workflowsApi = api.injectEndpoints({
}),
updateWorkflow: build.mutation<
paths['/api/v1/workflows/i/{workflow_id}']['patch']['responses']['200']['content']['application/json'],
WorkflowV2
paths['/api/v1/workflows/i/{workflow_id}']['patch']['requestBody']['content']['application/json']['workflow']
>({
query: (workflow) => ({
url: `workflows/i/${workflow.id}`,
method: 'PATCH',
body: workflow,
}),
invalidatesTags: (response, error, arg) => [
invalidatesTags: (response, error, workflow) => [
{ type: 'Workflow', id: LIST_TAG },
{ type: 'Workflow', id: arg.id },
{ type: 'Workflow', id: workflow.id },
],
}),
listWorkflows: build.query<
@ -53,10 +56,7 @@ export const workflowsApi = api.injectEndpoints({
url: 'workflows/',
params,
}),
providesTags: (result, error, params) => [
{ type: 'Workflow', id: LIST_TAG },
{ type: 'Workflow', id: params?.page },
],
providesTags: [{ type: 'Workflow', id: LIST_TAG }],
}),
}),
});

File diff suppressed because one or more lines are too long

View File

@ -56,7 +56,7 @@ export const menuTheme = defineMultiStyleConfig({
},
});
export const menuListMotionProps: MotionProps = {
export const MENU_LIST_MOTION_PROPS: MotionProps = {
variants: {
enter: {
visibility: 'visible',