added posibility to add or exclude private presentations for Public role users

This commit is contained in:
Dmitri 2026-06-26 12:41:39 +02:00
parent 222d08fbca
commit 788342808f
5 changed files with 259 additions and 85 deletions

View File

@ -173,6 +173,20 @@ module.exports = class UsersDBApi {
const users = await db.users.findByPk(id, { transaction }); 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) { if (!data?.app_role) {
data.app_role = users?.app_role?.id; data.app_role = users?.app_role?.id;
} }
@ -329,6 +343,37 @@ module.exports = class UsersDBApi {
output.app_role_permissions = output.app_role.permissions || []; 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; return output;
} }

View File

@ -27,6 +27,13 @@ class UsersService extends BaseUsersService {
.filter(Boolean); .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({ static async createProductionPresentationAccessForPublicUser({
user, user,
data, data,
@ -44,9 +51,10 @@ class UsersService extends BaseUsersService {
data.allowed_private_production_project_ids, 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; if (role?.name !== 'Public') return;
const privateProjects = await db.projects.findAll({ 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 * 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 * Remove user with self-deletion and permission checks
*/ */

View File

@ -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 { AsyncPaginate } from 'react-select-async-paginate';
import axios from 'axios'; import axios from 'axios';
const areStringArraysEqual = (left = [], right = []) => 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 = []) => const areSelectOptionsEqual = (left = [], right = []) =>
left.length === right.length && left.length === right.length &&
@ -21,6 +22,7 @@ export const SelectFieldMany = ({
showField, showField,
}) => { }) => {
const [value, setValue] = useState([]); const [value, setValue] = useState([]);
const appliedOptionsSignatureRef = useRef<string | null>(null);
const PAGE_SIZE = 100; const PAGE_SIZE = 100;
useEffect(() => { useEffect(() => {
@ -47,16 +49,26 @@ export const SelectFieldMany = ({
label: el[showField], label: el[showField],
})); }));
const selectedIds = options.map((el) => el.id); const selectedIds = options.map((el) => el.id);
const optionsSignature = selectedIds.join('|');
setValue((currentValue) => const shouldApplyInitialOptions =
areSelectOptionsEqual(currentValue, selectedOptions) appliedOptionsSignatureRef.current !== optionsSignature;
? currentValue const fieldValueMatchesInitialOptions = areStringArraysEqual(
: selectedOptions, field.value,
selectedIds,
); );
if (!areStringArraysEqual(field.value, selectedIds)) { if (shouldApplyInitialOptions) {
appliedOptionsSignatureRef.current = optionsSignature;
form.setFieldValue(field.name, selectedIds, false); form.setFieldValue(field.name, selectedIds, false);
} }
if (shouldApplyInitialOptions || fieldValueMatchesInitialOptions) {
setValue((currentValue) =>
areSelectOptionsEqual(currentValue, selectedOptions)
? currentValue
: selectedOptions,
);
}
} }
}, [field.name, field.value, form, options, showField]); }, [field.name, field.value, form, options, showField]);
@ -65,11 +77,12 @@ export const SelectFieldMany = ({
label: data.label, label: data.label,
}); });
const handleChange = (data: Array<{ value: string; label: string }>) => { const handleChange = (data: Array<{ value: string; label: string }> | null) => {
setValue(data); const selectedOptions = data || [];
setValue(selectedOptions);
form.setFieldValue( form.setFieldValue(
field.name, field.name,
data.map((el) => el?.value || null), selectedOptions.map((el) => el?.value || null),
); );
}; };

View File

