first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
import {useForm} from 'react-hook-form';
import {CrupdateResourceLayout} from '../../crupdate-resource-layout';
import {Trans} from '../../../i18n/trans';
import {CrupdateRolePageSettingsPanel} from './crupdate-role-settings-panel';
import {CreateRolePayload, useCreateRole} from '../requests/user-create-role';
import {useNavigate} from '../../../utils/hooks/use-navigate';
export function CreateRolePage() {
const form = useForm<CreateRolePayload>({defaultValues: {type: 'sitewide'}});
const createRole = useCreateRole(form);
const navigate = useNavigate();
return (
<CrupdateResourceLayout
form={form}
onSubmit={values => {
createRole.mutate(values, {
onSuccess: response => {
navigate(`/admin/roles/${response.role.id}/edit`);
},
});
}}
title={<Trans message="Add new role" />}
isLoading={createRole.isPending}
>
<CrupdateRolePageSettingsPanel />
</CrupdateResourceLayout>
);
}

View File

@@ -0,0 +1,102 @@
import {Role} from '../../../auth/role';
import {useTrans} from '../../../i18n/use-trans';
import {useFormContext} from 'react-hook-form';
import {FormTextField} from '../../../ui/forms/input-field/text-field/text-field';
import {Trans} from '../../../i18n/trans';
import {message} from '../../../i18n/message';
import {FormSelect} from '../../../ui/forms/select/select';
import {Item} from '../../../ui/forms/listbox/item';
import {FormSwitch} from '../../../ui/forms/toggle/switch';
import {FormPermissionSelector} from '../../../auth/ui/permission-selector';
import {useSettings} from '../../../core/settings/use-settings';
import {Button} from '@common/ui/buttons/button';
interface CrupdateRolePageSettingsPanelProps {
isInternal?: boolean;
}
export function CrupdateRolePageSettingsPanel({
isInternal = false,
}: CrupdateRolePageSettingsPanelProps) {
const {trans} = useTrans();
const {workspaces} = useSettings();
const {watch, setValue} = useFormContext<Role>();
const watchedType = watch('type');
return (
<>
<FormTextField
label={<Trans message="Name" />}
name="name"
className="mb-20"
required
/>
<FormTextField
label={<Trans message="Description" />}
name="description"
inputElementType="textarea"
placeholder={trans(message('Role description...'))}
rows={4}
className="mb-20"
/>
{workspaces.integrated && (
<FormSelect
label={<Trans message="Type" />}
name="type"
selectionMode="single"
className="mb-20"
description={
<Trans message="Whether this role will be assigned to users globally on the site or only within workspaces." />
}
>
<Item value="sitewide">
<Trans message="Sitewide" />
</Item>
<Item value="workspace">
<Trans message="Workspace" />
</Item>
</FormSelect>
)}
{!isInternal && (
<>
<FormSwitch
name="default"
className="mb-20"
description={
<Trans message="Assign this role to new users automatically." />
}
>
<Trans message="Default" />
</FormSwitch>
{watchedType === 'sitewide' && (
<FormSwitch
name="guests"
description={
<Trans message="Assign this role to guests (not logged in users)." />
}
>
<Trans message="Guests" />
</FormSwitch>
)}
</>
)}
<div className="mb-14 mt-30 flex items-end justify-between gap-12">
<h2 className="text-lg leading-tight">
<Trans message="Permissions" />
</h2>
<Button
variant="outline"
size="xs"
onClick={() => setValue('permissions', [])}
>
<Trans message="Remove all" />
</Button>
</div>
<FormPermissionSelector
name="permissions"
valueListKey={
watchedType === 'sitewide' ? 'permissions' : 'workspacePermissions'
}
/>
</>
);
}

View File

