diff --git a/src/management-system-v2/app/(dashboard)/processes/[processId]/loading.tsx b/src/management-system-v2/app/(dashboard)/processes/[processId]/loading.tsx
new file mode 100644
index 000000000..b6aa4c08f
--- /dev/null
+++ b/src/management-system-v2/app/(dashboard)/processes/[processId]/loading.tsx
@@ -0,0 +1,19 @@
+import Content from '@/components/content';
+import { Space, Spin } from 'antd';
+import { LoadingOutlined } from '@ant-design/icons';
+
+const ProcessSkeleton = () => {
+ return (
+
+
+ } />
+
+
+ );
+};
+
+export default ProcessSkeleton;
diff --git a/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx
index 96f0e4b5a..46b27e580 100644
--- a/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx
+++ b/src/management-system-v2/app/(dashboard)/processes/[processId]/page.tsx
@@ -1,5 +1,45 @@
-import Auth from '@/components/auth';
-import Processes from './_page';
+import Auth, { getCurrentUser } from '@/components/auth';
+import Wrapper from './wrapper';
+import styles from './page.module.scss';
+import { FC, useEffect, useState } from 'react';
+import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation';
+import Modeler from '@/components/modeler';
+import cn from 'classnames';
+import { getProcess, getProcessVersionBpmn, getProcesses } from '@/lib/data/legacy/process';
+import { toCaslResource } from '@/lib/ability/caslAbility';
+
+type ProcessProps = {
+ params: { processId: string };
+ searchParams: { version?: string };
+};
+
+const Process = async ({ params: { processId }, searchParams }: ProcessProps) => {
+ // TODO: check if params is correct after fix release. And maybe don't need
+ // refresh in processes.tsx anymore?
+ console.log('processId', processId);
+ console.log('query', searchParams);
+ const selectedVersionId = searchParams.version ? searchParams.version : undefined;
+ const { ability } = await getCurrentUser();
+ // Only load bpmn if no version selected.
+ const process = await getProcess(processId, !selectedVersionId);
+ const processes = await getProcesses(ability);
+
+ if (!ability.can('view', toCaslResource('Process', process))) {
+ throw new Error('Forbidden.');
+ }
+
+ const selectedVersionBpmn = selectedVersionId
+ ? await getProcessVersionBpmn(processId, selectedVersionId)
+ : process.bpmn;
+
+ // Since the user is able to minimize and close the page, everyting is in a
+ // client component from here.
+ return (
+
+
+
+ );
+};
export default Auth(
{
@@ -7,5 +47,5 @@ export default Auth(
resource: 'Process',
fallbackRedirect: '/processes',
},
- Processes,
+ Process,
);
diff --git a/src/management-system-v2/app/(dashboard)/processes/[processId]/_page.tsx b/src/management-system-v2/app/(dashboard)/processes/[processId]/wrapper.tsx
similarity index 87%
rename from src/management-system-v2/app/(dashboard)/processes/[processId]/_page.tsx
rename to src/management-system-v2/app/(dashboard)/processes/[processId]/wrapper.tsx
index 8f7939cb3..78fd60d69 100644
--- a/src/management-system-v2/app/(dashboard)/processes/[processId]/_page.tsx
+++ b/src/management-system-v2/app/(dashboard)/processes/[processId]/wrapper.tsx
@@ -1,7 +1,7 @@
'use client';
import styles from './page.module.scss';
-import { FC, useEffect, useState } from 'react';
+import { FC, PropsWithChildren, useEffect, useState } from 'react';
import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation';
import Modeler from '@/components/modeler';
import cn from 'classnames';
@@ -25,13 +25,14 @@ import VersionCreationButton from '@/components/version-creation-button';
import ProcessCreationButton from '@/components/process-creation-button';
import { AuthCan } from '@/components/auth-can';
-type ProcessProps = {
- params: { processId: string };
-};
+type WrapperProps = PropsWithChildren<{
+ processName: string;
+ versions: { version: number; name: string; description: string }[];
+}>;
const LATEST_VERSION = { version: -1, name: 'Latest Version', description: '' };
-const Processes: FC = () => {
+const Wrapper = ({ children, processName, versions }: WrapperProps) => {
// TODO: check if params is correct after fix release. And maybe don't need
// refresh in processes.tsx anymore?
const { processId } = useParams();
@@ -66,7 +67,7 @@ const Processes: FC = () => {
const minimized = pathname !== `/processes/${processId}`;
const selectedVersionId = parseInt(query.get('version') ?? '-1');
const selectedVersion =
- process?.versions.find((version) => version.version === selectedVersionId) ?? LATEST_VERSION;
+ versions.find((version) => version.version === selectedVersionId) ?? LATEST_VERSION;
useEffect(() => {
// Reset closed state when page is not minimized anymore.
@@ -106,8 +107,8 @@ const Processes: FC = () => {
showSearch
filterOption={filterOption}
value={{
- value: process?.definitionId,
- label: process?.definitionName,
+ value: processId,
+ label: processName,
}}
onSelect={(_, option) => {
router.push(`/processes/${option.value}`);
@@ -167,7 +168,7 @@ const Processes: FC = () => {
>
)}
- options={[LATEST_VERSION].concat(process?.versions ?? []).map(({ version, name }) => ({
+ options={[LATEST_VERSION].concat(versions ?? []).map(({ version, name }) => ({
value: version,
label: name,
}))}
@@ -183,19 +184,17 @@ const Processes: FC = () => {
return (
/}
- items={breadcrumItems}
- />
- )
+ /}
+ items={breadcrumItems}
+ />
}
compact
wrapperClass={cn(styles.Wrapper, { [styles.minimized]: minimized })}
headerClass={cn(styles.HF, { [styles.minimizedHF]: minimized })}
>
-
+ {children}
{minimized ? (
setClosed(true)} />
) : null}
@@ -203,4 +202,4 @@ const Processes: FC = () => {
);
};
-export default Processes;
+export default Wrapper;
diff --git a/src/management-system-v2/app/(dashboard)/processes/loading.tsx b/src/management-system-v2/app/(dashboard)/processes/_loading.tsx
similarity index 88%
rename from src/management-system-v2/app/(dashboard)/processes/loading.tsx
rename to src/management-system-v2/app/(dashboard)/processes/_loading.tsx
index ce1c7f8df..7f898630c 100644
--- a/src/management-system-v2/app/(dashboard)/processes/loading.tsx
+++ b/src/management-system-v2/app/(dashboard)/processes/_loading.tsx
@@ -1,7 +1,7 @@
import Content from '@/components/content';
import { Skeleton, Space } from 'antd';
-const ProcessesSkeleton = async () => {
+const ProcessesSkeleton = () => {
return (
diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts
new file mode 100644
index 000000000..d91703661
--- /dev/null
+++ b/src/management-system-v2/app/api/auth/[...nextauth]/auth-options.ts
@@ -0,0 +1,21 @@
+import { AuthOptions } from 'next-auth';
+import { User } from '@/types/next-auth';
+import { randomUUID } from 'crypto';
+
+export const nextAuthOptions: AuthOptions = {
+ secret: process.env.NEXTAUTH_SECRET,
+ providers: [],
+ callbacks: {
+ async jwt({ token, user, trigger }) {
+ if (trigger === 'signIn') token.csrfToken = randomUUID();
+ if (user) token.user = user as User;
+ return token;
+ },
+ session(args) {
+ const { session, token } = args;
+ if (token.user) session.user = token.user;
+ session.csrfToken = token.csrfToken;
+ return session;
+ },
+ },
+};
diff --git a/src/management-system-v2/app/api/auth/[...nextauth]/route.ts b/src/management-system-v2/app/api/auth/[...nextauth]/route.ts
index 695072af1..33a0edea1 100644
--- a/src/management-system-v2/app/api/auth/[...nextauth]/route.ts
+++ b/src/management-system-v2/app/api/auth/[...nextauth]/route.ts
@@ -1,27 +1,7 @@
-import { AuthOptions } from 'next-auth';
import NextAuth from 'next-auth/next';
import Auth0Provider from 'next-auth/providers/auth0';
-import { User } from '@/types/next-auth';
-import { randomUUID } from 'crypto';
import CredentialsProvider from 'next-auth/providers/credentials';
-
-export const nextAuthOptions: AuthOptions = {
- secret: process.env.NEXTAUTH_SECRET,
- providers: [],
- callbacks: {
- async jwt({ token, user, trigger }) {
- if (trigger === 'signIn') token.csrfToken = randomUUID();
- if (user) token.user = user as User;
- return token;
- },
- session(args) {
- const { session, token } = args;
- if (token.user) session.user = token.user;
- session.csrfToken = token.csrfToken;
- return session;
- },
- },
-};
+import { nextAuthOptions } from './auth-options';
if (process.env.USE_AUTH0) {
nextAuthOptions.providers.push(
diff --git a/src/management-system-v2/components/auth.tsx b/src/management-system-v2/components/auth.tsx
index 9e6d0444a..72f7ec6fd 100644
--- a/src/management-system-v2/components/auth.tsx
+++ b/src/management-system-v2/components/auth.tsx
@@ -1,12 +1,14 @@
-import 'server-only';
-
import { ComponentProps, ComponentType, cache } from 'react';
import { getServerSession } from 'next-auth/next';
import { redirect } from 'next/navigation';
-import { nextAuthOptions } from '@/app/api/auth/[...nextauth]/route';
import { AuthCan, AuthCanProps } from './auth-can';
import { getAbilityForUser } from '@/lib/authorization/authorization';
+import { nextAuthOptions } from '@/app/api/auth/[...nextauth]/auth-options';
+// TODO: To enable PPR move the session redirect into this function, so it will
+// be called when the session is first accessed and everything above can PPR. For
+// permissions, each server component should check its permissions anyway, for
+// composability.
export const getCurrentUser = cache(async () => {
const session = await getServerSession(nextAuthOptions);
const ability = await getAbilityForUser(session?.user.id || '');
diff --git a/src/management-system-v2/components/modeler.tsx b/src/management-system-v2/components/modeler.tsx
index 1cc832f0c..82ed67272 100644
--- a/src/management-system-v2/components/modeler.tsx
+++ b/src/management-system-v2/components/modeler.tsx
@@ -1,12 +1,12 @@
'use client';
-import React, { FC, useEffect, useRef, useState } from 'react';
+import React, { FC, useEffect, useRef, useState, useTransition } from 'react';
import 'bpmn-js/dist/assets/bpmn-js.css';
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
import type ModelerType from 'bpmn-js/lib/Modeler';
import type ViewerType from 'bpmn-js/lib/NavigatedViewer';
-import { useParams, useSearchParams } from 'next/navigation';
+import { useParams, usePathname, useSearchParams } from 'next/navigation';
import ModelerToolbar from './modeler-toolbar';
import XmlEditor from './xml-editor';
@@ -18,6 +18,7 @@ import { useProcessBpmn } from '@/lib/process-queries';
import VersionToolbar from './version-toolbar';
import { copyProcessImage } from '@/lib/process-export/copy-process-image';
+import { updateProcess } from '@/lib/data/processes';
// 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
@@ -32,25 +33,26 @@ const BPMNViewer =
: null;
type ModelerProps = React.HTMLAttributes & {
- minimized: boolean;
+ processBpmn: string;
};
-const Modeler: FC = ({ minimized, ...props }) => {
+const Modeler = ({ processBpmn, ...divProps }: ModelerProps) => {
const { processId } = useParams();
+ const pathname = usePathname();
const [initialized, setInitialized] = useState(false);
const [xmlEditorBpmn, setXmlEditorBpmn] = useState(undefined);
const query = useSearchParams();
+ const [isPending, startTransition] = useTransition();
const canvas = useRef(null);
const modeler = useRef(null);
- const { mutateAsync: updateProcessMutation } = usePutAsset('/process/{definitionId}');
-
const setModeler = useModelerStateStore((state) => state.setModeler);
const setSelectedElementId = useModelerStateStore((state) => state.setSelectedElementId);
const setEditingDisabled = useModelerStateStore((state) => state.setEditingDisabled);
/// Derived State
+ const minimized = pathname !== `/processes/${processId}`;
const selectedVersionId = query.get('version');
useEffect(() => {
@@ -88,9 +90,9 @@ const Modeler: FC = ({ minimized, ...props }) => {
try {
const { xml } = await modeler.current!.saveXML({ format: true });
/* await updateProcess(processId as string, { bpmn: xml! }); */
- await updateProcessMutation({
- params: { path: { definitionId: processId as string } },
- body: { bpmn: xml },
+
+ startTransition(async () => {
+ await updateProcess(processId as string, xml);
});
} catch (err) {
console.log(err);
@@ -124,9 +126,7 @@ const Modeler: FC = ({ minimized, ...props }) => {
modeler.current?.destroy();
};
// only reset the modeler if we switch between editing being enabled or disabled
- }, [setModeler, selectedVersionId, processId, updateProcessMutation]);
-
- const { data: processBpmn } = useProcessBpmn(processId as string, selectedVersionId);
+ }, [setModeler, selectedVersionId, processId]);
useEffect(() => {
// only import the bpmn once (the effect will be retriggered when initialized is set to false at its end)
@@ -165,9 +165,8 @@ const Modeler: FC = ({ minimized, ...props }) => {
});
// if the bpmn contains unexpected content (text content for an element where the model does not define text) the modeler will remove it automatically => make sure the stored bpmn is the same as the one in the modeler
const { xml: cleanedBpmn } = await modeler.current.saveXML({ format: true });
- await updateProcessMutation({
- params: { path: { definitionId: processId as string } },
- body: { bpmn: cleanedBpmn },
+ startTransition(async () => {
+ await updateProcess(processId as string, cleanedBpmn);
});
}
};
@@ -188,7 +187,7 @@ const Modeler: FC = ({ minimized, ...props }) => {
)}
>
)}
-
+
);
};
diff --git a/src/management-system-v2/lib/authorization/caslRules.ts b/src/management-system-v2/lib/authorization/caslRules.ts
index 3e70164d4..e6939285f 100644
--- a/src/management-system-v2/lib/authorization/caslRules.ts
+++ b/src/management-system-v2/lib/authorization/caslRules.ts
@@ -218,7 +218,7 @@ async function rulesForSharedResources(ability: CaslAbility, userId: string) {
conditions: {
conditions: {
id: { $eq: share.resourceId },
- $: { $not_expired_value: share.expiredAt },
+ $: { $not_expired_value: share.expiredAt ?? undefined },
},
},
});
@@ -240,7 +240,7 @@ function rulesForShares(resource: ResourceType, userId: string, expiration: stri
conditions: {
resourceOwner: { $eq: userId },
resourceType: { $eq_string_case_insensitive: resource },
- $: { $not_expired_value: expiration },
+ $: { $not_expired_value: expiration ?? undefined },
},
conditionsOperator: 'and',
},
@@ -253,7 +253,7 @@ function rulesForShares(resource: ResourceType, userId: string, expiration: stri
conditions: {
sharedBy: { $eq: userId },
resourceType: { $eq_string_case_insensitive: resource },
- $: { $not_expired_value: expiration },
+ $: { $not_expired_value: expiration ?? undefined },
},
conditionsOperator: 'and',
},
@@ -307,7 +307,7 @@ export async function rulesForUser(userId: string) {
for (const resource of resources) {
if (!(resource in role.permissions)) continue;
- const permissionsForResource = role.permissions[resource];
+ const permissionsForResource = role.permissions[resource]!;
const actionsSet = new Set();
@@ -321,14 +321,14 @@ export async function rulesForUser(userId: string) {
action: 'manage-roles',
conditions: {
conditions: {
- $: { $not_expired_value: role.expiration },
+ $: { $not_expired_value: role.expiration ?? undefined },
},
},
});
}
if (actionsSet.has('share'))
- translatedRules.push(...rulesForShares(resource, userId, role.expiration));
+ translatedRules.push(...rulesForShares(resource, userId, role.expiration ?? null));
if (actionsSet.has('admin'))
translatedRules.push({
@@ -337,12 +337,14 @@ export async function rulesForUser(userId: string) {
conditions: {
conditions: {
resourceType: { $eq_string_case_insensitive: resource },
- $: { $not_expired_value: role.expiration },
+ $: { $not_expired_value: role.expiration ?? undefined },
},
},
});
const ownershipConditions =
- needOwnership.has(resource) && !actionsSet.has('admin') ? { owner: { $eq: userId } } : {};
+ needOwnership.has(resource) && !actionsSet.has('admin')
+ ? { owner: { $eq: userId } }
+ : undefined;
translatedRules.push({
subject: resource,
@@ -350,7 +352,7 @@ export async function rulesForUser(userId: string) {
conditions: {
conditions: {
...ownershipConditions,
- $: { $not_expired_value: role.expiration },
+ $: { $not_expired_value: role.expiration ?? undefined },
},
},
});
@@ -371,7 +373,7 @@ export async function rulesForUser(userId: string) {
// casl uses the ordering of the rules to decide
// this way inverted rules allways decide over normal rules
- translatedRules.sort((a, b) => +a.inverted - +b.inverted);
+ translatedRules.sort((a, b) => Number(a.inverted) - Number(b.inverted));
return { rules: packRules(translatedRules), expiration: firstExpiration };
}
diff --git a/src/management-system-v2/lib/authorization/permissionHelpers.ts b/src/management-system-v2/lib/authorization/permissionHelpers.ts
index d9f1a8c84..3dd0714e3 100644
--- a/src/management-system-v2/lib/authorization/permissionHelpers.ts
+++ b/src/management-system-v2/lib/authorization/permissionHelpers.ts
@@ -15,7 +15,7 @@ export function permissionNumberToIdentifiers(permission: number): ResourceActio
return ['admin'];
}
- const actions = [];
+ const actions: ResourceActionType[] = [];
// starts at 1 because none would be allways included and
// ends at length-1 because admin needs to be added with adminPermissions number
diff --git a/src/management-system-v2/lib/authorization/rolesHelper.ts b/src/management-system-v2/lib/authorization/rolesHelper.ts
index 961d3a8ff..706e5f9af 100644
--- a/src/management-system-v2/lib/authorization/rolesHelper.ts
+++ b/src/management-system-v2/lib/authorization/rolesHelper.ts
@@ -12,21 +12,26 @@ type Role = {
export function getAppliedRolesForUser(userId: string): Role[] {
if (userId === '')
- return [Object.values(roleMetaObjects).find((role) => role.default && role.name === '@guest')];
+ return [
+ Object.values(roleMetaObjects).find((role: any) => role.default && role.name === '@guest'),
+ ] as Role[];
const userRoles: Role[] = [];
const adminRole = Object.values(roleMetaObjects).find(
- (role) => role.default && role.name === '@admin',
- );
- if (adminRole.members.map((member) => member.userId).includes(userId)) userRoles.push(adminRole);
+ (role: any) => role.default && role.name === '@admin',
+ ) as any;
+ if (adminRole.members.map((member: any) => member.userId).includes(userId))
+ userRoles.push(adminRole);
userRoles.push(
- Object.values(roleMetaObjects).find((role) => role.default && role.name === '@everyone'),
+ Object.values(roleMetaObjects).find(
+ (role: any) => role.default && role.name === '@everyone',
+ ) as Role,
);
if (roleMappingsMetaObjects.users.hasOwnProperty(userId)) {
- roleMappingsMetaObjects.users[userId].forEach((role) => {
+ roleMappingsMetaObjects.users[userId].forEach((role: any) => {
const roleObject = roleMetaObjects[role.roleId];
if (roleObject.expiration === null || new Date(roleObject.expiration) > new Date())
userRoles.push(roleMetaObjects[role.roleId]);
diff --git a/src/management-system-v2/lib/data/legacy/_process.js b/src/management-system-v2/lib/data/legacy/_process.js
index 4624b6ba9..40a64e1f8 100644
--- a/src/management-system-v2/lib/data/legacy/_process.js
+++ b/src/management-system-v2/lib/data/legacy/_process.js
@@ -73,6 +73,17 @@ export async function getProcesses(ability, includeBPMN = false) {
return userProcesses;
}
+export async function getProcess(processDefinitionsId, includeBPMN = false) {
+ const process = processMetaObjects[processDefinitionsId];
+
+ if (!process) {
+ throw new Error(`Process with id ${processDefinitionsId} could not be found!`);
+ }
+
+ const bpmn = includeBPMN ? await getProcessBpmn(processDefinitionsId) : null;
+ return toExternalFormat({ ...process, bpmn });
+}
+
/**
* Throws if process with given id doesn't exist
*
@@ -129,7 +140,7 @@ export async function addProcess(processData) {
*
* @param {String} processDefinitionsId
* @param {String} newBpmn
- * @returns {Object} - contains the new process meta information
+ * @returns {Promise