Skip to content

Commit

Permalink
Merge pull request #33 from socialappslab/feat/25/edit-role-permissions
Browse files Browse the repository at this point in the history
(feat): Edit role permissions
  • Loading branch information
zant authored Aug 12, 2024
2 parents e123067 + 3171994 commit 1c25c76
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 74 deletions.
1 change: 0 additions & 1 deletion src/components/dialog/ChangeUserRoleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export function ChangeUserRoleDialog({ open, handleClose, updateTable, user }: C
const { enqueueSnackbar } = useSnackbar();
const [roleOptions, setRoleOptions] = useState<FormSelectOption[]>([]);

console.log('user', user);
const [{ data: rolesData, loading: loadingRoles }] = useAxios<ExistingDocumentObject, unknown, ErrorResponse>({
url: '/roles?page[number]=1&page[size]=100&sort=name',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Button } from '../../themed/button/Button';
import { FormInput } from '../../themed/form-input/FormInput';
import { Title } from '../../themed/title/Title';
import { extractAxiosErrorData } from '../../util';
import { BaseObject } from '@/schemas';
import { City } from '@/schemas';

export interface EditUserProps {
user: IUser;
Expand All @@ -30,9 +30,8 @@ export function CreateCityDialog({ handleClose, updateTable }: CreateCityDialogP
const { state } = useStateContext();
const user = state.user as IUser;
const { t } = useTranslation(['register', 'errorCodes', 'admin']);
// Add casting to remove typing errors
const { createMutation: createCityMutation } = useCreateMutation<CreateCity>(
`admin/countries/${(user.country as BaseObject).id}/states/${user.state.id}/cities/`,
const { createMutation: createCityMutation } = useCreateMutation<CreateCity, City>(
`admin/countries/1/states/${user.state.id}/cities/`,
);

const { enqueueSnackbar } = useSnackbar();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,55 @@ import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useSnackbar } from 'notistack';

import useAxios from 'axios-hooks';
import { useEffect, useState } from 'react';
import useCreateMutation from '@/hooks/useCreateMutation';
import { FormSelectOption } from '@/schemas';
import { CreateRole, CreateRoleInputType, createRoleSchema } from '@/schemas/create';
import { Permission, Role } from '@/schemas/entities';
import FormMultipleSelect from '@/themed/form-multiple-select/FormMultipleSelect';
import { IUser } from '../../schemas/auth';
import { Button } from '../../themed/button/Button';
import { FormInput } from '../../themed/form-input/FormInput';
import FormSelect from '../../themed/form-select/FormSelect';
import { Title } from '../../themed/title/Title';
import { extractAxiosErrorData } from '../../util';

export interface EditUserProps {
user: IUser;
}

// Based on DB and https://docs.google.com/spreadsheets/d/1SKZ-qW-5fvgyS1INllLZhXu1emn1__KCKiNZirsJpjo/edit?gid=0#gid=0
const PERMISSIONS = {
organization: [
{ name: 'index', id: 8 },
{ name: 'create', id: 11 },
{ name: 'update', id: 13 },
{ name: 'destroy', id: 14 },
],
users: [
{ name: 'edit', id: 19 },
{ name: 'update', id: 20 },
{ name: 'destroy', id: 21 },
],
roles: [
{ name: 'show', id: 23 },
{ name: 'edit', id: 26 },
{ name: 'create', id: 25 },
{ name: 'destroy', id: 28 },
],
teams: [
{ name: 'index', id: 1 },
{ name: 'create', id: 4 },
{ name: 'destroy', id: 7 },
{ name: 'update', id: 6 },
],
} as const;

interface CreateRoleDialogProps {
handleClose: () => void;
updateTable: () => void;
}

export function CreateRoleDialog({ handleClose, updateTable }: CreateRoleDialogProps) {
const { t } = useTranslation(['register', 'errorCodes', 'permissions', 'admin']);
const { createMutation: createRoleMutation } = useCreateMutation<CreateRole>('roles');
const { createMutation: createRoleMutation } = useCreateMutation<CreateRole, Role>('roles');
const [permissionsOptions, setPermissionsOptions] = useState([]);

const { enqueueSnackbar } = useSnackbar();

const [{ data, loading, error }, refetch] = useAxios({
url: `/permissions`,
});

const normalizePermissions = (rows: Permission[]) => {
if (!rows) return [];
return rows.map((row: Permission) => {
const attr = row.attributes;
return { label: `${attr.resource}.${attr.name}`, value: attr.id };
}, []);
};

useEffect(() => {
if (!loading) {
const normalizedPermissions = normalizePermissions(data.data);
setPermissionsOptions(normalizedPermissions);
}
}, [data, loading]);

const methods = useForm<CreateRoleInputType>({
resolver: zodResolver(createRoleSchema()),
defaultValues: {},
});

Expand All @@ -71,34 +66,15 @@ export function CreateRoleDialog({ handleClose, updateTable }: CreateRoleDialogP
// formState: { isValid, errors },
} = methods;

const permissionOptions = Object.keys(PERMISSIONS).flatMap((resource) => {
type RoleKey = keyof typeof PERMISSIONS;
// type RoleItem = (typeof PERMISSIONS)[RoleKey][number];
return [
{
label: t(`permissions:${resource as RoleKey}.title`),
value: '',
disabled: true,
},
...PERMISSIONS[resource as RoleKey].map((permission) => {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
label: t(`permissions:${resource as RoleKey}.${permission.name}`),
value: permission.id,
};
}),
];
});

const onSubmitHandler: SubmitHandler<CreateRoleInputType> = async (values) => {
try {
const { name, permissionIds } = values;
const { name, permissions } = values;

const payload: CreateRole = {
name,
permissionIds,
permissionIds: permissions.map((permission) => parseInt(permission.value, 10)),
};

await createRoleMutation(payload);
enqueueSnackbar(t('edit.success'), {
variant: 'success',
Expand Down Expand Up @@ -158,12 +134,13 @@ export function CreateRoleDialog({ handleClose, updateTable }: CreateRoleDialogP
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormSelect
multiple
name="permissionIds"
className="mt-2"
label={t('admin:roles.form.permissions')}
options={permissionOptions as FormSelectOption[]}
<FormMultipleSelect
name="permissions"
loading={loading}
label={t('roles')}
placeholder={t('edit.roles_placeholder')}
options={permissionsOptions}
renderOption={(option: FormSelectOption) => t(`permissions:${option.label}`)}
/>
</Grid>
</Grid>
Expand Down
173 changes: 173 additions & 0 deletions src/components/dialog/EditRoleDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Box, Grid } from '@mui/material';

import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import { useSnackbar } from 'notistack';

import useAxios from 'axios-hooks';
import { useEffect, useState } from 'react';
import { FormSelectOption } from '@/schemas';
import { CreateRole, CreateRoleInputType } from '@/schemas/create';
import { Permission, Role } from '@/schemas/entities';
import FormMultipleSelect from '@/themed/form-multiple-select/FormMultipleSelect';
import { IUser } from '../../schemas/auth';
import { Button } from '../../themed/button/Button';
import { FormInput } from '../../themed/form-input/FormInput';
import { Title } from '../../themed/title/Title';
import { extractAxiosErrorData } from '../../util';
import useUpdateMutation from '@/hooks/useUpdateMutation';

export interface EditUserProps {
user: IUser;
}

interface CreateRoleDialogProps {
role: Role | null;
handleClose: () => void;
updateTable: () => void;
}

export function EditRoleDialog({ role, handleClose, updateTable }: CreateRoleDialogProps) {
const { t } = useTranslation(['register', 'errorCodes', 'permissions', 'admin']);
const { udpateMutation: updateRoleMutation } = useUpdateMutation<CreateRole, Role>(`roles/${role?.id}`);

const [permissionsOptions, setPermissionsOptions] = useState<FormSelectOption[]>([]);

const { enqueueSnackbar } = useSnackbar();

const [{ data, loading, fetchError }, refetch] = useAxios({
url: `/permissions`,
});

const normalizePermissions = (rows: Permission[]): FormSelectOption[] => {
if (!rows) return [];
return rows.map((row: Permission) => {
const attr = row.attributes;
return { label: `${attr.resource}.${attr.name}`, value: String(attr.id) };
});
};

useEffect(() => {
if (!loading) {
const normalizedPermissions = normalizePermissions(data.data);
setPermissionsOptions(normalizedPermissions);
}
}, [data, loading, fetchError]);

const methods = useForm<CreateRoleInputType>({
defaultValues: {
name: role?.name,
permissionIds: role?.permissions.data.map((permission) => ({
value: String(permission.attributes.id),
label: `${permission.attributes.resource}.${permission.attributes.name}`,
})),
},
});

const {
handleSubmit,
setError,
// setValue,
watch,
// formState: { isValid, errors },
} = methods;

const renderOption = (option: FormSelectOption): string => t(`permissions:${option.label}`);

const onSubmitHandler: SubmitHandler<CreateRoleInputType> = async (values) => {
try {
const { name, permissionIds } = values;

const payload: CreateRole = {
role: {
name,
permissionIds: permissionIds.map((permission: FormSelectOption) => parseInt(permission.value, 10)),
},
};
await updateRoleMutation(payload);
enqueueSnackbar(t('edit.success'), {
variant: 'success',
});
updateTable();
handleClose();
} catch (error) {
const errorData = extractAxiosErrorData(error);

// eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/no-explicit-any
errorData?.errors?.forEach((error: any) => {
if (error?.field && watch(error.field)) {
setError(error.field, {
type: 'manual',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
message: t(`errorCodes:${String(error?.error_code)}` || 'errorCodes:genericField', {
field: watch(error.field),
}),
});
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
enqueueSnackbar(t(`errorCodes:${error?.error_code || 'generic'}`), {
variant: 'error',
});
}
});

if (!errorData?.errors || errorData?.errors.length === 0) {
enqueueSnackbar(t('errorCodes:generic'), {
variant: 'error',
});
}
}
};

return (
<div className="flex flex-col py-6 px-4">
<FormProvider {...methods}>
<Box
component="form"
onSubmit={handleSubmit(onSubmitHandler)}
noValidate
autoComplete="off"
className="w-full p-8"
>
<Title type="section" className="self-center mb-8i w-full" label={t('admin:roles.create_role')} />
<Grid container spacing={2}>
<Grid item xs={12} sm={12}>
<FormInput
className="mt-2"
name="name"
label={t('admin:roles.form.name')}
type="text"
placeholder={t('admin:roles.form.name_placeholder')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormMultipleSelect
name="permissionIds"
loading={loading}
label={t('roles')}
placeholder={t('edit.roles_placeholder')}
options={permissionsOptions}
renderOption={renderOption}
/>
</Grid>
</Grid>

<div className="mt-8 grid grid-cols-1 gap-4 md:flex md:justify-end md:gap-0">
<div className="md:mr-2">
<Button buttonType="large" label={t('edit.action')} disabled={false} type="submit" />
</div>

<div>
<Button buttonType="large" primary={false} disabled={false} label={t('back')} onClick={handleClose} />
</div>
</div>
</Box>
</FormProvider>
</div>
);
}

export default EditRoleDialog;
2 changes: 1 addition & 1 deletion src/components/list/CityList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import Button from '@/themed/button/Button';
import { BaseObject, City } from '@/schemas';
import CreateCityDialog from '@/pages/admin/CreateCityDialog';
import CreateCityDialog from '@/components/dialog/CreateCityDialog';
import { HeadCell } from '../../themed/table/DataTable';
import FilteredDataTable from './FilteredDataTable';
import useStateContext from '@/hooks/useStateContext';
Expand Down
Loading

0 comments on commit 1c25c76

Please sign in to comment.