diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/executions/deployments-modal.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/executions/deployments-modal.tsx index 536a46b1a..7c1d612b4 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/executions/deployments-modal.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/executions/deployments-modal.tsx @@ -137,7 +137,7 @@ const DeploymentsModal = ({ }); const openFolder = async (id: string) => { - const folder = await getFolder(id); + const folder = await getFolder(environment.spaceId, id); if ('error' in folder) { throw new Error('Failed to fetch folder'); } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx index bb15a82ec..5bc2767c5 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx @@ -1,12 +1,18 @@ import { getCurrentEnvironment } from '@/components/auth'; import Content from '@/components/content'; -import { getRoleById } from '@/lib/data/DTOs'; +import { getRoleById } from '@/lib/data/legacy/iam/roles'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; import { toCaslResource } from '@/lib/ability/caslAbility'; -import RoleId from './role-id-page'; import { getMembers } from '@/lib/data/DTOs'; import { getUserById } from '@/lib/data/DTOs'; +import { Button, Card, Space, Tabs } from 'antd'; +import { LeftOutlined } from '@ant-design/icons'; +import RoleGeneralData from './roleGeneralData'; +import RolePermissions from './rolePermissions'; +import RoleMembers from './role-members'; import { AuthenticatedUser } from '@/lib/data/user-schema'; +import SpaceLink from '@/components/space-link'; +import { getFolderById } from '@/lib/data/legacy/folders'; const Page = async ({ params: { roleId, environmentId }, @@ -15,6 +21,7 @@ const Page = async ({ }) => { const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); const role = await getRoleById(roleId, ability); + if (role && !ability.can('manage', toCaslResource('Role', role))) return ; if (!role) return ( @@ -35,9 +42,52 @@ const Page = async ({ .map((user) => getUserById(user.userId)), )) as AuthenticatedUser[]; - if (!ability.can('manage', toCaslResource('Role', role))) return ; + const roleParentFolder = role.parentId ? await getFolderById(role.parentId, ability) : undefined; - return ; + return ( + + + + + {role?.name} + + } + > +
+ + , + }, + { + key: 'permissions', + label: 'Permissions', + children: , + }, + { + key: 'members', + label: 'Manage Members', + children: ( + + ), + }, + ]} + /> + +
+
+ ); }; export default Page; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-id-page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-id-page.tsx deleted file mode 100644 index 2210d1eed..000000000 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-id-page.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Content from '@/components/content'; -import { Button, Card, Space, Tabs } from 'antd'; -import { LeftOutlined } from '@ant-design/icons'; -import RoleGeneralData from './roleGeneralData'; -import RolePermissions from './rolePermissions'; -import RoleMembers from './role-members'; -import { FC } from 'react'; -import { Role } from '@/lib/data/role-schema'; -import { AuthenticatedUser } from '@/lib/data/user-schema'; -import SpaceLink from '@/components/space-link'; - -const RoleId: FC<{ - role: Role; - usersInRole: AuthenticatedUser[]; - usersNotInRole: AuthenticatedUser[]; -}> = ({ role, usersInRole, usersNotInRole }) => { - return ( - - - - - {role?.name} - - } - > -
- - , - }, - { - key: 'permissions', - label: 'Permissions', - children: , - }, - { - key: 'members', - label: 'Manage Members', - children: ( - - ), - }, - ]} - /> - -
-
- ); -}; - -export default RoleId; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/roleGeneralData.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/roleGeneralData.tsx index b02cb9513..ba872d09d 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/roleGeneralData.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/roleGeneralData.tsx @@ -1,42 +1,106 @@ 'use client'; import { toCaslResource } from '@/lib/ability/caslAbility'; -import { Alert, App, Button, DatePicker, Form, Input } from 'antd'; -import { FC, useEffect, useState } from 'react'; +import { Alert, App, Button, DatePicker, Form, Input, Modal, Space } from 'antd'; +import { FC, useState } from 'react'; import dayjs from 'dayjs'; import germanLocale from 'antd/es/date-picker/locale/de_DE'; import { useAbilityStore } from '@/lib/abilityStore'; import { updateRole } from '@/lib/data/roles'; import { useRouter } from 'next/navigation'; -import { Role } from '@/lib/data/role-schema'; +import { Role, RoleInputSchema } from '@/lib/data/role-schema'; import { useEnvironment } from '@/components/auth-can'; +import useParseZodErrors, { antDesignInputProps } from '@/lib/useParseZodErrors'; +import FormSubmitButton from '@/components/form-submit-button'; +import { FolderTree } from '@/components/FolderTree'; +import { ProcessListItemIcon } from '@/components/process-list'; +import { Folder } from '@/lib/data/folder-schema'; import { wrapServerCall } from '@/lib/wrap-server-call'; -const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { +const InputSchema = RoleInputSchema.omit({ environmentId: true, permissions: true }); + +const FolderInput = ({ + onChange, + defaultFolder, +}: { + value?: string; + onChange?: (id: Role['parentId']) => void; + defaultFolder?: Folder; +}) => { + const [modalOpen, setModalOpen] = useState(false); + const [selectedFolder, setSelectedFolder] = useState<{ type: string; name: string } | undefined>( + () => + defaultFolder && { + type: 'folder', + name: defaultFolder.parentId ? defaultFolder.name : '< root >', + }, + ); + + return ( + <> + setModalOpen(false)} + closeIcon={null} + > + + + nodes.filter((node) => node.element.type === 'folder')} + treeProps={{ + onSelect(_, info) { + const element = info.node.element; + if (element.type !== 'folder') return; + + onChange?.(element.id); + setSelectedFolder(element); + setModalOpen(false); + }, + }} + showRootAsFolder + /> + + + + + ); +}; + +const RoleGeneralData: FC<{ role: Role; roleParentFolder?: Folder }> = ({ + role: _role, + roleParentFolder, +}) => { const app = App.useApp(); const ability = useAbilityStore((store) => store.ability); const [form] = Form.useForm(); const router = useRouter(); const environment = useEnvironment(); - const [submittable, setSubmittable] = useState(false); - const values = Form.useWatch('name', form); - - useEffect(() => { - form.validateFields({ validateOnly: true }).then( - () => { - setSubmittable(true); - }, - () => { - setSubmittable(false); - }, - ); - }, [form, values]); + const [errors, parseInput] = useParseZodErrors(InputSchema); const role = toCaslResource('Role', _role); async function submitChanges(values: Record) { - if (typeof values.expirationDayJs === 'object') { + if (typeof values?.expirationDayJs === 'object') { values.expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString(); delete values.expirationDayJs; } @@ -60,19 +124,18 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => { )} - + - + = ({ role: _role }) => { /> + + + + - + !!parseInput(values)} /> ); diff --git a/src/management-system-v2/app/globals.css b/src/management-system-v2/app/globals.css index e693a03de..277588387 100644 --- a/src/management-system-v2/app/globals.css +++ b/src/management-system-v2/app/globals.css @@ -133,6 +133,42 @@ } } +.proceed-robot-icon::before { + font-family: var(--custom-icon-font); + content: 'a'; + width: 1em; +} + +.proceed-screen-icon::before { + font-family: var(--custom-icon-font); + content: 'b'; + width: 1em; +} + +.proceed-user-icon::before { + font-family: var(--custom-icon-font); + content: 'c'; + width: 1em; +} + +.proceed-server-icon::before { + font-family: var(--custom-icon-font); + content: 'd'; + width: 1em; +} + +.proceed-laptop-icon::before { + font-family: var(--custom-icon-font); + content: 'e'; + width: 1em; +} + +.proceed-server-with-screen-icon::before { + font-family: var(--custom-icon-font); + content: 'f'; + width: 1em; +} + .orange:not(.djs-connection) .djs-visual > :nth-child(1) { fill: orange !important; } diff --git a/src/management-system-v2/app/layout.tsx b/src/management-system-v2/app/layout.tsx index cf74d2f6b..4dd5294b1 100644 --- a/src/management-system-v2/app/layout.tsx +++ b/src/management-system-v2/app/layout.tsx @@ -2,12 +2,17 @@ import 'antd/dist/reset.css'; import '@/public/antd.min.css'; import './globals.css'; import { Inter } from 'next/font/google'; +import localFont from 'next/font/local'; import { FC, PropsWithChildren } from 'react'; import App from '@/components/app'; import { ConfigProvider } from 'antd'; +import classNames from 'classnames'; + const inter = Inter({ subsets: ['latin'], variable: '--inter' }); +const myFont = localFont({ src: './performer-icons.woff', variable: '--custom-icon-font' }); + export const metadata = { title: 'PROCEED', description: 'Next Gen Business Processes', @@ -18,7 +23,7 @@ type RootLayoutProps = PropsWithChildren; const RootLayout: FC = ({ children }) => { return ( - + {children} diff --git a/src/management-system-v2/app/performer-icons.woff b/src/management-system-v2/app/performer-icons.woff new file mode 100644 index 000000000..be7f721a7 Binary files /dev/null and b/src/management-system-v2/app/performer-icons.woff differ diff --git a/src/management-system-v2/components/FolderTree.tsx b/src/management-system-v2/components/FolderTree.tsx index a3dbba489..4e8fd0c80 100644 --- a/src/management-system-v2/components/FolderTree.tsx +++ b/src/management-system-v2/components/FolderTree.tsx @@ -1,6 +1,6 @@ 'use client'; -import { getFolderContents } from '@/lib/data/folders'; +import { getFolder, getFolderContents } from '@/lib/data/folders'; import { Spin, Tree, TreeProps } from 'antd'; import React, { useEffect, useRef, useState } from 'react'; import { useEnvironment } from './auth-can'; @@ -23,17 +23,9 @@ function generateNode(element: FolderChildren): TreeNode { return { key: element.id, title: ( - + <> {element.name} - + ), isLeaf, element, @@ -44,15 +36,18 @@ export const FolderTree = ({ rootNodes, newChildrenHook, treeProps, + showRootAsFolder, }: { rootNodes?: FolderChildren[]; /** The return value is used to update the tree */ newChildrenHook?: (nodes: TreeNode[], parent?: TreeNode) => TreeNode[]; - treeProps?: TreeProps; + treeProps?: TreeProps; + showRootAsFolder?: boolean; }) => { const spaceId = useEnvironment().spaceId; const nodeMap = useRef(new Map()); + const loadedKeys = useRef(new Map()); const [tree, setTree] = useState(() => { if (!rootNodes) return []; @@ -71,25 +66,41 @@ export const FolderTree = ({ const loadData = async (node?: TreeNode) => { const nodeId = node?.element.id; - const children = await getFolderContents(spaceId, nodeId); - console.log(children); - if ('error' in children) return; + let rootNode: TreeNode | undefined; + if (!nodeId && showRootAsFolder) { + const rootFolder = await getFolder(spaceId); + if ('error' in rootFolder) return; - let childrenNodes = children.map(generateNode); + rootNode = generateNode({ type: 'folder', name: '< root >', id: rootFolder.id }); + if (newChildrenHook) rootNode = newChildrenHook?.([rootNode])[0]; + + if (!rootNode) return; + + nodeMap.current.set(rootNode.key, rootNode); + + console.log({ rootNode }); + } - const actualNode = nodeId ? nodeMap.current.get(nodeId) : undefined; - if (newChildrenHook) childrenNodes = newChildrenHook(childrenNodes, actualNode); + const parentNode = nodeId ? nodeMap.current.get(nodeId) : rootNode; + // get children nodes + const children = await getFolderContents(spaceId, nodeId); + if ('error' in children) return; + let childrenNodes = children.map(generateNode); + if (newChildrenHook) childrenNodes = newChildrenHook(childrenNodes, parentNode); for (const node of childrenNodes) nodeMap.current.set(node.key, node); - if (nodeId) { - actualNode!.children = childrenNodes; - actualNode!.isLeaf = childrenNodes.length === 0; + if (parentNode) { + parentNode!.children = childrenNodes; + parentNode!.isLeaf = childrenNodes.length === 0; + loadedKeys.current.set(parentNode.key, true); + } + if (nodeId) { // trigger re-render setTree((currentTree) => [...currentTree]); } else { - setTree(childrenNodes); + setTree(parentNode ? [parentNode] : childrenNodes); } }; @@ -110,7 +121,22 @@ export const FolderTree = ({ return ( - + ); }; diff --git a/src/management-system-v2/components/bpmn-canvas.tsx b/src/management-system-v2/components/bpmn-canvas.tsx index a0c11086a..591faeb19 100644 --- a/src/management-system-v2/components/bpmn-canvas.tsx +++ b/src/management-system-v2/components/bpmn-canvas.tsx @@ -19,6 +19,16 @@ import { copyProcessImage } from '@/lib/process-export/copy-process-image'; import Modeling, { CommandStack, Shape } from 'bpmn-js/lib/features/modeling/Modeling'; import { Root, Element } from 'bpmn-js/lib/model/Types'; +import { + PerformerRulesModule, + PerformerReplaceModule, + PerformerRendererModule, + PerformerLabelEditingModule, + PerformerPaletteProviderModule, + PerformerContextPadProviderModule, + PerformerLabelBehaviorModule, +} from '@/lib/modeler-extensions/Performers'; + // Conditionally load the BPMN modeler only on the client, because it uses // "window" reference. It won't be included in the initial bundle, but will be // immediately loaded when the initial script first executes (not after @@ -192,11 +202,28 @@ const BPMNCanvas = forwardRef( const ModelerOrViewer = type === 'modeler' ? Modeler : type === 'navigatedviewer' ? NavigatedViewer : Viewer; + // this will allow any type of viewer or editor we create to render our performer elements + const additionalModules: any[] = [PerformerRendererModule]; + + // the modules related to editing can only be registered in modelers since they depend on + // other modeler modules + if (type === 'modeler') { + additionalModules.push( + PerformerContextPadProviderModule, + PerformerPaletteProviderModule, + PerformerLabelEditingModule, + PerformerReplaceModule, + PerformerRulesModule, + PerformerLabelBehaviorModule, + ); + } + modeler.current = new ModelerOrViewer({ container: canvas.current!, moddleExtensions: { proceed: schema, }, + additionalModules, }); if (type === 'modeler') { @@ -228,7 +255,7 @@ const BPMNCanvas = forwardRef( m.destroy(); }); }; - }, [Modeler, NavigatedViewer, type]); + }, [Modeler, Viewer, NavigatedViewer, type]); useEffect(() => { // Store handlers so we can remove them later. @@ -303,6 +330,7 @@ const BPMNCanvas = forwardRef( // Import the new bpmn. await m.importXML(bpmn.bpmn); + if (m !== modeler.current) { // The modeler was reset in the meantime. return; diff --git a/src/management-system-v2/components/form-submit-button.tsx b/src/management-system-v2/components/form-submit-button.tsx index 90804291d..10d6d0f69 100644 --- a/src/management-system-v2/components/form-submit-button.tsx +++ b/src/management-system-v2/components/form-submit-button.tsx @@ -1,38 +1,51 @@ +import { ButtonProps } from '@react-email/components'; import { FormInstance, Form, Button } from 'antd'; -import React, { ReactNode, useState } from 'react'; +import React, { useState } from 'react'; -const FormSubmitButton = ({ - form, +const FormSubmitButton = ({ + form: _form, onSubmit, submitText, + isValidData, + buttonProps, }: { - form: FormInstance; - onSubmit: Function; - submitText: ReactNode; + form?: FormInstance; + onSubmit?: Function; + submitText: string; + isValidData?: (data: TData) => boolean; + buttonProps?: ButtonProps; }) => { const [submittable, setSubmittable] = useState(false); + const [hookForm] = Form.useForm(); + const form = _form ?? hookForm; + // Watch all values - const values = Form.useWatch([], form); + const values = Form.useWatch([], _form); React.useEffect(() => { - form.validateFields({ validateOnly: true }).then( - () => { - setSubmittable(true); - }, - () => { - setSubmittable(false); - }, - ); + if (isValidData) { + setSubmittable(isValidData(values as TData)); + } else { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + } }, [form, values]); return (