mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-15 15:10:31 -08:00
feat: save graph to wiki
This commit is contained in:
parent
e265bb4fcf
commit
4dfbc57966
8 changed files with 123 additions and 98 deletions
|
|
@ -1,13 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
||||
/* eslint-disable @typescript-eslint/promise-function-async */
|
||||
import { sidebarWidth } from '@/constants/style';
|
||||
import { useThemeObservable } from '@services/theme/hooks';
|
||||
import { type Graph, loadJSON } from 'fbp-graph/lib/Graph';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import type { IFBPLibrary, ITheGraphEditor } from 'the-graph';
|
||||
import type { ITheGraphEditor } from 'the-graph';
|
||||
import TheGraph from 'the-graph';
|
||||
import { Component as ThumbnailNav } from 'the-graph/the-graph-nav/the-graph-nav';
|
||||
import 'the-graph/themes/the-graph-dark.css';
|
||||
|
|
@ -16,11 +13,11 @@ import '@fortawesome/fontawesome-free/js/all.js';
|
|||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
import '@fortawesome/fontawesome-free/css/v4-font-face.css';
|
||||
|
||||
import { photoboothJSON } from '../photobooth.json';
|
||||
import { SearchComponents } from './components/SearchComponents';
|
||||
import { getBrowserComponentLibrary } from './library';
|
||||
import { useLibrary } from './library';
|
||||
import { useMenu } from './menu';
|
||||
import { useMouseEvents } from './mouseEvents';
|
||||
import { useSaveLoadGraph } from './useSaveLoadGraph';
|
||||
|
||||
const TheGraphContainer = styled.main`
|
||||
/**
|
||||
|
|
@ -50,75 +47,12 @@ const ThumbnailContainer = styled.div`
|
|||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export interface IGraphEditorProps {
|
||||
animatedEdges?: any[];
|
||||
appView?: any;
|
||||
autolayout?: boolean;
|
||||
autolayouter?: any;
|
||||
copyNodes?: any[];
|
||||
debounceLibraryRefeshTimer?: any;
|
||||
displaySelectionGroup?: boolean;
|
||||
editable?: boolean;
|
||||
errorNodes?: any;
|
||||
forceSelection?: boolean;
|
||||
graph: Graph;
|
||||
graphChanges?: any[];
|
||||
graphView?: any;
|
||||
grid?: number;
|
||||
height?: number;
|
||||
icons?: any;
|
||||
library?: IFBPLibrary;
|
||||
maxZoom?: number;
|
||||
menus?: any;
|
||||
minZoom?: number;
|
||||
notifyView?: () => void;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
onContextMenu?: () => void;
|
||||
pan?: any;
|
||||
plugins?: object;
|
||||
readonly?: boolean;
|
||||
scale?: number;
|
||||
selectedEdges?: any[];
|
||||
selectedNodes?: any[];
|
||||
|
||||
selectedNodesHash?: any;
|
||||
setGraph: (graph: Graph) => void;
|
||||
snap?: number;
|
||||
theme: 'light' | 'dark';
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function GraphEditor() {
|
||||
// const initializeAutolayouter = () => {
|
||||
// // Logic for initializing autolayouter
|
||||
// const auto = klayNoflo.init({
|
||||
// onSuccess: applyAutolayout,
|
||||
// workerScript: 'vendor/klayjs/klay.js',
|
||||
// });
|
||||
// setAutolayouter(auto);
|
||||
// };
|
||||
// const applyAutolayout = (keilerGraph: any) => {
|
||||
// // Logic for applying autolayout
|
||||
// };
|
||||
|
||||
const { t } = useTranslation();
|
||||
const theme = useThemeObservable();
|
||||
|
||||
const [graph, setGraph] = useState<Graph | undefined>();
|
||||
useEffect(() => {
|
||||
void loadJSON(photoboothJSON).then(graph => {
|
||||
setGraph(graph);
|
||||
});
|
||||
}, []);
|
||||
// load library bundled by webpack noflo-component-loader from installed noflo related npm packages
|
||||
const [library, setLibrary] = useState<IFBPLibrary | undefined>();
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const libraryToLoad = await getBrowserComponentLibrary();
|
||||
setLibrary(libraryToLoad);
|
||||
})();
|
||||
}, []);
|
||||
const [graph, setGraph] = useSaveLoadGraph();
|
||||
const library = useLibrary();
|
||||
|
||||
// methods
|
||||
// const { subscribeGraph, unsubscribeGraph } = useSubscribeGraph({ readonly });
|
||||
|
|
@ -136,12 +70,12 @@ export function GraphEditor() {
|
|||
library,
|
||||
setGraph,
|
||||
});
|
||||
const { addMenu, addMenuCallback, addMenuAction, getMenuDef } = useMenu();
|
||||
const { getMenuDef } = useMenu();
|
||||
const editorReference = useRef<ITheGraphEditor>();
|
||||
if (!graph || !library) return <div>{t('Loading')}</div>;
|
||||
if ((graph === undefined) || (library === undefined)) return <div>{t('Loading')}</div>;
|
||||
return (
|
||||
<ErrorBoundary fallback={<div>Something went wrong</div>}>
|
||||
<TheGraphContainer className={`the-graph-${theme?.shouldUseDarkColors ? 'dark' : 'light'}`}>
|
||||
<TheGraphContainer className={`the-graph-${theme?.shouldUseDarkColors === true ? 'dark' : 'light'}`}>
|
||||
<TheGraph.App
|
||||
graph={graph}
|
||||
library={library}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Component, ComponentLoader, InPort } from 'noflo';
|
||||
import type BasePort from 'noflo/lib/BasePort';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { IFBPLibrary, INoFloProtocolComponent, INoFloProtocolComponentPort, INoFloUIComponent, INoFloUIComponentPort } from 'the-graph';
|
||||
|
||||
/**
|
||||
|
|
@ -109,4 +110,16 @@ export async function getBrowserComponentLibrary() {
|
|||
// unnamespaced: true,
|
||||
// }, false);
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
|
||||
export function useLibrary() {
|
||||
// load library bundled by webpack noflo-component-loader from installed noflo related npm packages
|
||||
const [library, setLibrary] = useState<IFBPLibrary | undefined>();
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
const libraryToLoad = await getBrowserComponentLibrary();
|
||||
setLibrary(libraryToLoad);
|
||||
})();
|
||||
}, []);
|
||||
return library;
|
||||
}
|
||||
|
|
|
|||
43
src/pages/Workflow/GraphEditor/useSaveLoadGraph.ts
Normal file
43
src/pages/Workflow/GraphEditor/useSaveLoadGraph.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import useDebouncedCallback from 'beautiful-react-hooks/useDebouncedCallback';
|
||||
import { Graph } from 'fbp-graph';
|
||||
import { loadJSON } from 'fbp-graph/lib/Graph';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { IWorkflowContext, WorkflowContext } from '../useContext';
|
||||
import { addWorkflowToWiki } from '../WorkflowManage/useWorkflowDataSource';
|
||||
|
||||
export function useSaveLoadGraph() {
|
||||
const workflowContext = useContext(WorkflowContext);
|
||||
|
||||
const [graph, setGraph] = useState<Graph | undefined>();
|
||||
useEffect(() => {
|
||||
// this hook is only for initial load
|
||||
if (graph !== undefined) return;
|
||||
// this is set when when click open on src/pages/Workflow/WorkflowManage/WorkflowList.tsx , so it usually won't be undefined.
|
||||
const graphJSON = workflowContext.openedWorkflowItem?.graphJSONString;
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
if (graphJSON) {
|
||||
void loadJSON(graphJSON).then(graph => {
|
||||
setGraph(graph);
|
||||
});
|
||||
}
|
||||
}, [graph, workflowContext.openedWorkflowItem]);
|
||||
const debouncedOnSave = useDebouncedCallback(onSave, [], 1000);
|
||||
useEffect(() => {
|
||||
// save on graph changed
|
||||
if (graph !== undefined) {
|
||||
void debouncedOnSave(graph, workflowContext);
|
||||
}
|
||||
}, [debouncedOnSave, graph, workflowContext]);
|
||||
|
||||
return [graph, setGraph] as const;
|
||||
}
|
||||
|
||||
async function onSave(graph: Graph, workflowContext: IWorkflowContext) {
|
||||
if (workflowContext.openedWorkflowItem === undefined) return;
|
||||
const graphJSON = graph.toJSON();
|
||||
const graphJSONString = JSON.stringify(graphJSON);
|
||||
if (graphJSONString === workflowContext.openedWorkflowItem.graphJSONString) return;
|
||||
const newItem = { ...workflowContext.openedWorkflowItem, graphJSONString };
|
||||
workflowContext.setOpenedWorkflowItem(newItem);
|
||||
await addWorkflowToWiki(newItem);
|
||||
}
|
||||
|
|
@ -56,6 +56,7 @@ export const AddItemDialog: React.FC<AddItemDialogProps> = ({
|
|||
title,
|
||||
tags,
|
||||
workspaceID,
|
||||
graphJSONString: '{}',
|
||||
};
|
||||
await onAdd(newItem);
|
||||
setDoneMessageSnackBarOpen(true);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import { Box, Button, Card, CardActionArea, CardActions, CardContent, CardMedia,
|
|||
import { PageType } from '@services/pages/interface';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import type { IWorkspaceWithMetadata } from '@services/workspaces/interface';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import { useLocation } from 'wouter';
|
||||
import { WorkflowContext } from '../useContext';
|
||||
import { DeleteConfirmationDialog } from './DeleteConfirmationDialog';
|
||||
import type { IWorkflowTiddler } from './useWorkflowDataSource';
|
||||
|
||||
|
|
@ -67,10 +68,16 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
|
|||
setLocation(`/${WindowNames.main}/${PageType.wiki}/${item.workspaceID}/`);
|
||||
window.service.wiki.wikiOperation(WikiChannel.openTiddler, item.workspaceID, item.title);
|
||||
}, [item, setLocation]);
|
||||
|
||||
const workflowContext = useContext(WorkflowContext);
|
||||
const handleOpenInTheGraphEditor = useCallback(() => {
|
||||
setLocation(`/${WindowNames.main}/${PageType.workflow}/${item.id}/`);
|
||||
workflowContext.setOpenedWorkflowItem(item);
|
||||
}, [item, setLocation, workflowContext]);
|
||||
const menuID = `workflow-list-item-menu-${item.id}`;
|
||||
return (
|
||||
<WorkflowCard>
|
||||
<CardActionArea>
|
||||
<CardActionArea onClick={handleOpenInTheGraphEditor}>
|
||||
{item.image && (
|
||||
<CardMedia
|
||||
component='img'
|
||||
|
|
@ -89,7 +96,7 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
|
|||
</CardContent>
|
||||
</CardActionArea>
|
||||
<ItemMenuCardActions>
|
||||
<Button>{t('Open')}</Button>
|
||||
<Button onClick={handleOpenInTheGraphEditor}>{t('Open')}</Button>
|
||||
<Button aria-controls={menuID} aria-haspopup='true' onClick={handleOpenItemMenu}>
|
||||
{anchorElement === null ? <MenuIcon /> : <MenuOpenIcon />}
|
||||
</Button>
|
||||
|
|
@ -113,6 +120,10 @@ export function WorkflowListItem(props: IWorkflowListItemProps) {
|
|||
|
||||
export interface IWorkflowListItem {
|
||||
description?: string;
|
||||
/**
|
||||
* Map to tiddler's text field. Store the JSON format of the graph.
|
||||
*/
|
||||
graphJSONString: string;
|
||||
id: string;
|
||||
image?: string;
|
||||
metadata?: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
|
||||
import { WikiChannel } from '@/constants/channels';
|
||||
import { usePromiseValue } from '@/helpers/useServiceValue';
|
||||
import { workflowTiddlerTagName } from '@services/wiki/plugin/nofloWorkflow/constants';
|
||||
|
|
@ -83,6 +84,7 @@ export function useWorkflowFromWiki(workspacesList: IWorkspaceWithMetadata[] | u
|
|||
const workflowItem: IWorkflowListItem = {
|
||||
id: `${workspace.id}:${tiddler.title}`,
|
||||
title: tiddler.title,
|
||||
graphJSONString: tiddler.text,
|
||||
description: tiddler.description,
|
||||
tags: tiddler.tags.filter(item => item !== workflowTiddlerTagName),
|
||||
workspaceID: workspace.id,
|
||||
|
|
@ -113,21 +115,7 @@ export function useWorkflows(workspacesList: IWorkspaceWithMetadata[] | undefine
|
|||
setWorkflows(initialWorkflows);
|
||||
}, [initialWorkflows]);
|
||||
const onAddWorkflow = useCallback(async (newItem: IWorkflowListItem) => {
|
||||
// add workflow to wiki
|
||||
await window.service.wiki.wikiOperation(
|
||||
WikiChannel.addTiddler,
|
||||
newItem.workspaceID,
|
||||
newItem.title,
|
||||
// only save an initial value at this creation time
|
||||
'{}',
|
||||
{
|
||||
type: 'application/json',
|
||||
tags: [...newItem.tags, workflowTiddlerTagName],
|
||||
description: newItem.description ?? '',
|
||||
'page-cover': newItem.image ?? '',
|
||||
} satisfies Omit<IWorkflowTiddler, 'text' | 'title'>,
|
||||
{ withDate: true },
|
||||
);
|
||||
await addWorkflowToWiki(newItem);
|
||||
// can overwrite a old workflow with same title
|
||||
setWorkflows((workflows) => [...workflows.filter(item => item.title !== newItem.title), newItem]);
|
||||
// update tag list in the search region tags filter
|
||||
|
|
@ -155,3 +143,20 @@ export function useWorkflows(workspacesList: IWorkspaceWithMetadata[] | undefine
|
|||
|
||||
return [workflows, onAddWorkflow, onDeleteWorkflow] as const;
|
||||
}
|
||||
|
||||
export async function addWorkflowToWiki(newItem: IWorkflowListItem) {
|
||||
await window.service.wiki.wikiOperation(
|
||||
WikiChannel.addTiddler,
|
||||
newItem.workspaceID,
|
||||
newItem.title,
|
||||
// Store the graph json on modify, or only save an initial value at this creation time
|
||||
newItem.graphJSONString || '{}',
|
||||
{
|
||||
type: 'application/json',
|
||||
tags: [...newItem.tags, workflowTiddlerTagName],
|
||||
description: newItem.description ?? '',
|
||||
'page-cover': newItem.image ?? '',
|
||||
} satisfies Omit<IWorkflowTiddler, 'text' | 'title'>,
|
||||
{ withDate: true },
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@ import { Route, Switch } from 'wouter';
|
|||
|
||||
import { PageType } from '@services/pages/interface';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import { useState } from 'react';
|
||||
import { GraphEditor } from './GraphEditor';
|
||||
import { WorkflowContext } from './useContext';
|
||||
import { WorkflowManage } from './WorkflowManage';
|
||||
import { IWorkflowListItem } from './WorkflowManage/WorkflowList';
|
||||
|
||||
export default function Workflow(): JSX.Element {
|
||||
const [openedWorkflowItem, setOpenedWorkflowItem] = useState<IWorkflowListItem | undefined>();
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route path={`/${WindowNames.main}/${PageType.workflow}/:title/`} component={GraphEditor} />
|
||||
<Route path={`/${WindowNames.main}/${PageType.workflow}/`} component={WorkflowManage} />
|
||||
</Switch>
|
||||
<WorkflowContext.Provider value={{ openedWorkflowItem, setOpenedWorkflowItem }}>
|
||||
<Switch>
|
||||
<Route path={`/${WindowNames.main}/${PageType.workflow}/:id/`} component={GraphEditor} />
|
||||
<Route path={`/${WindowNames.main}/${PageType.workflow}/`} component={WorkflowManage} />
|
||||
</Switch>
|
||||
</WorkflowContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
11
src/pages/Workflow/useContext.ts
Normal file
11
src/pages/Workflow/useContext.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { createContext } from 'react';
|
||||
import { IWorkflowListItem } from './WorkflowManage/WorkflowList';
|
||||
|
||||
export interface IWorkflowContext {
|
||||
openedWorkflowItem: IWorkflowListItem | undefined;
|
||||
setOpenedWorkflowItem: (newItem: IWorkflowListItem | undefined) => void;
|
||||
}
|
||||
export const WorkflowContext = createContext<IWorkflowContext>({
|
||||
openedWorkflowItem: undefined,
|
||||
setOpenedWorkflowItem: () => {},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue