mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add support for Conditional Fields (#6864)
* Add conditions field to directus_fields * Add conditions configuration * Apply conditional overrides * Handle conditions in nested groups * Fix reverse mutating conditions * Start on filter setup interface * Move field types/constants to shared * [WIP] Updated client side filter validation * Support logical operators in client validation step * Use new validation util in conditions check * Add nesting in filter seutp * Add filter rule setup configurator * Fixes that should've been done in the merge * Strip out filter-settings interface TBD in a new PR * Move browser to index
This commit is contained in:
@@ -5,7 +5,7 @@ import env from '../../../env';
|
||||
import logger from '../../../logger';
|
||||
import { getSchema } from '../../../utils/get-schema';
|
||||
import { RolesService, UsersService, SettingsService } from '../../../services';
|
||||
import getDatabase, { isInstalled, hasDatabaseConnection, validateDBConnection } from '../../../database';
|
||||
import getDatabase, { isInstalled, validateDBConnection } from '../../../database';
|
||||
import { SchemaOverview } from '../../../types';
|
||||
|
||||
export default async function bootstrap({ skipAdminInit }: { skipAdminInit?: boolean }): Promise<void> {
|
||||
|
||||
@@ -6,7 +6,8 @@ import validateCollection from '../middleware/collection-exists';
|
||||
import { respond } from '../middleware/respond';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { FieldsService } from '../services/fields';
|
||||
import { Field, types } from '../types';
|
||||
import { Field, Type } from '@directus/shared/types';
|
||||
import { TYPES } from '@directus/shared/constants';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
|
||||
const router = Router();
|
||||
@@ -65,7 +66,7 @@ const newFieldSchema = Joi.object({
|
||||
collection: Joi.string().optional(),
|
||||
field: Joi.string().required(),
|
||||
type: Joi.string()
|
||||
.valid(...types, ...ALIAS_TYPES)
|
||||
.valid(...TYPES, ...ALIAS_TYPES)
|
||||
.allow(null)
|
||||
.optional(),
|
||||
schema: Joi.object({
|
||||
@@ -93,7 +94,7 @@ router.post(
|
||||
throw new InvalidPayloadException(error.message);
|
||||
}
|
||||
|
||||
const field: Partial<Field> & { field: string; type: typeof types[number] | null } = req.body;
|
||||
const field: Partial<Field> & { field: string; type: Type | null } = req.body;
|
||||
|
||||
await service.createField(req.params.collection, field);
|
||||
|
||||
@@ -152,7 +153,7 @@ router.patch(
|
||||
|
||||
const updateSchema = Joi.object({
|
||||
type: Joi.string()
|
||||
.valid(...types, ...ALIAS_TYPES)
|
||||
.valid(...TYPES, ...ALIAS_TYPES)
|
||||
.allow(null),
|
||||
schema: Joi.object({
|
||||
default_value: Joi.any(),
|
||||
@@ -183,7 +184,7 @@ router.patch(
|
||||
throw new InvalidPayloadException(`You need to provide "type" when providing "schema".`);
|
||||
}
|
||||
|
||||
const fieldData: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
|
||||
const fieldData: Partial<Field> & { field: string; type: Type } = req.body;
|
||||
|
||||
if (!fieldData.field) fieldData.field = req.params.field;
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_fields', (table) => {
|
||||
table.json('conditions');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_files', (table) => {
|
||||
table.dropColumn('conditions');
|
||||
});
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import yaml from 'js-yaml';
|
||||
import { Knex } from 'knex';
|
||||
import { isObject } from 'lodash';
|
||||
import path from 'path';
|
||||
import { types } from '../../types';
|
||||
import { Type } from '@directus/shared/types';
|
||||
|
||||
type TableSeed = {
|
||||
table: string;
|
||||
columns: {
|
||||
[column: string]: {
|
||||
type?: typeof types[number];
|
||||
type?: Type;
|
||||
primary?: boolean;
|
||||
nullable?: boolean;
|
||||
default?: any;
|
||||
@@ -45,6 +45,8 @@ export default async function runSeed(database: Knex): Promise<void> {
|
||||
for (const [columnName, columnInfo] of Object.entries(seedData.columns)) {
|
||||
let column: Knex.ColumnBuilder;
|
||||
|
||||
if (columnInfo.type === 'alias' || columnInfo.type === 'unknown') return;
|
||||
|
||||
if (columnInfo.type === 'string') {
|
||||
column = tableBuilder.string(columnName, columnInfo.length);
|
||||
} else if (columnInfo.increments) {
|
||||
|
||||
@@ -73,3 +73,8 @@ fields:
|
||||
- collection: directus_fields
|
||||
field: note
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: conditions
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fse from 'fs-extra';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import { FieldMeta } from '../../../types';
|
||||
import { FieldMeta } from '@directus/shared/types';
|
||||
import { requireYAML } from '../../../utils/require-yaml';
|
||||
|
||||
const defaults = requireYAML(require.resolve('./_defaults.yaml'));
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getLocalExtensions,
|
||||
getPackageExtensions,
|
||||
resolvePackage,
|
||||
} from '@directus/shared/utils';
|
||||
} from '@directus/shared/utils/node';
|
||||
import { APP_EXTENSION_TYPES, APP_SHARED_DEPS } from '@directus/shared/constants';
|
||||
import getDatabase from './database';
|
||||
import emitter from './emitter';
|
||||
|
||||
@@ -10,14 +10,8 @@ import logger from '../logger';
|
||||
import { FieldsService, RawField } from '../services/fields';
|
||||
import { ItemsService, MutationOptions } from '../services/items';
|
||||
import Keyv from 'keyv';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
CollectionMeta,
|
||||
FieldMeta,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { AbstractServiceOptions, Accountability, Collection, CollectionMeta, SchemaOverview } from '../types';
|
||||
import { FieldMeta } from '@directus/shared/types';
|
||||
|
||||
export type RawCollection = {
|
||||
collection: string;
|
||||
|
||||
@@ -11,8 +11,8 @@ import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { translateDatabaseError } from '../exceptions/database/translate';
|
||||
import { ItemsService } from '../services/items';
|
||||
import { PayloadService } from '../services/payload';
|
||||
import { AbstractServiceOptions, Accountability, FieldMeta, SchemaOverview, types } from '../types';
|
||||
import { Field } from '../types/field';
|
||||
import { AbstractServiceOptions, Accountability, SchemaOverview } from '../types';
|
||||
import { Field, FieldMeta, Type } from '@directus/shared/types';
|
||||
import getDefaultValue from '../utils/get-default-value';
|
||||
import getLocalType from '../utils/get-local-type';
|
||||
import { toArray } from '../utils/to-array';
|
||||
@@ -21,7 +21,7 @@ import { RelationsService } from './relations';
|
||||
import Keyv from 'keyv';
|
||||
import { DeepPartial } from '@directus/shared/types';
|
||||
|
||||
export type RawField = DeepPartial<Field> & { field: string; type: typeof types[number] };
|
||||
export type RawField = DeepPartial<Field> & { field: string; type: Type };
|
||||
|
||||
export class FieldsService {
|
||||
knex: Knex;
|
||||
@@ -214,7 +214,7 @@ export class FieldsService {
|
||||
|
||||
async createField(
|
||||
collection: string,
|
||||
field: Partial<Field> & { field: string; type: typeof types[number] | null },
|
||||
field: Partial<Field> & { field: string; type: Type | null },
|
||||
table?: Knex.CreateTableBuilder // allows collection creation to
|
||||
): Promise<void> {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
@@ -435,6 +435,9 @@ export class FieldsService {
|
||||
public addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, alter: Column | null = null): void {
|
||||
let column: Knex.ColumnBuilder;
|
||||
|
||||
// Don't attempt to add a DB column for alias / corrupt fields
|
||||
if (field.type === 'alias' || field.type === 'unknown') return;
|
||||
|
||||
if (field.schema?.has_auto_increment) {
|
||||
column = table.increments(field.field);
|
||||
} else if (field.type === 'string') {
|
||||
|
||||
@@ -7,16 +7,8 @@ import { OpenAPIObject, OperationObject, PathItemObject, SchemaObject, TagObject
|
||||
import { version } from '../../package.json';
|
||||
import getDatabase from '../database';
|
||||
import env from '../env';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
Field,
|
||||
Permission,
|
||||
Relation,
|
||||
SchemaOverview,
|
||||
types,
|
||||
} from '../types';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Permission, Relation, SchemaOverview } from '../types';
|
||||
import { Field, Type } from '@directus/shared/types';
|
||||
import { getRelationType } from '../utils/get-relation-type';
|
||||
import { CollectionsService } from './collections';
|
||||
import { FieldsService } from './fields';
|
||||
@@ -459,20 +451,33 @@ class OASSpecsService implements SpecificationSubService {
|
||||
}
|
||||
|
||||
private fieldTypes: Record<
|
||||
typeof types[number],
|
||||
Type,
|
||||
{
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'integer' | 'null' | undefined;
|
||||
format?: string;
|
||||
items?: any;
|
||||
}
|
||||
> = {
|
||||
alias: {
|
||||
type: 'string',
|
||||
},
|
||||
bigInteger: {
|
||||
type: 'integer',
|
||||
format: 'int64',
|
||||
},
|
||||
binary: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
boolean: {
|
||||
type: 'boolean',
|
||||
},
|
||||
csv: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
format: 'date',
|
||||
@@ -488,6 +493,9 @@ class OASSpecsService implements SpecificationSubService {
|
||||
type: 'number',
|
||||
format: 'float',
|
||||
},
|
||||
hash: {
|
||||
type: 'string',
|
||||
},
|
||||
integer: {
|
||||
type: 'integer',
|
||||
},
|
||||
@@ -511,23 +519,13 @@ class OASSpecsService implements SpecificationSubService {
|
||||
type: 'string',
|
||||
format: 'timestamp',
|
||||
},
|
||||
binary: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
unknown: {
|
||||
type: undefined,
|
||||
},
|
||||
uuid: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
csv: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
hash: {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Table } from 'knex-schema-inspector/dist/types/table';
|
||||
import { Field } from './field';
|
||||
import { Field } from '@directus/shared/types';
|
||||
|
||||
export type CollectionMeta = {
|
||||
collection: string;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Column } from 'knex-schema-inspector/dist/types/column';
|
||||
|
||||
export const types = [
|
||||
'bigInteger',
|
||||
'boolean',
|
||||
'date',
|
||||
'dateTime',
|
||||
'decimal',
|
||||
'float',
|
||||
'integer',
|
||||
'json',
|
||||
'string',
|
||||
'text',
|
||||
'time',
|
||||
'timestamp',
|
||||
'binary',
|
||||
'uuid',
|
||||
'hash',
|
||||
'csv',
|
||||
] as const;
|
||||
|
||||
export type FieldMeta = {
|
||||
id: number;
|
||||
collection: string;
|
||||
field: string;
|
||||
special: string[] | null;
|
||||
interface: string | null;
|
||||
options: Record<string, any> | null;
|
||||
readonly: boolean;
|
||||
hidden: boolean;
|
||||
sort: number | null;
|
||||
width: string | null;
|
||||
group: number | null;
|
||||
note: string | null;
|
||||
translations: null;
|
||||
};
|
||||
|
||||
export type Field = {
|
||||
collection: string;
|
||||
field: string;
|
||||
type: typeof types[number];
|
||||
schema: Column | null;
|
||||
meta: FieldMeta | null;
|
||||
};
|
||||
@@ -4,7 +4,6 @@ export * from './assets';
|
||||
export * from './ast';
|
||||
export * from './collection';
|
||||
export * from './extensions';
|
||||
export * from './field';
|
||||
export * from './files';
|
||||
export * from './graphql';
|
||||
export * from './items';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { types } from './field';
|
||||
import { Type } from '@directus/shared/types';
|
||||
import { Permission } from './permissions';
|
||||
import { Relation } from './relation';
|
||||
|
||||
@@ -15,7 +15,7 @@ type CollectionsOverview = {
|
||||
field: string;
|
||||
defaultValue: any;
|
||||
nullable: boolean;
|
||||
type: typeof types[number] | 'unknown' | 'alias';
|
||||
type: Type | 'unknown' | 'alias';
|
||||
dbType: string | null;
|
||||
precision: number | null;
|
||||
scale: number | null;
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
} from 'graphql';
|
||||
import { GraphQLJSON } from 'graphql-compose';
|
||||
import { GraphQLDate } from '../services/graphql';
|
||||
import { types } from '../types';
|
||||
import { Type } from '@directus/shared/types';
|
||||
|
||||
export function getGraphQLType(
|
||||
localType: typeof types[number] | 'alias' | 'unknown'
|
||||
): GraphQLScalarType | GraphQLList<GraphQLType> {
|
||||
export function getGraphQLType(localType: Type | 'alias' | 'unknown'): GraphQLScalarType | GraphQLList<GraphQLType> {
|
||||
switch (localType) {
|
||||
case 'boolean':
|
||||
return GraphQLBoolean;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SchemaOverview } from '@directus/schema/dist/types/overview';
|
||||
import { Column } from 'knex-schema-inspector/dist/types/column';
|
||||
import { FieldMeta, types } from '../types';
|
||||
import { FieldMeta, Type } from '@directus/shared/types';
|
||||
|
||||
/**
|
||||
* Typemap graciously provided by @gpetrov
|
||||
*/
|
||||
const localTypeMap: Record<string, { type: typeof types[number]; useTimezone?: boolean }> = {
|
||||
const localTypeMap: Record<string, { type: Type; useTimezone?: boolean }> = {
|
||||
// Shared
|
||||
boolean: { type: 'boolean' },
|
||||
tinyint: { type: 'boolean' },
|
||||
@@ -90,7 +90,7 @@ const localTypeMap: Record<string, { type: typeof types[number]; useTimezone?: b
|
||||
export default function getLocalType(
|
||||
column: SchemaOverview[string]['columns'][string] | Column,
|
||||
field?: { special?: FieldMeta['special'] }
|
||||
): typeof types[number] | 'unknown' {
|
||||
): Type | 'unknown' {
|
||||
const type = localTypeMap[column.data_type.toLowerCase().split('(')[0]];
|
||||
|
||||
const special = field?.special;
|
||||
|
||||
Reference in New Issue
Block a user