mirror of
https://github.com/directus/directus.git
synced 2026-01-24 21:27:59 -05:00
treat empty array as null on relational field (#15958)
* treat empty array as null on relational field * Run prettier * add unit test * Fix linter warnings Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -100,7 +100,7 @@ import { useInsightsStore } from '@/stores/insights';
|
||||
import { CreatePanel } from '@/stores/insights';
|
||||
import { Panel } from '@directus/shared/types';
|
||||
import { assign, clone, omitBy, isUndefined } from 'lodash';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, reactive, unref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -69,7 +69,7 @@ import { useFieldDetailStore, syncFieldDetailStoreProperty } from '../store/';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import ExtensionOptions from '../shared/extension-options.vue';
|
||||
import RelationshipConfiguration from './relationship-configuration.vue';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ExtensionOptions, RelationshipConfiguration },
|
||||
|
||||
@@ -74,7 +74,7 @@ import { FlowRaw } from '@directus/shared/types';
|
||||
import slugify from '@sindresorhus/slugify';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { customAlphabet } from 'nanoid/non-secure';
|
||||
|
||||
const generateSuffix = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 5);
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ import { useEditsGuard } from '@/composables/use-edits-guard';
|
||||
import { useShortcut } from '@/composables/use-shortcut';
|
||||
import { isEmpty, merge, omit, cloneDeep } from 'lodash';
|
||||
import { router } from '@/router';
|
||||
import { nanoid, customAlphabet } from 'nanoid';
|
||||
import { nanoid, customAlphabet } from 'nanoid/non-secure';
|
||||
|
||||
import SettingsNotFound from '../not-found.vue';
|
||||
import SettingsNavigation from '../../components/navigation.vue';
|
||||
|
||||
87
app/src/utils/validate-item.test.ts
Normal file
87
app/src/utils/validate-item.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { beforeEach, expect, test } from 'vitest';
|
||||
|
||||
import { validateItem } from '@/utils/validate-item';
|
||||
import { DeepPartial, Field } from '@directus/shared/types';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
const fields: DeepPartial<Field>[] = [
|
||||
{
|
||||
field: 'id',
|
||||
collection: 'users',
|
||||
type: 'integer',
|
||||
name: 'ID',
|
||||
meta: {
|
||||
required: true,
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
collection: 'users',
|
||||
type: 'string',
|
||||
name: 'Name',
|
||||
meta: {
|
||||
required: true,
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
field: 'email',
|
||||
collection: 'users',
|
||||
type: 'string',
|
||||
name: 'Email',
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
field: 'role',
|
||||
collection: 'users',
|
||||
type: 'integer',
|
||||
name: 'Role',
|
||||
meta: {
|
||||
required: true,
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
createSpy: () => (_collection, field) => {
|
||||
if (field === 'role') {
|
||||
return [{ some: 'relation' }];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Required fields', () => {
|
||||
let result = validateItem(
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
email: 'test@test.com',
|
||||
role: [1, 2],
|
||||
},
|
||||
fields as Field[],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result.length).toEqual(0);
|
||||
|
||||
result = validateItem(
|
||||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
email: 'test@test.com',
|
||||
role: [],
|
||||
},
|
||||
fields as Field[],
|
||||
true
|
||||
);
|
||||
|
||||
expect(result.length).toEqual(1);
|
||||
});
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useRelationsStore } from '@/stores/relations';
|
||||
import { FailedValidationException } from '@directus/shared/exceptions';
|
||||
import { Field, LogicalFilterAND } from '@directus/shared/types';
|
||||
import { validatePayload } from '@directus/shared/utils';
|
||||
import { flatten, isNil } from 'lodash';
|
||||
import { cloneDeep, flatten, isEmpty, isNil } from 'lodash';
|
||||
import { applyConditions } from './apply-conditions';
|
||||
|
||||
export function validateItem(item: Record<string, any>, fields: Field[], isNew: boolean) {
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const validationRules = {
|
||||
_and: [],
|
||||
} as LogicalFilterAND;
|
||||
@@ -13,6 +16,8 @@ export function validateItem(item: Record<string, any>, fields: Field[], isNew:
|
||||
|
||||
const requiredFields = fieldsWithConditions.filter((field) => field.meta?.required === true);
|
||||
|
||||
const updatedItem = cloneDeep(item);
|
||||
|
||||
for (const field of requiredFields) {
|
||||
if (isNew && isNil(field.schema?.default_value)) {
|
||||
validationRules._and.push({
|
||||
@@ -22,6 +27,13 @@ export function validateItem(item: Record<string, any>, fields: Field[], isNew:
|
||||
});
|
||||
}
|
||||
|
||||
const relation = relationsStore.getRelationsForField(field.collection, field.field);
|
||||
|
||||
// Check if we are dealing with a relational field that has an empty array as its value
|
||||
if (relation.length > 0 && Array.isArray(updatedItem[field.field]) && isEmpty(updatedItem[field.field])) {
|
||||
updatedItem[field.field] = null;
|
||||
}
|
||||
|
||||
validationRules._and.push({
|
||||
[field.field]: {
|
||||
_nnull: true,
|
||||
@@ -30,7 +42,7 @@ export function validateItem(item: Record<string, any>, fields: Field[], isNew:
|
||||
}
|
||||
|
||||
return flatten(
|
||||
validatePayload(validationRules, item).map((error) =>
|
||||
validatePayload(validationRules, updatedItem).map((error) =>
|
||||
error.details.map((details) => new FailedValidationException(details).extensions)
|
||||
)
|
||||
).map((error) => {
|
||||
|
||||
@@ -133,7 +133,7 @@ import { getRootPath } from '@/utils/get-root-path';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
import Cropper from 'cropperjs';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
|
||||
type Image = {
|
||||
type: string;
|
||||
|
||||
Reference in New Issue
Block a user