added posibility to add or exclude private presentations for Public role users
This commit is contained in:
parent
222d08fbca
commit
788342808f
@ -173,6 +173,20 @@ module.exports = class UsersDBApi {
|
||||
|
||||
const users = await db.users.findByPk(id, { transaction });
|
||||
|
||||
if (data?.app_role && typeof data.app_role === 'object') {
|
||||
data.app_role = data.app_role.id || data.app_role.value || null;
|
||||
}
|
||||
|
||||
if (Array.isArray(data?.custom_permissions)) {
|
||||
data.custom_permissions = data.custom_permissions
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') return item;
|
||||
if (item && typeof item === 'object') return item.id || item.value;
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (!data?.app_role) {
|
||||
data.app_role = users?.app_role?.id;
|
||||
}
|
||||
@ -329,6 +343,37 @@ module.exports = class UsersDBApi {
|
||||
output.app_role_permissions = output.app_role.permissions || [];
|
||||
}
|
||||
|
||||
const productionPresentationAccess =
|
||||
await db.production_presentation_access.findAll({
|
||||
where: { userId: output.id },
|
||||
include: [
|
||||
{
|
||||
association: 'project',
|
||||
attributes: ['id', 'name', 'slug'],
|
||||
where: { production_presentation_visibility: 'private' },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
transaction,
|
||||
});
|
||||
|
||||
output.allowed_private_production_project_ids =
|
||||
productionPresentationAccess
|
||||
.map((row) => {
|
||||
const plain =
|
||||
typeof row.get === 'function' ? row.get({ plain: true }) : row;
|
||||
const project = plain.project;
|
||||
if (!project?.id) return null;
|
||||
|
||||
return {
|
||||
id: project.id,
|
||||
label: `${project.name} (${project.slug})`,
|
||||
name: project.name,
|
||||
slug: project.slug,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,13 @@ class UsersService extends BaseUsersService {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
static normalizeRoleId(value) {
|
||||
if (!value) return null;
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'object') return value.id || value.value || null;
|
||||
return null;
|
||||
}
|
||||
|
||||
static async createProductionPresentationAccessForPublicUser({
|
||||
user,
|
||||
data,
|
||||
@ -44,9 +51,10 @@ class UsersService extends BaseUsersService {
|
||||
data.allowed_private_production_project_ids,
|
||||
);
|
||||
|
||||
if (!selectedProjectIds.length || !data.app_role) return;
|
||||
const roleId = this.normalizeRoleId(data.app_role);
|
||||
if (!selectedProjectIds.length || !roleId) return;
|
||||
|
||||
const role = await db.roles.findByPk(data.app_role, { transaction });
|
||||
const role = await db.roles.findByPk(roleId, { transaction });
|
||||
if (role?.name !== 'Public') return;
|
||||
|
||||
const privateProjects = await db.projects.findAll({
|
||||
@ -76,6 +84,27 @@ class UsersService extends BaseUsersService {
|
||||
);
|
||||
}
|
||||
|
||||
static async updateProductionPresentationAccessForPublicUser({
|
||||
user,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
}) {
|
||||
if (!user?.id) return;
|
||||
|
||||
await db.production_presentation_access.destroy({
|
||||
where: { userId: user.id },
|
||||
transaction,
|
||||
});
|
||||
|
||||
await this.createProductionPresentationAccessForPublicUser({
|
||||
user,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user with email validation and optional invitation
|
||||
*/
|
||||
@ -135,6 +164,43 @@ class UsersService extends BaseUsersService {
|
||||
}
|
||||
}
|
||||
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const existingUser = await UsersDBApi.findBy({ id }, { transaction });
|
||||
|
||||
if (!existingUser) {
|
||||
throw new ValidationError('UsersNotFound');
|
||||
}
|
||||
|
||||
const user = await UsersDBApi.update(id, data, {
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
data,
|
||||
'allowed_private_production_project_ids',
|
||||
)
|
||||
) {
|
||||
await this.updateProductionPresentationAccessForPublicUser({
|
||||
user,
|
||||
data,
|
||||
currentUser,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return user;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user with self-deletion and permission checks
|
||||
*/
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useId, useState } from 'react';
|
||||
import React, { useEffect, useId, useRef, useState } from 'react';
|
||||
import { AsyncPaginate } from 'react-select-async-paginate';
|
||||
import axios from 'axios';
|
||||
|
||||
const areStringArraysEqual = (left = [], right = []) =>
|
||||
left.length === right.length && left.every((value, index) => value === right[index]);
|
||||
left.length === right.length &&
|
||||
left.every((value, index) => value === right[index]);
|
||||
|
||||
const areSelectOptionsEqual = (left = [], right = []) =>
|
||||
left.length === right.length &&
|
||||
@ -21,6 +22,7 @@ export const SelectFieldMany = ({
|
||||
showField,
|
||||
}) => {
|
||||
const [value, setValue] = useState([]);
|
||||
const appliedOptionsSignatureRef = useRef<string | null>(null);
|
||||
const PAGE_SIZE = 100;
|
||||
|
||||
useEffect(() => {
|
||||
@ -47,16 +49,26 @@ export const SelectFieldMany = ({
|
||||
label: el[showField],
|
||||
}));
|
||||
const selectedIds = options.map((el) => el.id);
|
||||
|
||||
setValue((currentValue) =>
|
||||
areSelectOptionsEqual(currentValue, selectedOptions)
|
||||
? currentValue
|
||||
: selectedOptions,
|
||||
const optionsSignature = selectedIds.join('|');
|
||||
const shouldApplyInitialOptions =
|
||||
appliedOptionsSignatureRef.current !== optionsSignature;
|
||||
const fieldValueMatchesInitialOptions = areStringArraysEqual(
|
||||
field.value,
|
||||
selectedIds,
|
||||
);
|
||||
|
||||
if (!areStringArraysEqual(field.value, selectedIds)) {
|
||||
if (shouldApplyInitialOptions) {
|
||||
appliedOptionsSignatureRef.current = optionsSignature;
|
||||
form.setFieldValue(field.name, selectedIds, false);
|
||||
}
|
||||
|
||||
if (shouldApplyInitialOptions || fieldValueMatchesInitialOptions) {
|
||||
setValue((currentValue) =>
|
||||
areSelectOptionsEqual(currentValue, selectedOptions)
|
||||
? currentValue
|
||||
: selectedOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [field.name, field.value, form, options, showField]);
|
||||
|
||||
@ -65,11 +77,12 @@ export const SelectFieldMany = ({
|
||||
label: data.label,
|
||||
});
|
||||
|
||||
const handleChange = (data: Array<{ value: string; label: string }>) => {
|
||||
setValue(data);
|
||||
const handleChange = (data: Array<{ value: string; label: string }> | null) => {
|
||||
const selectedOptions = data || [];
|
||||
setValue(selectedOptions);
|
||||
form.setFieldValue(
|
||||
field.name,
|
||||
data.map((el) => el?.value || null),
|
||||
selectedOptions.map((el) => el?.value || null),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -32,6 +32,10 @@ const initVals = {
|
||||
avatar: [] as Array<{ id: string; publicUrl: string }>,
|
||||
app_role: null as { id: string; name: string } | null,
|
||||
custom_permissions: [] as Array<{ id: string; name: string }>,
|
||||
allowed_private_production_project_ids: [] as Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
}>,
|
||||
password: '',
|
||||
};
|
||||
|
||||
@ -44,10 +48,20 @@ const EditUsersPage = () => {
|
||||
fetchAction: fetch,
|
||||
initialValues: initVals,
|
||||
});
|
||||
const [selectedRoleLabel, setSelectedRoleLabel] = React.useState('');
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedRoleLabel(initialValues.app_role?.name || '');
|
||||
}, [initialValues.app_role?.name]);
|
||||
|
||||
const handleSubmit = async (data: typeof initVals) => {
|
||||
if (id) {
|
||||
await dispatch(update({ id, data }));
|
||||
const payload = { ...data };
|
||||
if (selectedRoleLabel !== 'Public') {
|
||||
payload.allowed_private_production_project_ids = [];
|
||||
}
|
||||
|
||||
await dispatch(update({ id, data: payload }));
|
||||
await router.push('/users/users-list');
|
||||
}
|
||||
};
|
||||
@ -71,85 +85,119 @@ const EditUsersPage = () => {
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
<FormField label='First Name'>
|
||||
<Field name='firstName' placeholder='First Name' />
|
||||
</FormField>
|
||||
{({ setFieldValue }) => (
|
||||
<Form>
|
||||
<FormField label='First Name'>
|
||||
<Field name='firstName' placeholder='First Name' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Last Name'>
|
||||
<Field name='lastName' placeholder='Last Name' />
|
||||
</FormField>
|
||||
<FormField label='Last Name'>
|
||||
<Field name='lastName' placeholder='Last Name' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Phone Number'>
|
||||
<Field name='phoneNumber' placeholder='Phone Number' />
|
||||
</FormField>
|
||||
<FormField label='Phone Number'>
|
||||
<Field name='phoneNumber' placeholder='Phone Number' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='E-Mail'>
|
||||
<Field name='email' placeholder='E-Mail' />
|
||||
</FormField>
|
||||
<FormField label='E-Mail'>
|
||||
<Field name='email' placeholder='E-Mail' />
|
||||
</FormField>
|
||||
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field name='disabled' id='disabled' component={SwitchField} />
|
||||
</FormField>
|
||||
<FormField label='Disabled' labelFor='disabled'>
|
||||
<Field
|
||||
name='disabled'
|
||||
id='disabled'
|
||||
component={SwitchField}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Field
|
||||
label='Avatar'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Field
|
||||
label='Avatar'
|
||||
color='info'
|
||||
icon={mdiUpload}
|
||||
path={'users/avatar'}
|
||||
name='avatar'
|
||||
id='avatar'
|
||||
schema={{
|
||||
size: undefined,
|
||||
formats: undefined,
|
||||
}}
|
||||
component={FormImagePicker}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label='App Role' labelFor='app_role'>
|
||||
<Field
|
||||
name='app_role'
|
||||
id='app_role'
|
||||
component={SelectField}
|
||||
options={initialValues.app_role}
|
||||
itemRef={'roles'}
|
||||
showField={'name'}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label='App Role' labelFor='app_role'>
|
||||
<Field
|
||||
name='app_role'
|
||||
id='app_role'
|
||||
component={SelectField}
|
||||
options={initialValues.app_role}
|
||||
itemRef={'roles'}
|
||||
showField={'name'}
|
||||
onOptionChange={(option) => {
|
||||
const label = option?.label || '';
|
||||
setSelectedRoleLabel(label);
|
||||
if (label !== 'Public') {
|
||||
setFieldValue(
|
||||
'allowed_private_production_project_ids',
|
||||
[],
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
label='Custom Permissions'
|
||||
labelFor='custom_permissions'
|
||||
>
|
||||
<Field
|
||||
name='custom_permissions'
|
||||
id='custom_permissions'
|
||||
component={SelectFieldMany}
|
||||
options={initialValues.custom_permissions}
|
||||
itemRef={'permissions'}
|
||||
showField={'name'}
|
||||
/>
|
||||
</FormField>
|
||||
{selectedRoleLabel === 'Public' && (
|
||||
<FormField
|
||||
label='Allowed Private Production Presentations'
|
||||
labelFor='allowed_private_production_project_ids'
|
||||
>
|
||||
<Field
|
||||
name='allowed_private_production_project_ids'
|
||||
id='allowed_private_production_project_ids'
|
||||
component={SelectFieldMany}
|
||||
options={
|
||||
initialValues.allowed_private_production_project_ids
|
||||
}
|
||||
itemRef='runtime-access/private-production-presentations'
|
||||
showField='label'
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label='Password'>
|
||||
<Field name='password' placeholder='password' />
|
||||
</FormField>
|
||||
<FormField
|
||||
label='Custom Permissions'
|
||||
labelFor='custom_permissions'
|
||||
>
|
||||
<Field
|
||||
name='custom_permissions'
|
||||
id='custom_permissions'
|
||||
component={SelectFieldMany}
|
||||
options={initialValues.custom_permissions}
|
||||
itemRef={'permissions'}
|
||||
showField={'name'}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/users/users-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
<FormField label='Password'>
|
||||
<Field name='password' placeholder='password' />
|
||||
</FormField>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type='submit' color='info' label='Submit' />
|
||||
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||
<BaseButton
|
||||
type='reset'
|
||||
color='danger'
|
||||
outline
|
||||
label='Cancel'
|
||||
onClick={() => router.push('/users/users-list')}
|
||||
/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
|
||||
@ -25,7 +25,9 @@ export interface User extends BaseEntity {
|
||||
app_role?: Role | null;
|
||||
custom_permissions?: PermissionEntity[];
|
||||
allowedPrivateProductionSlugs?: string[];
|
||||
allowed_private_production_project_ids?: string[];
|
||||
allowed_private_production_project_ids?: Array<
|
||||
string | { id: string; label?: string; name?: string; slug?: string }
|
||||
>;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user