mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Fix readonly and required on groups (#19962)
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
5
.changeset/fuzzy-rice-refuse.md
Normal file
5
.changeset/fuzzy-rice-refuse.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': patch
|
||||
---
|
||||
|
||||
Ensured the "Readonly" and "Required" options work on groups
|
||||
@@ -12,6 +12,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import FormField from './form-field.vue';
|
||||
import type { FormField as TFormField } from './types';
|
||||
import ValidationErrors from './validation-errors.vue';
|
||||
import { pushGroupOptionsDown } from '@/utils/push-group-options-down';
|
||||
|
||||
type FieldValues = {
|
||||
[field: string]: any;
|
||||
@@ -151,13 +152,23 @@ function useForm() {
|
||||
|
||||
const { formFields } = useFormFields(fields);
|
||||
|
||||
const fieldsMap: ComputedRef<Record<string, TFormField | undefined>> = computed(() => {
|
||||
const fieldsWithConditions = computed(() => {
|
||||
const valuesWithDefaults = Object.assign({}, defaultValues.value, values.value);
|
||||
return formFields.value.reduce((result: Record<string, Field>, field: Field) => {
|
||||
|
||||
let fields = formFields.value.reduce((result, field) => {
|
||||
const newField = applyConditions(valuesWithDefaults, setPrimaryKeyReadonly(field));
|
||||
if (newField.field) result[newField.field] = newField;
|
||||
|
||||
if (newField.field) result.push(newField);
|
||||
return result;
|
||||
}, {} as Record<string, Field>);
|
||||
}, [] as Field[]);
|
||||
|
||||
fields = pushGroupOptionsDown(fields);
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
const fieldsMap: ComputedRef<Record<string, TFormField | undefined>> = computed(() => {
|
||||
return Object.fromEntries(fieldsWithConditions.value.map((field) => [field.field, field]));
|
||||
});
|
||||
|
||||
const fieldsInGroup = computed(() =>
|
||||
@@ -171,17 +182,7 @@ function useForm() {
|
||||
});
|
||||
|
||||
const fieldsForGroup = computed(() => {
|
||||
const valuesWithDefaults = Object.assign({}, defaultValues.value, values.value);
|
||||
|
||||
return fieldNames.value.map((name: string) => {
|
||||
const fields = getFieldsForGroup(fieldsMap.value[name]?.meta?.field || null);
|
||||
|
||||
return fields.reduce((result: Field[], field: Field) => {
|
||||
const newField = applyConditions(valuesWithDefaults, setPrimaryKeyReadonly(field));
|
||||
if (newField.field) result.push(newField);
|
||||
return result;
|
||||
}, [] as Field[]);
|
||||
});
|
||||
return fieldNames.value.map((name: string) => getFieldsForGroup(fieldsMap.value[name]?.meta?.field || null));
|
||||
});
|
||||
|
||||
return { fieldNames, fieldsMap, isDisabled, getFieldsForGroup, fieldsForGroup };
|
||||
@@ -199,7 +200,7 @@ function useForm() {
|
||||
}
|
||||
|
||||
function getFieldsForGroup(group: null | string, passed: string[] = []): Field[] {
|
||||
const fieldsInGroup: Field[] = fields.value.filter((field) => {
|
||||
const fieldsInGroup = fieldsWithConditions.value.filter((field) => {
|
||||
const meta = fieldsMap.value?.[field.field]?.meta;
|
||||
return meta?.group === group || (group === null && isNil(meta));
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import { AxiosResponse } from 'axios';
|
||||
import { mergeWith } from 'lodash';
|
||||
import { ComputedRef, Ref, computed, isRef, ref, unref, watch } from 'vue';
|
||||
import { usePermissions } from './use-permissions';
|
||||
import { pushGroupOptionsDown } from '@/utils/push-group-options-down';
|
||||
|
||||
type UsableItem<T extends Record<string, any>> = {
|
||||
edits: Ref<Record<string, any>>;
|
||||
@@ -129,7 +130,9 @@ export function useItem<T extends Record<string, any>>(
|
||||
}
|
||||
);
|
||||
|
||||
const errors = validateItem(payloadToValidate, fieldsWithPermissions.value, isNew.value);
|
||||
const fields = pushGroupOptionsDown(fieldsWithPermissions.value);
|
||||
|
||||
const errors = validateItem(payloadToValidate, fields, isNew.value);
|
||||
|
||||
if (errors.length > 0) {
|
||||
validationErrors.value = errors;
|
||||
|
||||
@@ -199,7 +199,10 @@ async function onGroupSortChange(fields: Field[]) {
|
||||
<template #header>
|
||||
<div class="header full">
|
||||
<v-icon class="drag-handle" name="drag_indicator" @click.stop />
|
||||
<span class="name">{{ field.field }}</span>
|
||||
<span class="name">
|
||||
{{ field.field }}
|
||||
<v-icon v-if="field.meta?.required === true" name="star" class="required" sup filled />
|
||||
</span>
|
||||
<v-icon v-if="hidden" v-tooltip="t('hidden_field')" name="visibility_off" class="hidden-icon" small />
|
||||
<field-select-menu
|
||||
:field="field"
|
||||
|
||||
169
app/src/utils/push-group-options-down.test.ts
Normal file
169
app/src/utils/push-group-options-down.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
import { pushGroupOptionsDown } from './push-group-options-down.js';
|
||||
import { Field } from '@directus/types';
|
||||
|
||||
const fields: Field[] = [
|
||||
{
|
||||
field: 'group1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: true,
|
||||
readonly: true,
|
||||
special: ['group'],
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Group 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: false,
|
||||
group: 'group1',
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Field in group 1',
|
||||
},
|
||||
];
|
||||
|
||||
test('Test pushGroupOptionsDown not mutating', () => {
|
||||
expect(pushGroupOptionsDown(fields)).not.toBe(fields);
|
||||
});
|
||||
|
||||
test('Test pushGroupOptionsDown', () => {
|
||||
expect(pushGroupOptionsDown(fields)).toEqual([
|
||||
{
|
||||
field: 'group1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: false,
|
||||
readonly: false,
|
||||
special: ['group'],
|
||||
},
|
||||
schema: null,
|
||||
name: 'Group 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: true,
|
||||
readonly: true,
|
||||
group: 'group1',
|
||||
},
|
||||
schema: null,
|
||||
name: 'Field in group 1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const fieldsNested: Field[] = [
|
||||
{
|
||||
field: 'group1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: true,
|
||||
readonly: false,
|
||||
special: ['group'],
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Group 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: false,
|
||||
readonly: false,
|
||||
group: 'group1',
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Field in group 1',
|
||||
},
|
||||
{
|
||||
field: 'group1_1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
group: 'group1',
|
||||
required: false,
|
||||
readonly: true,
|
||||
special: ['group'],
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Group 1 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1_1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: false,
|
||||
readonly: false,
|
||||
group: 'group1_1',
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Field in group 1 1',
|
||||
},
|
||||
];
|
||||
|
||||
test('Test pushGroupOptionsDown with nested groups', () => {
|
||||
expect(pushGroupOptionsDown(fieldsNested)).toEqual([
|
||||
{
|
||||
field: 'group1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: false,
|
||||
readonly: false,
|
||||
special: ['group'],
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Group 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: true,
|
||||
readonly: false,
|
||||
group: 'group1',
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Field in group 1',
|
||||
},
|
||||
{
|
||||
field: 'group1_1',
|
||||
type: 'alias',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
group: 'group1',
|
||||
required: false,
|
||||
readonly: false,
|
||||
special: ['group'],
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Group 1 1',
|
||||
},
|
||||
{
|
||||
field: 'field_in_group1_1',
|
||||
type: 'boolean',
|
||||
collection: 'test',
|
||||
meta: {
|
||||
required: true,
|
||||
readonly: true,
|
||||
group: 'group1_1',
|
||||
} as any,
|
||||
schema: null,
|
||||
name: 'Field in group 1 1',
|
||||
},
|
||||
]);
|
||||
});
|
||||
42
app/src/utils/push-group-options-down.ts
Normal file
42
app/src/utils/push-group-options-down.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Field, FieldMeta } from '@directus/types';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export function pushGroupOptionsDown(fields: Field[]) {
|
||||
fields = cloneDeep(fields);
|
||||
|
||||
const updatedGroups: string[] = [];
|
||||
const fieldsQueue: Field[] = [...fields];
|
||||
|
||||
while (fieldsQueue.length > 0) {
|
||||
const field = fieldsQueue.shift();
|
||||
|
||||
if (!field) break;
|
||||
|
||||
const parent = field?.meta?.group;
|
||||
const isGroup = field.meta?.special?.includes('group');
|
||||
|
||||
if (!isGroup) continue;
|
||||
|
||||
if (parent && !updatedGroups.includes(parent)) {
|
||||
fieldsQueue.push(field);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const childField of fields) {
|
||||
if (childField.meta?.group !== field.field) continue;
|
||||
|
||||
childField.meta.required = field.meta?.required || childField.meta.required;
|
||||
childField.meta.readonly = field.meta?.readonly || childField.meta.readonly;
|
||||
}
|
||||
|
||||
if (!field.meta) {
|
||||
field.meta = {} as FieldMeta;
|
||||
}
|
||||
|
||||
field.meta.required = false;
|
||||
field.meta.readonly = false;
|
||||
updatedGroups.push(field.field);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
Reference in New Issue
Block a user