Skip to content

Commit 4bb4f72

Browse files
authored
ms2/select role folder (#362)
* fix(ms2/useParseZodErrors): avoid unnecessary rerenders * refactor(ms2/FormSubmitButton): easier use * fix(ms2/FormSubmitButton): use form instance for watch * refactor(ms2/role-general-data): use zod schema and FormSubmitButton * fix(ms2/abilities): identify folders for processes * fix(ms2/FolderTree): correct prop types * refactor(ms2/role-page): removed unnecessary component * feat(ms2/getFolder): return root if no id * feat(ms2/FolderTree): show root as folder * style(ms2/FolderTree): prevent loading spinner from overflowing * fix(ms2/FolderTree): prevent overflow I tried to also add text-overflow: ellipsis, but I didn't manage to pull it off * feat(ms2/role-general-data): choose role folder * refactor * revert changes to schema * remove accidental expression
1 parent 6d3e24a commit 4bb4f72

File tree

9 files changed

+241
-143
lines changed

9 files changed

+241
-143
lines changed

src/management-system-v2/app/(dashboard)/[environmentId]/executions/deployments-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ const DeploymentsModal = ({
137137
});
138138

139139
const openFolder = async (id: string) => {
140-
const folder = await getFolder(id);
140+
const folder = await getFolder(environment.spaceId, id);
141141
if ('error' in folder) {
142142
throw new Error('Failed to fetch folder');
143143
}

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { getCurrentEnvironment } from '@/components/auth';
22
import Content from '@/components/content';
3-
import { getRoleById } from '@/lib/data/DTOs';
3+
import { getRoleById } from '@/lib/data/legacy/iam/roles';
44
import UnauthorizedFallback from '@/components/unauthorized-fallback';
55
import { toCaslResource } from '@/lib/ability/caslAbility';
6-
import RoleId from './role-id-page';
76
import { getMembers } from '@/lib/data/DTOs';
87
import { getUserById } from '@/lib/data/DTOs';
8+
import { Button, Card, Space, Tabs } from 'antd';
9+
import { LeftOutlined } from '@ant-design/icons';
10+
import RoleGeneralData from './roleGeneralData';
11+
import RolePermissions from './rolePermissions';
12+
import RoleMembers from './role-members';
913
import { AuthenticatedUser } from '@/lib/data/user-schema';
14+
import SpaceLink from '@/components/space-link';
15+
import { getFolderById } from '@/lib/data/legacy/folders';
1016

1117
const Page = async ({
1218
params: { roleId, environmentId },
@@ -15,6 +21,7 @@ const Page = async ({
1521
}) => {
1622
const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId);
1723
const role = await getRoleById(roleId, ability);
24+
if (role && !ability.can('manage', toCaslResource('Role', role))) return <UnauthorizedFallback />;
1825

1926
if (!role)
2027
return (
@@ -35,9 +42,52 @@ const Page = async ({
3542
.map((user) => getUserById(user.userId)),
3643
)) as AuthenticatedUser[];
3744

38-
if (!ability.can('manage', toCaslResource('Role', role))) return <UnauthorizedFallback />;
45+
const roleParentFolder = role.parentId ? await getFolderById(role.parentId, ability) : undefined;
3946

40-
return <RoleId role={role} usersNotInRole={usersNotInRole} usersInRole={usersInRole} />;
47+
return (
48+
<Content
49+
title={
50+
<Space>
51+
<SpaceLink href={`/iam/roles`}>
52+
<Button icon={<LeftOutlined />} type="text">
53+
Roles
54+
</Button>
55+
</SpaceLink>
56+
{role?.name}
57+
</Space>
58+
}
59+
>
60+
<div style={{ maxWidth: '800px', margin: 'auto' }}>
61+
<Card>
62+
<Tabs
63+
items={[
64+
{
65+
key: 'generalData',
66+
label: 'General Data',
67+
children: <RoleGeneralData role={role} roleParentFolder={roleParentFolder} />,
68+
},
69+
{
70+
key: 'permissions',
71+
label: 'Permissions',
72+
children: <RolePermissions role={role} />,
73+
},
74+
{
75+
key: 'members',
76+
label: 'Manage Members',
77+
children: (
78+
<RoleMembers
79+
role={role}
80+
usersNotInRole={usersNotInRole}
81+
usersInRole={usersInRole}
82+
/>
83+
),
84+
},
85+
]}
86+
/>
87+
</Card>
88+
</div>
89+
</Content>
90+
);
4191
};
4292

4393
export default Page;

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/role-id-page.tsx

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/roleGeneralData.tsx

Lines changed: 93 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,106 @@
11
'use client';
22

33
import { toCaslResource } from '@/lib/ability/caslAbility';
4-
import { Alert, App, Button, DatePicker, Form, Input } from 'antd';
5-
import { FC, useEffect, useState } from 'react';
4+
import { Alert, App, Button, DatePicker, Form, Input, Modal, Space } from 'antd';
5+
import { FC, useState } from 'react';
66
import dayjs from 'dayjs';
77
import germanLocale from 'antd/es/date-picker/locale/de_DE';
88
import { useAbilityStore } from '@/lib/abilityStore';
99
import { updateRole } from '@/lib/data/roles';
1010
import { useRouter } from 'next/navigation';
11-
import { Role } from '@/lib/data/role-schema';
11+
import { Role, RoleInputSchema } from '@/lib/data/role-schema';
1212
import { useEnvironment } from '@/components/auth-can';
13+
import useParseZodErrors, { antDesignInputProps } from '@/lib/useParseZodErrors';
14+
import FormSubmitButton from '@/components/form-submit-button';
15+
import { FolderTree } from '@/components/FolderTree';
16+
import { ProcessListItemIcon } from '@/components/process-list';
17+
import { Folder } from '@/lib/data/folder-schema';
1318
import { wrapServerCall } from '@/lib/wrap-server-call';
1419

15-
const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => {
20+
const InputSchema = RoleInputSchema.omit({ environmentId: true, permissions: true });
21+
22+
const FolderInput = ({
23+
onChange,
24+
defaultFolder,
25+
}: {
26+
value?: string;
27+
onChange?: (id: Role['parentId']) => void;
28+
defaultFolder?: Folder;
29+
}) => {
30+
const [modalOpen, setModalOpen] = useState(false);
31+
const [selectedFolder, setSelectedFolder] = useState<{ type: string; name: string } | undefined>(
32+
() =>
33+
defaultFolder && {
34+
type: 'folder',
35+
name: defaultFolder.parentId ? defaultFolder.name : '< root >',
36+
},
37+
);
38+
39+
return (
40+
<>
41+
<Modal
42+
title="Choose a folder"
43+
open={modalOpen}
44+
onCancel={() => setModalOpen(false)}
45+
closeIcon={null}
46+
>
47+
<Space direction="vertical" style={{ maxWidth: '100%' }}>
48+
<Button
49+
onClick={() => {
50+
onChange?.(undefined);
51+
setSelectedFolder(undefined);
52+
setModalOpen(false);
53+
}}
54+
type="default"
55+
danger
56+
>
57+
Clear folder
58+
</Button>
59+
<FolderTree
60+
newChildrenHook={(nodes) => nodes.filter((node) => node.element.type === 'folder')}
61+
treeProps={{
62+
onSelect(_, info) {
63+
const element = info.node.element;
64+
if (element.type !== 'folder') return;
65+
66+
onChange?.(element.id);
67+
setSelectedFolder(element);
68+
setModalOpen(false);
69+
},
70+
}}
71+
showRootAsFolder
72+
/>
73+
</Space>
74+
</Modal>
75+
<Button onClick={() => setModalOpen(true)}>
76+
{selectedFolder ? (
77+
<>
78+
<ProcessListItemIcon item={selectedFolder as any} /> {selectedFolder.name}
79+
</>
80+
) : (
81+
'Select folder'
82+
)}
83+
</Button>
84+
</>
85+
);
86+
};
87+
88+
const RoleGeneralData: FC<{ role: Role; roleParentFolder?: Folder }> = ({
89+
role: _role,
90+
roleParentFolder,
91+
}) => {
1692
const app = App.useApp();
1793
const ability = useAbilityStore((store) => store.ability);
1894
const [form] = Form.useForm();
1995
const router = useRouter();
2096
const environment = useEnvironment();
2197

22-
const [submittable, setSubmittable] = useState(false);
23-
const values = Form.useWatch('name', form);
24-
25-
useEffect(() => {
26-
form.validateFields({ validateOnly: true }).then(
27-
() => {
28-
setSubmittable(true);
29-
},
30-
() => {
31-
setSubmittable(false);
32-
},
33-
);
34-
}, [form, values]);
98+
const [errors, parseInput] = useParseZodErrors(InputSchema);
3599

36100
const role = toCaslResource('Role', _role);
37101

38102
async function submitChanges(values: Record<string, any>) {
39-
if (typeof values.expirationDayJs === 'object') {
103+
if (typeof values?.expirationDayJs === 'object') {
40104
values.expiration = (values.expirationDayJs as dayjs.Dayjs).toISOString();
41105
delete values.expirationDayJs;
42106
}
@@ -60,19 +124,18 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => {
60124
</>
61125
)}
62126

63-
<Form.Item
64-
label="Name"
65-
name="name"
66-
rules={[{ required: true, message: 'this field is required' }]}
67-
required
68-
>
127+
<Form.Item label="Name" name="name" {...antDesignInputProps(errors, 'name')} required>
69128
<Input
70129
placeholder="input placeholder"
71130
disabled={!ability.can('update', role, { field: 'name' })}
72131
/>
73132
</Form.Item>
74133

75-
<Form.Item label="Description" name="description">
134+
<Form.Item
135+
label="Description"
136+
name="description"
137+
{...antDesignInputProps(errors, 'description')}
138+
>
76139
<Input.TextArea
77140
placeholder="input placeholder"
78141
disabled={!ability.can('update', role, { field: 'description' })}
@@ -89,10 +152,12 @@ const RoleGeneralData: FC<{ role: Role }> = ({ role: _role }) => {
89152
/>
90153
</Form.Item>
91154

155+
<Form.Item label="Folder" name="parentId">
156+
<FolderInput defaultFolder={roleParentFolder} />
157+
</Form.Item>
158+
92159
<Form.Item>
93-
<Button type="primary" htmlType="submit" disabled={!submittable}>
94-
Update Role
95-
</Button>
160+
<FormSubmitButton submitText="Update Role" isValidData={(values) => !!parseInput(values)} />
96161
</Form.Item>
97162
</Form>
98163
);

0 commit comments

Comments
 (0)