feat: Add workflow tiddler to workspace

This commit is contained in:
linonetwo 2023-07-14 23:17:07 +08:00 committed by lin onetwo
parent abf531dc50
commit 72db085f2e
9 changed files with 101 additions and 23 deletions

View file

@ -460,7 +460,8 @@
"AddNewWorkflow": "Add new workflow",
"AddNewWorkflowDescription": "Create a new automated workflow and save it to the selected workspace wiki to backup.",
"BelongsToWorkspace": "Belongs to workspace",
"AddNewWorkflowDoneMessage": "Add successfully"
"AddNewWorkflowDoneMessage": "Add successfully",
"AddTagsDescription": "press Enter to input a tag"
},
"Description": "Description",
"Tags": "Tags",

View file

@ -463,6 +463,7 @@
"AddNewWorkflow": "添加新的工作流",
"AddNewWorkflowDescription": "创建新的自动化工作流并保存到所选的工作区Wiki里备份。",
"AddNewWorkflowDoneMessage": "添加成功",
"BelongsToWorkspace": "所属工作区"
"BelongsToWorkspace": "所属工作区",
"AddTagsDescription": "回车后变成标签的样子才算成功添加"
}
}

View file

@ -58,7 +58,7 @@ export enum WikiChannel {
getTiddlerTextDone = 'wiki-get-tiddler-text-done',
/**
* `$tw.wiki.getTiddlersAsJson('[all[]]')`
*
*
* result example:
* ```js
* `[

18
src/helpers/twUtils.ts Normal file
View file

@ -0,0 +1,18 @@
// Convert a date into UTC YYYYMMDDHHMMSSmmm format
export const stringifyDate = (value: Date) => {
return value.getUTCFullYear().toString() +
pad(value.getUTCMonth() + 1) +
pad(value.getUTCDate()) +
pad(value.getUTCHours()) +
pad(value.getUTCMinutes()) +
pad(value.getUTCSeconds()) +
pad(value.getUTCMilliseconds(), 3);
};
function pad(value: number, length = 2) {
let s = value.toString();
if (s.length < length) {
s = '000000000000000000000000000'.substring(0, length - s.length) + s;
}
return s;
}

View file

@ -8,7 +8,7 @@ import type { IWorkflowListItem } from './WorkflowList';
interface AddItemDialogProps {
availableFilterTags: string[];
onAdd: (newItem: IWorkflowListItem) => void;
onAdd: (newItem: IWorkflowListItem) => Promise<void>;
onClose: () => void;
open: boolean;
workspacesList: IWorkspaceWithMetadata[] | undefined;
@ -40,7 +40,7 @@ export const AddItemDialog: React.FC<AddItemDialogProps> = ({
onClose();
}, [onClose]);
const workspaceIDs = useMemo(() => workspacesList?.map(workspace => workspace.id) ?? [], [workspacesList]);
const onSubmit = useCallback(() => {
const onSubmit = useCallback(async () => {
const workspaceID = workspaceToSaveTo?.id ?? workspacesList?.[0]?.id;
if (!workspaceID || !workspaceIDs.includes(workspaceID ?? '')) {
console.error('No workspaceID found');
@ -57,9 +57,10 @@ export const AddItemDialog: React.FC<AddItemDialogProps> = ({
tags,
workspaceID,
};
onAdd(newItem);
await onAdd(newItem);
setDoneMessageSnackBarOpen(true);
}, [onAdd, tags, title, workspaceIDs, workspaceToSaveTo, workspacesList, setHasError, setDoneMessageSnackBarOpen]);
closeAndCleanup();
}, [workspaceToSaveTo?.id, workspacesList, workspaceIDs, title, tags, onAdd, closeAndCleanup]);
return (
<>
@ -111,7 +112,7 @@ export const AddItemDialog: React.FC<AddItemDialogProps> = ({
renderInput={(parameters) => (
<TextField
{...parameters}
label={t('Tags')}
label={`${t('Tags')} (${t('Workflow.AddTagsDescription')})`}
margin='dense'
/>
)}

View file

@ -20,8 +20,8 @@ export const WorkflowManage: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false);
const workspacesList = useWorkspacesListObservable();
const [availableFilterTags] = useAvailableFilterTags(workspacesList);
const [workflows, onAddWorkflow] = useWorkflows(workspacesList);
const [availableFilterTags, setTagsByWorkspace] = useAvailableFilterTags(workspacesList);
const [workflows, onAddWorkflow] = useWorkflows(workspacesList, setTagsByWorkspace);
const handleOpenDialog = useCallback(() => {
setDialogOpen(true);
@ -30,8 +30,8 @@ export const WorkflowManage: React.FC = () => {
const handleCloseDialog = useCallback(() => {
setDialogOpen(false);
}, []);
const handleDialogAddWorkflow = useCallback((newItem: IWorkflowListItem) => {
onAddWorkflow(newItem);
const handleDialogAddWorkflow = useCallback(async (newItem: IWorkflowListItem) => {
await onAddWorkflow(newItem);
handleCloseDialog();
}, [handleCloseDialog, onAddWorkflow]);

View file

@ -7,7 +7,8 @@ import type { ITiddlerFields } from 'tiddlywiki';
import { IWorkflowListItem } from './WorkflowList';
export function useAvailableFilterTags(workspacesList: IWorkspaceWithMetadata[] | undefined) {
const tagsByWorkspace = usePromiseValue<Record<string, string[]>>(
const [tagsByWorkspace, setTagsByWorkspace] = useState<Record<string, string[]>>({});
const initialTagsByWorkspace = usePromiseValue<Record<string, string[]>>(
async () => {
const tasks = workspacesList?.map(async (workspace) => {
try {
@ -32,6 +33,10 @@ export function useAvailableFilterTags(workspacesList: IWorkspaceWithMetadata[]
{},
[workspacesList],
)!;
// loading TagsByWorkspace using filter expression is expensive, so we only do this on initial load. Later just update&use local state value
useEffect(() => {
setTagsByWorkspace(initialTagsByWorkspace);
}, [initialTagsByWorkspace]);
const allTagsSet = useMemo(() => {
const allTags = new Set<string>();
for (const tags of Object.values(tagsByWorkspace)) {
@ -42,7 +47,7 @@ export function useAvailableFilterTags(workspacesList: IWorkspaceWithMetadata[]
return allTags;
}, [tagsByWorkspace]);
const allTags = useMemo(() => [...allTagsSet], [allTagsSet]);
return [allTags, allTagsSet, tagsByWorkspace] as const;
return [allTags, setTagsByWorkspace, allTagsSet, tagsByWorkspace] as const;
}
export interface IWorkflowTiddler extends ITiddlerFields {
@ -97,16 +102,41 @@ export function useWorkflowFromWiki(workspacesList: IWorkspaceWithMetadata[] | u
return workflowItems;
}
export function useWorkflows(workspacesList: IWorkspaceWithMetadata[] | undefined) {
export function useWorkflows(workspacesList: IWorkspaceWithMetadata[] | undefined, setTagsByWorkspace: React.Dispatch<React.SetStateAction<Record<string, string[]>>>) {
const [workflows, setWorkflows] = useState<IWorkflowListItem[]>([]);
const initialWorkflows = useWorkflowFromWiki(workspacesList);
// loading workflows using filter expression is expensive, so we only do this on initial load. Later just update&use local state value
useEffect(() => {
setWorkflows(initialWorkflows);
}, [initialWorkflows]);
const onAddWorkflow = useCallback((newItem: IWorkflowListItem) => {
// TODO: add workflow to wiki
setWorkflows((workflows) => [...workflows, newItem]);
}, []);
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,
description: newItem.description ?? '',
'page-cover': newItem.image ?? '',
} satisfies Omit<IWorkflowTiddler, 'text' | 'title'>,
{ withDate: true },
);
// 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
setTagsByWorkspace((previousTagsByWorkspace) => {
const newTags = newItem.tags.filter((tag) => !previousTagsByWorkspace[newItem.workspaceID]?.includes(tag));
if (newTags.length === 0) return previousTagsByWorkspace;
const previousTags = previousTagsByWorkspace[newItem.workspaceID] ?? [];
return {
...previousTagsByWorkspace,
[newItem.workspaceID]: [...previousTags, ...newTags],
};
});
}, [setTagsByWorkspace]);
return [workflows, onAddWorkflow] as const;
}

View file

@ -59,11 +59,32 @@ async function executeTWJavaScriptWhenIdle(script: string, options?: { onlyWhenV
*
* @param title tiddler title
* @param text tiddler text
* @param options stringifyed JSON object, is `{}` by default.
* @param extraMeta extra meta data, is `{}` by default, a JSONStringified object
*
* ## options
*
* - withDate: boolean, whether to add `created` and `modified` field to tiddler
*/
ipcRenderer.on(WikiChannel.addTiddler, async (event, nonceReceived: number, title: string, text: string, extraMeta: string = '{}') => {
ipcRenderer.on(WikiChannel.addTiddler, async (event, nonceReceived: number, title: string, text: string, extraMeta: string = '{}', optionsString: string = '{}') => {
const options = JSON.parse(optionsString) as { withDate?: boolean };
await executeTWJavaScriptWhenIdle(`
$tw.wiki.addTiddler({ title: \`${title}\`, text: \`${text}\`, ...${extraMeta} });
const dateObject = {};
${
options.withDate === true
? `
const existedTiddler = $tw.wiki.getTiddler(\`${title}\`);
let created = existedTiddler?.fields?.created;
const modified = $tw.utils.stringifyDate(new Date());
if (!existedTiddler) {
created = $tw.utils.stringifyDate(new Date());
}
dateObject.created = created;
dateObject.modified = modified;
`
: ''
}
$tw.wiki.addTiddler({ title: \`${title}\`, text: \`${text}\`, ...${extraMeta}, ...dateObject });
`);
ipcRenderer.send(WikiChannel.addTiddler, nonceReceived);
});

View file

@ -38,9 +38,15 @@ export const wikiOperations = {
[WikiChannel.runFilter]: async <T extends string[]>(workspaceID: string, filterString: string): Promise<T | undefined> => {
return await sendToMainWindowAndAwait<T>(WikiChannel.runFilter, workspaceID, [filterString]);
},
[WikiChannel.addTiddler]: async (workspaceID: string, title: string, text: string, meta?: unknown, options?: { timeout?: number }): Promise<void> => {
[WikiChannel.addTiddler]: async (
workspaceID: string,
title: string,
text: string,
meta?: Record<string, unknown>,
options?: { timeout?: number; withDate?: boolean },
): Promise<void> => {
const extraMeta = typeof meta === 'object' ? JSON.stringify(meta) : '{}';
await sendToMainWindowAndAwait(WikiChannel.addTiddler, workspaceID, [title, text, extraMeta], options);
await sendToMainWindowAndAwait(WikiChannel.addTiddler, workspaceID, [title, text, extraMeta, JSON.stringify(options ?? {})], options);
},
[WikiChannel.setTiddlerText]: async (workspaceID: string, title: string, value: string, options?: { timeout?: number }): Promise<void> => {
await sendToMainWindowAndAwait(WikiChannel.setTiddlerText, workspaceID, [title, value], options);