feat: save graph to wiki

This commit is contained in:
linonetwo 2023-07-15 18:02:58 +08:00 committed by lin onetwo
parent e265bb4fcf
commit 4dfbc57966
8 changed files with 123 additions and 98 deletions

View file

@ -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}

View file

@ -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;
}

View 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);
}

View file

@ -56,6 +56,7 @@ export const AddItemDialog: React.FC<AddItemDialogProps> = ({
title,
tags,
workspaceID,
graphJSONString: '{}',
};
await onAdd(newItem);
setDoneMessageSnackBarOpen(true);

View file

@ -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?: {

View file

@ -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 },
);
}

View file

@ -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>
);
}

View 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: () => {},
});