diff --git a/src/modules/admin/layout/page-content.component.tsx b/src/modules/admin/layout/page-content.component.tsx index dea57081..15b4b981 100644 --- a/src/modules/admin/layout/page-content.component.tsx +++ b/src/modules/admin/layout/page-content.component.tsx @@ -10,7 +10,7 @@ import PageWrapper from './page-wrapper.component'; const { Content } = Layout; export default function PageContent() { - const Flow = lazy(() => import('../../flow/components/index.component')); + const Flow = lazy(() => import('../../flow/components/_router.component')); const Database = lazy( () => import('../../database/component/_router.component'), diff --git a/src/modules/content/component/_router.component.tsx b/src/modules/content/component/_router.component.tsx index 9f4c1bf0..170c189b 100644 --- a/src/modules/content/component/_router.component.tsx +++ b/src/modules/content/component/_router.component.tsx @@ -29,8 +29,6 @@ type SchemaWithModule = ISchema & { module?: IContentModule; }; -type SchemaRouteParams = { database?: string; reference?: string }; - const applyQuickFilter = (filterValue: string) => (schema: SchemaWithModule) => { if (!filterValue) { diff --git a/src/modules/flow/components/_menu/manager.component.tsx b/src/modules/flow/components/_menu/manager.component.tsx new file mode 100644 index 00000000..5ef9f4f1 --- /dev/null +++ b/src/modules/flow/components/_menu/manager.component.tsx @@ -0,0 +1,53 @@ +import { Menu } from 'antd'; +import { useEffect, useState } from 'react'; +import { Link, matchPath, useLocation } from 'react-router-dom'; +import { ADMIN_URL } from '../../../admin/admin.constants'; +import Icon from '../../../admin/component/icon.component'; +import MenuBlock from '../../../admin/component/menu-block.component'; + +type IMenuItem = { + key: string; + icon: string; + title: string; + path: string; +}; + +const elements: IMenuItem[] = [ + { + key: 'flows', + icon: 'table_rows', + title: 'Flows', + path: 'list', + }, +]; + +export default function ManagerMenuComponent() { + const location = useLocation(); + const base = `${ADMIN_URL}/flow`; + const [selected, setSelected] = useState([]); + + useEffect(() => { + setSelected([]); + + for (const e of elements) { + const isRouted = matchPath(location.pathname, `${base}/${e.path}`); + + if (isRouted) { + setSelected([e.key]); + break; + } + } + }, [location]); + + return ( + + + {elements.map(e => ( + }> + {e.title} + + ))} + + + ); +} diff --git a/src/modules/flow/components/_router.component.tsx b/src/modules/flow/components/_router.component.tsx new file mode 100644 index 00000000..bafbba2a --- /dev/null +++ b/src/modules/flow/components/_router.component.tsx @@ -0,0 +1,224 @@ +import { SearchOutlined } from '@ant-design/icons'; +import { + Divider, + Empty, + Input, + Layout, + Result, + Tree, + TreeDataNode, +} from 'antd'; +import Sider from 'antd/lib/layout/Sider'; +import { QueryBuilder } from 'odata-query-builder'; +import React, { lazy, useEffect, useState } from 'react'; +import { + generatePath, + Navigate, + Route, + Routes, + useLocation, + useNavigate, +} from 'react-router'; +import MenuBlock from '../../admin/component/menu-block.component'; +import { useHttpClient } from '../../admin/library/use-http-client'; +import { IContentModule } from '../../content/interface/content-module.interface'; +import { IFlow } from '../interface'; +import CreateFlowComponent from './create.component'; +import FlowListComponent from './list.component'; +import ManagerMenuComponent from './_menu/manager.component'; + +type FlowWithModule = IFlow & { + module?: IContentModule; +}; + +const applyQuickFilter = (filterValue: string) => (flow: FlowWithModule) => { + if (!filterValue) { + return true; + } + + filterValue = filterValue.toLowerCase(); + const words = filterValue.replace(/\s+/, ' ').split(' '); + + for (const word of words) { + // Match in the title + if (flow.name.toLowerCase().match(word)) { + return true; + } + + // In the matched module + if (flow.moduleId) { + if (flow.module.name.toLowerCase().match(word)) { + return true; + } + } + } + + return false; +}; + +export default function FlowRouterComponent() { + const navigate = useNavigate(); + const location = useLocation(); + + const [quickFilter, setQuickFilter] = useState(null); + const [selected, setSelected] = useState(null); + const [tree, setTree] = useState([]); + + const [showCreate, setShowCreate] = useState(false); + + const [{ data: flows, loading, error }, refetch] = useHttpClient< + FlowWithModule[] + >( + '/api/odata/main/flow' + + new QueryBuilder().top(1_000).select('*,module').toQuery(), + { + useCache: false, + }, + ); + + useEffect(() => { + const segments = location.pathname.split('/').slice(4, 5); + + if (segments && segments.length === 1) { + setSelected([segments[0]]); + } + }, [location.pathname]); + + useEffect(() => { + if (flows) { + const modules: IContentModule[] = []; + const tree: TreeDataNode[] = []; + + // Collect modules from existing references + for (const flow of flows + .filter(f => f.module) + .sort((a, b) => (a.name > b.name ? 1 : -1))) { + if (!modules.find(m => flow.module.id === m.id)) { + modules.push(flow.module); + } + } + + // Build the module branches + for (const module of modules) { + const children = flows + .filter(f => f.module) + .filter(f => f.module.id == module.id) + .filter(applyQuickFilter(quickFilter)) + .sort((a, b) => (a.name > b.name ? 1 : -1)) + .map(f => ({ + key: f.id, + title: f.name, + isLeaf: true, + })); + + // Filtered + if (!children.length) { + continue; + } + + tree.push({ + title: module.name, + key: module.id, + selectable: false, + children, + isLeaf: false, + }); + } + + // Add the flows which are not in any module + for (const flow of flows + .filter(s => !s.module) + .filter(applyQuickFilter(quickFilter))) { + tree.push({ + title: flow.name, + key: flow.id, + isLeaf: true, + }); + } + + setTree(tree); + } + }, [loading, quickFilter]); + + // Clear the quick filter when the location changes + // I assume the user clicked to the match + useEffect(() => setQuickFilter(null), [location.pathname]); + + if (error) { + return ( + + ); + } + + const Artboard = lazy(() => import('./artboard.component')); + + return ( + + + +
+ } + value={quickFilter} + onChange={e => { + setQuickFilter(e.target?.value ?? null); + }} + size="small" + allowClear + /> +
+ + + {tree.length ? ( + { + if (selected.length) { + const path = generatePath('/admin/flow/artboard/:flowId', { + flowId: selected[0].toString(), + }); + + navigate(path); + } + }} + /> + ) : ( + + )} +
+ + +
+ + + + }> + }> + } + > + + + + {showCreate ? ( + { + setShowCreate(false); + refetch(); + }} + /> + ) : undefined} +
+ ); +} diff --git a/src/modules/flow/components/artboard/catalog.component.tsx b/src/modules/flow/components/artboard/catalog.component.tsx index 58fc256c..11c17854 100644 --- a/src/modules/flow/components/artboard/catalog.component.tsx +++ b/src/modules/flow/components/artboard/catalog.component.tsx @@ -64,75 +64,73 @@ export default function ArtboardCatalogComponent() { }; return ( -
- - - - Catalog - - } - ghost - > -
- setSearch(value)} - allowClear - autoFocus={!isCollapsed} - /> -
- -
- ( - onDragStart(event, lambda.type)} - draggable - onTouchStart={event => onDragStart(event, lambda.type)} - actions={[ - addNode(lambda)} - > - - , - ]} - > - - } - title={ - - {startCase(lambda.type)} - - } - description={ - lambda.description ?? 'Does not have a description?!' - } - /> - - )} - /> -
-
-
-
+ + + + Catalog + + } + ghost + > +
+ setSearch(value)} + allowClear + autoFocus={!isCollapsed} + /> +
+ +
+ ( + onDragStart(event, lambda.type)} + draggable + onTouchStart={event => onDragStart(event, lambda.type)} + className="bg-midnight-700" + actions={[ + addNode(lambda)} + > + + , + ]} + > + + } + title={ + {startCase(lambda.type)} + } + description={ + lambda.description ?? 'Does not have a description?!' + } + /> + + )} + /> +
+
+
); } diff --git a/src/modules/flow/components/create.component.tsx b/src/modules/flow/components/create.component.tsx index 1a1ebde6..3e6d0440 100644 --- a/src/modules/flow/components/create.component.tsx +++ b/src/modules/flow/components/create.component.tsx @@ -29,13 +29,13 @@ export default function CreateFlowComponent({ onClose }: Props) { name="flow" initialValues={{ remember: true }} onFinish={fdata => { - message.info('Sending the WF data...'); sendRequest({ name: fdata.name, nodes: [], edges: [], }).then(id => { redirect(`/admin/flow/artboard/${id}`); + onClose(); message.success('Flow ready!'); }); }} diff --git a/src/modules/flow/components/index.component.tsx b/src/modules/flow/components/index.component.tsx deleted file mode 100644 index 37402cab..00000000 --- a/src/modules/flow/components/index.component.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { lazy } from 'react'; -import { Route, Routes } from 'react-router-dom'; -import FlowListComponent from './list.component'; - -export default function FlowPageComponent() { - const Create = lazy(() => import('./create.component')); - const Artboard = lazy(() => import('./artboard.component')); - - return ( - - }> - }> - - ); -} diff --git a/src/modules/flow/components/list.component.tsx b/src/modules/flow/components/list.component.tsx index dc1b4ac5..dcdeb263 100644 --- a/src/modules/flow/components/list.component.tsx +++ b/src/modules/flow/components/list.component.tsx @@ -65,7 +65,7 @@ export default function FlowListComponent() { , }} diff --git a/src/modules/flow/interface/flow.interface.ts b/src/modules/flow/interface/flow.interface.ts index 512231c4..766fc11a 100644 --- a/src/modules/flow/interface/flow.interface.ts +++ b/src/modules/flow/interface/flow.interface.ts @@ -7,6 +7,11 @@ export interface IFlow { */ readonly id: string; + /** + * Content module's identifier + */ + moduleId?: string; + /** * Human readable display name */ diff --git a/storage/seed/identity.blueprint.json b/storage/seed/identity.blueprint.json index 0be3dfe2..152a7d9c 100644 --- a/storage/seed/identity.blueprint.json +++ b/storage/seed/identity.blueprint.json @@ -115,7 +115,8 @@ "flows": [ { "id": "51c0abf2-5924-4fdf-a920-a1b3487f6710", - "name": "Authentication - Sign In", + "moduleId": "d01ab3a7-8ffc-4dd3-b706-ef194e460535", + "name": "Sign In", "nodes": [ { "id": "trigger.http.1", @@ -301,7 +302,8 @@ }, { "id": "ced473f0-522c-47db-a88c-f0f981d93f9e", - "name": "Authentication - Sign Up", + "moduleId": "d01ab3a7-8ffc-4dd3-b706-ef194e460535", + "name": "Sign Up", "nodes": [ { "id": "auth.sign.in.44", diff --git a/storage/seed/system.blueprint.json b/storage/seed/system.blueprint.json index 9c498dbe..b7263c51 100644 --- a/storage/seed/system.blueprint.json +++ b/storage/seed/system.blueprint.json @@ -24,6 +24,17 @@ ], "args": {} }, + { + "reference": "moduleId", + "columnName": "moduleId", + "title": "Module ID", + "type": "uuid", + "tags": [ + "nullable" + ], + "args": {}, + "defaultValue": null + }, { "reference": "name", "columnName": "name", @@ -55,7 +66,15 @@ ], "indices": [], "uniques": [], - "relations": [], + "relations": [ + { + "kind": "belongs-to-one", + "name": "module", + "target": "Module", + "localField": "moduleId", + "remoteField": "id" + } + ], "tags": [ "active", "system" @@ -546,7 +565,8 @@ "flows": [ { "id": "30d9c6a9-3d0d-424d-ab5e-7ffe0ff7c840", - "name": "Blueprint Store - Proxy", + "moduleId": "c2046325-9d0e-42a4-aa55-1116fdd97913", + "name": "Blueprint Proxy", "nodes": [ { "id": "trigger.http.1", @@ -782,7 +802,8 @@ }, { "id": "2ecfd7bd-d051-46d4-9e8e-a1229dd1857f", - "name": "Lambda - List", + "moduleId": "c2046325-9d0e-42a4-aa55-1116fdd97913", + "name": "Lambdas", "nodes": [ { "id": "lambda.read.184",