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:
Rijk van Zanten
2021-07-27 00:02:24 +02:00
committed by GitHub
parent 47e9d2f1fe
commit 92e1ee77bd
121 changed files with 792 additions and 261 deletions

View File

@@ -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> {

View File

@@ -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;

View File

@@ -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');
});
}

View File

@@ -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) {

View File

@@ -73,3 +73,8 @@ fields:
- collection: directus_fields
field: note
width: half
- collection: directus_fields
field: conditions
hidden: true
special: json

View File

@@ -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'));

View File

@@ -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';

View File

@@ -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;

View File

@@ -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') {

View File

@@ -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',
},
};
}

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;