@@ -0,0 +1,173 @@
import {Role} from '../../../auth/role';
import {ColumnConfig} from '../../../datatable/column-config';
import {User} from '../../../auth/user';
import {Trans} from '../../../i18n/trans';
import {NameWithAvatar} from '../../../datatable/column-templates/name-with-avatar';
import {FormattedDate} from '../../../i18n/formatted-date';
import React from 'react';
import teamSvg from '../team.svg';
import {DialogTrigger} from '../../../ui/overlays/dialog/dialog-trigger';
import {Button} from '../../../ui/buttons/button';
import {SelectUserDialog} from '../../../users/select-user-dialog';
import {queryClient} from '../../../http/query-client';
import {DatatableDataQueryKey} from '../../../datatable/requests/paginated-resources';
import {DataTableEmptyStateMessage} from '../../../datatable/page/data-table-emty-state-message';
import {useDataTable} from '../../../datatable/page/data-table-context';
import {ConfirmationDialog} from '../../../ui/overlays/dialog/confirmation-dialog';
import {useRemoveUsersFromRole} from '../requests/use-remove-users-from-role';
import {useAddUsersToRole} from '../requests/use-add-users-to-role';
import {DataTable} from '../../../datatable/data-table';
import {useIsMobileMediaQuery} from '../../../utils/hooks/is-mobile-media-query';
const userColumn: ColumnConfig<User> = {
key: 'name',
allowsSorting: true,
sortingKey: 'email',
header: () => <Trans message="User" />,
body: user => (
<NameWithAvatar
image={user.avatar}
label={user.display_name}
description={user.email}
/>
),
width: 'col-w-3',
};
const desktopColumns: ColumnConfig<User>[] = [
userColumn,
{
key: 'first_name',
allowsSorting: true,
header: () => <Trans message="First name" />,
body: user => user.first_name,
},
{
key: 'last_name',
allowsSorting: true,
header: () => <Trans message="Last name" />,
body: user => user.last_name,
},
{
key: 'created_at',
allowsSorting: true,
header: () => <Trans message="Assigned at" />,
body: user => <FormattedDate date={user.created_at} />,
},
];
const mobileColumns: ColumnConfig<User>[] = [userColumn];
interface CrupdateRolePageUsersPanelProps {
role: Role;
}
export function EditRolePageUsersPanel({
role,
}: CrupdateRolePageUsersPanelProps) {
const isMobile = useIsMobileMediaQuery();
if (role.guests || role.type === 'workspace') {
return (
<div className="pt-30 pb-10">
<DataTableEmptyStateMessage
image={teamSvg}
title={<Trans message="Users can't be assigned to this role" />}
/>
</div>
);
}
return (
<DataTable
endpoint="users"
columns={isMobile ? mobileColumns : desktopColumns}
queryParams={{roleId: `${role.id}`}}
actions={<AssignUserAction role={role} />}
selectedActions={<RemoveUsersAction role={role} />}
emptyStateMessage={
<DataTableEmptyStateMessage
image={teamSvg}
title={
<Trans message="No users have been assigned to this role yet" />
}
filteringTitle={<Trans message="No matching users" />}
/>
}
/>
);
}
interface AssignUserActionProps {
role: Role;
}
function AssignUserAction({role}: AssignUserActionProps) {
const addUsers = useAddUsersToRole(role);
return (
<DialogTrigger type="modal">
<Button variant="flat" color="primary" disabled={addUsers.isPending}>
<Trans message="Assign user" />
</Button>
<SelectUserDialog
onUserSelected={user => {
addUsers.mutate(
{userIds: [user.id as number]},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('users', {
roleId: `${role.id}`,
}),
});
},
},
);
}}
/>
</DialogTrigger>
);
}
type RemoveUsersActionProps = {
role: Role;
};
export function RemoveUsersAction({role}: RemoveUsersActionProps) {
const removeUsers = useRemoveUsersFromRole(role);
const {selectedRows} = useDataTable();
return (
<DialogTrigger
type="modal"
onClose={isConfirmed => {
if (isConfirmed) {
removeUsers.mutate(
{userIds: selectedRows as number[]},
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: DatatableDataQueryKey('users', {
roleId: `${role.id}`,
}),
});
},
},
);
}
}}
>
<Button variant="flat" color="danger" disabled={removeUsers.isPending}>
<Trans message="Remove users" />
</Button>
<ConfirmationDialog
title={
<Trans
message="Remove [one 1 user|other :count users] from “:name“ role?"
values={{count: selectedRows.length, name: role.name}}
/>
}
body={<Trans message="This will permanently remove the users." />}
confirm={<Trans message="Remove" />}
isDanger
/>
</DialogTrigger>
);
}

View File

@@ -0,0 +1,61 @@
import {useRole} from '../requests/use-role';
import {FullPageLoader} from '../../../ui/progress/full-page-loader';
import {Role} from '../../../auth/role';
import {Trans} from '../../../i18n/trans';
import {useForm} from 'react-hook-form';
import {Tabs} from '../../../ui/tabs/tabs';
import {Tab} from '../../../ui/tabs/tab';
import {TabList} from '../../../ui/tabs/tab-list';
import {TabPanel, TabPanels} from '../../../ui/tabs/tab-panels';
import {useUpdateRole} from '../requests/use-update-role';
import {CrupdateResourceLayout} from '../../crupdate-resource-layout';
import {CrupdateRolePageSettingsPanel} from './crupdate-role-settings-panel';
import {EditRolePageUsersPanel} from './edit-role-page-users-panel';
export function EditRolePage() {
const query = useRole();
if (query.status !== 'success') {
return <FullPageLoader />;
}
return <PageContent role={query.data.role} />;
}
interface PageContentProps {
role: Role;
}
function PageContent({role}: PageContentProps) {
const form = useForm<Role>({defaultValues: role});
const updateRole = useUpdateRole();
return (
<CrupdateResourceLayout
form={form}
onSubmit={values => {
updateRole.mutate(values);
}}
title={<Trans message="Edit “:name“ role" values={{name: role.name}} />}
isLoading={updateRole.isPending}
>
<Tabs isLazy>
<TabList>
<Tab>
<Trans message="Settings" />
</Tab>
<Tab>
<Trans message="Users" />
</Tab>
</TabList>
<TabPanels className="pt-20">
<TabPanel>
<CrupdateRolePageSettingsPanel isInternal={role.internal} />
</TabPanel>
<TabPanel>
<EditRolePageUsersPanel role={role} />
</TabPanel>
</TabPanels>
</Tabs>
</CrupdateResourceLayout>
);
}