@ -32,6 +32,10 @@ const initVals = {
avatar: [] as Array<{ id: string; publicUrl: string }>, avatar: [] as Array<{ id: string; publicUrl: string }>,
app_role: null as { id: string; name: string } | null, app_role: null as { id: string; name: string } | null,
custom_permissions: [] as Array<{ id: string; name: string }>, custom_permissions: [] as Array<{ id: string; name: string }>,
allowed_private_production_project_ids: [] as Array<{
id: string;
label: string;
}>,
password: '', password: '',
}; };
@ -44,10 +48,20 @@ const EditUsersPage = () => {
fetchAction: fetch, fetchAction: fetch,
initialValues: initVals, initialValues: initVals,
}); });
const [selectedRoleLabel, setSelectedRoleLabel] = React.useState('');
React.useEffect(() => {
setSelectedRoleLabel(initialValues.app_role?.name || '');
}, [initialValues.app_role?.name]);
const handleSubmit = async (data: typeof initVals) => { const handleSubmit = async (data: typeof initVals) => {
if (id) { 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'); await router.push('/users/users-list');
} }
}; };
@ -71,85 +85,119 @@ const EditUsersPage = () => {
initialValues={initialValues} initialValues={initialValues}
onSubmit={(values) => handleSubmit(values)} onSubmit={(values) => handleSubmit(values)}
> >
<Form> {({ setFieldValue }) => (
<FormField label='First Name'> <Form>
<Field name='firstName' placeholder='First Name' /> <FormField label='First Name'>
</FormField> <Field name='firstName' placeholder='First Name' />
</FormField>
<FormField label='Last Name'> <FormField label='Last Name'>
<Field name='lastName' placeholder='Last Name' /> <Field name='lastName' placeholder='Last Name' />
</FormField> </FormField>
<FormField label='Phone Number'> <FormField label='Phone Number'>
<Field name='phoneNumber' placeholder='Phone Number' /> <Field name='phoneNumber' placeholder='Phone Number' />
</FormField> </FormField>
<FormField label='E-Mail'> <FormField label='E-Mail'>
<Field name='email' placeholder='E-Mail' /> <Field name='email' placeholder='E-Mail' />
</FormField> </FormField>
<FormField label='Disabled' labelFor='disabled'> <FormField label='Disabled' labelFor='disabled'>
<Field name='disabled' id='disabled' component={SwitchField} /> <Field
</FormField> name='disabled'
id='disabled'
component={SwitchField}
/>
</FormField>
<FormField> <FormField>
<Field <Field
label='Avatar' label='Avatar'
color='info' color='info'
icon={mdiUpload} icon={mdiUpload}
path={'users/avatar'} path={'users/avatar'}
name='avatar' name='avatar'
id='avatar' id='avatar'
schema={{ schema={{
size: undefined, size: undefined,
formats: undefined, formats: undefined,
}} }}
component={FormImagePicker} component={FormImagePicker}
/> />
</FormField> </FormField>
<FormField label='App Role' labelFor='app_role'> <FormField label='App Role' labelFor='app_role'>
<Field <Field
name='app_role' name='app_role'
id='app_role' id='app_role'
component={SelectField} component={SelectField}
options={initialValues.app_role} options={initialValues.app_role}
itemRef={'roles'} itemRef={'roles'}
showField={'name'} showField={'name'}
/> onOptionChange={(option) => {
</FormField> const label = option?.label || '';
setSelectedRoleLabel(label);
if (label !== 'Public') {
setFieldValue(
'allowed_private_production_project_ids',
[],
);
}
}}
/>
</FormField>
<FormField {selectedRoleLabel === 'Public' && (
label='Custom Permissions' <FormField
labelFor='custom_permissions' label='Allowed Private Production Presentations'
> labelFor='allowed_private_production_project_ids'
<Field >
name='custom_permissions' <Field
id='custom_permissions' name='allowed_private_production_project_ids'
component={SelectFieldMany} id='allowed_private_production_project_ids'
options={initialValues.custom_permissions} component={SelectFieldMany}
itemRef={'permissions'} options={
showField={'name'} initialValues.allowed_private_production_project_ids
/> }
</FormField> itemRef='runtime-access/private-production-presentations'
showField='label'
/>
</FormField>
)}
<FormField label='Password'> <FormField
<Field name='password' placeholder='password' /> label='Custom Permissions'
</FormField> labelFor='custom_permissions'
>
<Field
name='custom_permissions'
id='custom_permissions'
component={SelectFieldMany}
options={initialValues.custom_permissions}
itemRef={'permissions'}
showField={'name'}
/>
</FormField>
<BaseDivider /> <FormField label='Password'>
<BaseButtons> <Field name='password' placeholder='password' />
<BaseButton type='submit' color='info' label='Submit' /> </FormField>
<BaseButton type='reset' color='info' outline label='Reset' />
<BaseButton <BaseDivider />
type='reset' <BaseButtons>
color='danger' <BaseButton type='submit' color='info' label='Submit' />
outline <BaseButton type='reset' color='info' outline label='Reset' />
label='Cancel' <BaseButton
onClick={() => router.push('/users/users-list')} type='reset'
/> color='danger'
</BaseButtons> outline
</Form> label='Cancel'
onClick={() => router.push('/users/users-list')}
/>
</BaseButtons>
</Form>
)}
</Formik> </Formik>
</CardBox> </CardBox>
</SectionMain> </SectionMain>

View File

@ -25,7 +25,9 @@ export interface User extends BaseEntity {
app_role?: Role | null; app_role?: Role | null;
custom_permissions?: PermissionEntity[]; custom_permissions?: PermissionEntity[];
allowedPrivateProductionSlugs?: string[]; 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; password?: string;
} }