mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'aggregation' into insights
This commit is contained in:
164
api/src/database/helpers/geometry.ts
Normal file
164
api/src/database/helpers/geometry.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Field, RawField } from '@directus/shared/types';
|
||||
import { Knex } from 'knex';
|
||||
import { stringify as geojsonToWKT, GeoJSONGeometry } from 'wellknown';
|
||||
import getDatabase from '..';
|
||||
|
||||
let geometryHelper: KnexSpatial | undefined;
|
||||
|
||||
export function getGeometryHelper(): KnexSpatial {
|
||||
if (!geometryHelper) {
|
||||
const db = getDatabase();
|
||||
const client = db.client.config.client as string;
|
||||
const constructor = {
|
||||
mysql: KnexSpatial_MySQL,
|
||||
mariadb: KnexSpatial_MySQL,
|
||||
sqlite3: KnexSpatial,
|
||||
pg: KnexSpatial_PG,
|
||||
postgres: KnexSpatial_PG,
|
||||
redshift: KnexSpatial_Redshift,
|
||||
mssql: KnexSpatial_MSSQL,
|
||||
oracledb: KnexSpatial_Oracle,
|
||||
}[client];
|
||||
if (!constructor) {
|
||||
throw new Error(`Geometry helper not implemented on ${client}.`);
|
||||
}
|
||||
geometryHelper = new constructor(db);
|
||||
}
|
||||
return geometryHelper;
|
||||
}
|
||||
|
||||
class KnexSpatial {
|
||||
constructor(protected knex: Knex) {}
|
||||
isTrue(expression: Knex.Raw) {
|
||||
return expression;
|
||||
}
|
||||
isFalse(expression: Knex.Raw) {
|
||||
return expression.wrap('NOT ', '');
|
||||
}
|
||||
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
|
||||
const type = field.schema?.geometry_type ?? 'geometry';
|
||||
return table.specificType(field.field, type);
|
||||
}
|
||||
asText(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw('st_astext(??.??) as ??', [table, column, column]);
|
||||
}
|
||||
fromText(text: string): Knex.Raw {
|
||||
return this.knex.raw('st_geomfromtext(?, 4326)', text);
|
||||
}
|
||||
fromGeoJSON(geojson: GeoJSONGeometry): Knex.Raw {
|
||||
return this.fromText(geojsonToWKT(geojson));
|
||||
}
|
||||
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw('st_intersects(??, ?)', [key, geometry]);
|
||||
}
|
||||
intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
return this.isTrue(this._intersects(key, geojson));
|
||||
}
|
||||
nintersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
return this.isFalse(this._intersects(key, geojson));
|
||||
}
|
||||
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw('intersects(??, ?)', [key, geometry]);
|
||||
}
|
||||
intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
return this.isTrue(this._intersects_bbox(key, geojson));
|
||||
}
|
||||
nintersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
return this.isFalse(this._intersects_bbox(key, geojson));
|
||||
}
|
||||
collect(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw('st_astext(st_collect(??.??))', [table, column]);
|
||||
}
|
||||
}
|
||||
|
||||
class KnexSpatial_PG extends KnexSpatial {
|
||||
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
|
||||
const type = field.schema?.geometry_type ?? 'geometry';
|
||||
return table.specificType(field.field, `geometry(${type})`);
|
||||
}
|
||||
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw('?? && ?', [key, geometry]);
|
||||
}
|
||||
}
|
||||
|
||||
class KnexSpatial_MySQL extends KnexSpatial {
|
||||
collect(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw(
|
||||
`concat('geometrycollection(', group_concat(? separator ', '), ')'`,
|
||||
this.asText(table, column)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class KnexSpatial_Redshift extends KnexSpatial {
|
||||
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
|
||||
const type = field.schema?.geometry_type ?? 'geometry';
|
||||
if (type !== 'geometry') field.meta!.special![1] = type;
|
||||
return table.specificType(field.field, 'geometry');
|
||||
}
|
||||
}
|
||||
|
||||
class KnexSpatial_MSSQL extends KnexSpatial {
|
||||
isTrue(expression: Knex.Raw) {
|
||||
return expression.wrap(``, ` = 1`);
|
||||
}
|
||||
isFalse(expression: Knex.Raw) {
|
||||
return expression.wrap(``, ` = 0`);
|
||||
}
|
||||
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
|
||||
const type = field.schema?.geometry_type ?? 'geometry';
|
||||
if (type !== 'geometry') field.meta!.special![1] = type;
|
||||
return table.specificType(field.field, 'geometry');
|
||||
}
|
||||
asText(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw('??.??.STAsText() as ??', [table, column, column]);
|
||||
}
|
||||
fromText(text: string): Knex.Raw {
|
||||
return this.knex.raw('geometry::STGeomFromText(?, 4326)', text);
|
||||
}
|
||||
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw('??.STIntersects(?)', [key, geometry]);
|
||||
}
|
||||
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw('??.STEnvelope().STIntersects(?.STEnvelope())', [key, geometry]);
|
||||
}
|
||||
collect(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw('geometry::CollectionAggregate(??.??).STAsText()', [table, column]);
|
||||
}
|
||||
}
|
||||
|
||||
class KnexSpatial_Oracle extends KnexSpatial {
|
||||
isTrue(expression: Knex.Raw) {
|
||||
return expression.wrap(``, ` = 'TRUE'`);
|
||||
}
|
||||
isFalse(expression: Knex.Raw) {
|
||||
return expression.wrap(``, ` = 'FALSE'`);
|
||||
}
|
||||
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
|
||||
const type = field.schema?.geometry_type ?? 'geometry';
|
||||
if (type !== 'geometry') field.meta!.special![1] = type;
|
||||
return table.specificType(field.field, 'sdo_geometry');
|
||||
}
|
||||
asText(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw('sdo_util.from_wktgeometry(??.??) as ??', [table, column, column]);
|
||||
}
|
||||
fromText(text: string): Knex.Raw {
|
||||
return this.knex.raw('sdo_geometry(?, 4326)', text);
|
||||
}
|
||||
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw(`sdo_overlapbdyintersect(??, ?)`, [key, geometry]);
|
||||
}
|
||||
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
|
||||
const geometry = this.fromGeoJSON(geojson);
|
||||
return this.knex.raw(`sdo_overlapbdyintersect(sdo_geom.sdo_mbr(??), sdo_geom.sdo_mbr(?))`, [key, geometry]);
|
||||
}
|
||||
collect(table: string, column: string): Knex.Raw {
|
||||
return this.knex.raw(`concat('geometrycollection(', listagg(?, ', '), ')'`, this.asText(table, column));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ import env from '../env';
|
||||
import logger from '../logger';
|
||||
import { getConfigFromEnv } from '../utils/get-config-from-env';
|
||||
import { validateEnv } from '../utils/validate-env';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { merge } from 'lodash';
|
||||
import { promisify } from 'util';
|
||||
|
||||
let database: Knex | null = null;
|
||||
let inspector: ReturnType<typeof SchemaInspector> | null = null;
|
||||
@@ -19,6 +23,7 @@ export default function getDatabase(): Knex {
|
||||
'DB_SEARCH_PATH',
|
||||
'DB_CONNECTION_STRING',
|
||||
'DB_POOL',
|
||||
'DB_EXCLUDE_TABLES',
|
||||
]);
|
||||
|
||||
const poolConfig = getConfigFromEnv('DB_POOL');
|
||||
@@ -50,7 +55,15 @@ export default function getDatabase(): Knex {
|
||||
searchPath: env.DB_SEARCH_PATH,
|
||||
connection: env.DB_CONNECTION_STRING || connectionConfig,
|
||||
log: {
|
||||
warn: (msg) => logger.warn(msg),
|
||||
warn: (msg) => {
|
||||
// Ignore warnings about returning not being supported in some DBs
|
||||
if (msg.startsWith('.returning()')) return;
|
||||
|
||||
// Ignore warning about MySQL not supporting TRX for DDL
|
||||
if (msg.startsWith('Transaction was implicitly committed, do not mix transactions and DDL with MySQL')) return;
|
||||
|
||||
return logger.warn(msg);
|
||||
},
|
||||
error: (msg) => logger.error(msg),
|
||||
deprecate: (msg) => logger.info(msg),
|
||||
debug: (msg) => logger.debug(msg),
|
||||
@@ -60,11 +73,24 @@ export default function getDatabase(): Knex {
|
||||
|
||||
if (env.DB_CLIENT === 'sqlite3') {
|
||||
knexConfig.useNullAsDefault = true;
|
||||
poolConfig.afterCreate = (conn: any, cb: any) => {
|
||||
conn.run('PRAGMA foreign_keys = ON', cb);
|
||||
|
||||
poolConfig.afterCreate = async (conn: any, callback: any) => {
|
||||
logger.trace('Enabling SQLite Foreign Keys support...');
|
||||
|
||||
const run = promisify(conn.run.bind(conn));
|
||||
await run('PRAGMA foreign_keys = ON');
|
||||
|
||||
callback(null, conn);
|
||||
};
|
||||
}
|
||||
|
||||
if (env.DB_CLIENT === 'mssql') {
|
||||
// This brings MS SQL in line with the other DB vendors. We shouldn't do any automatic
|
||||
// timezone conversion on the database level, especially not when other database vendors don't
|
||||
// act the same
|
||||
merge(knexConfig, { connection: { options: { useUTC: false } } });
|
||||
}
|
||||
|
||||
database = knex(knexConfig);
|
||||
|
||||
const times: Record<string, number> = {};
|
||||
@@ -94,36 +120,131 @@ export function getSchemaInspector(): ReturnType<typeof SchemaInspector> {
|
||||
return inspector;
|
||||
}
|
||||
|
||||
export async function hasDatabaseConnection(): Promise<boolean> {
|
||||
const database = getDatabase();
|
||||
export async function hasDatabaseConnection(database?: Knex): Promise<boolean> {
|
||||
database = database ?? getDatabase();
|
||||
|
||||
try {
|
||||
if (env.DB_CLIENT === 'oracledb') {
|
||||
if (getDatabaseClient(database) === 'oracle') {
|
||||
await database.raw('select 1 from DUAL');
|
||||
} else {
|
||||
await database.raw('SELECT 1');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateDBConnection(): Promise<void> {
|
||||
export async function validateDatabaseConnection(database?: Knex): Promise<void> {
|
||||
database = database ?? getDatabase();
|
||||
|
||||
try {
|
||||
await hasDatabaseConnection();
|
||||
} catch (error) {
|
||||
if (getDatabaseClient(database) === 'oracle') {
|
||||
await database.raw('select 1 from DUAL');
|
||||
} else {
|
||||
await database.raw('SELECT 1');
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(`Can't connect to the database.`);
|
||||
logger.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getDatabaseClient(database?: Knex): 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'mssql' {
|
||||
database = database ?? getDatabase();
|
||||
|
||||
switch (database.client.constructor.name) {
|
||||
case 'Client_MySQL':
|
||||
return 'mysql';
|
||||
case 'Client_PG':
|
||||
return 'postgres';
|
||||
case 'Client_SQLite3':
|
||||
return 'sqlite';
|
||||
case 'Client_Oracledb':
|
||||
case 'Client_Oracle':
|
||||
return 'oracle';
|
||||
case 'Client_MSSQL':
|
||||
return 'mssql';
|
||||
}
|
||||
|
||||
throw new Error(`Couldn't extract database client`);
|
||||
}
|
||||
|
||||
export async function isInstalled(): Promise<boolean> {
|
||||
const inspector = getSchemaInspector();
|
||||
|
||||
// The existence of a directus_collections table alone isn't a "proper" check to see if everything
|
||||
// is installed correctly of course, but it's safe enough to assume that this collection only
|
||||
// exists when using the installer CLI.
|
||||
// exists when Directus is properly installed.
|
||||
return await inspector.hasTable('directus_collections');
|
||||
}
|
||||
|
||||
export async function validateMigrations(): Promise<boolean> {
|
||||
const database = getDatabase();
|
||||
|
||||
try {
|
||||
let migrationFiles = await fse.readdir(path.join(__dirname, 'migrations'));
|
||||
|
||||
const customMigrationsPath = path.resolve(env.EXTENSIONS_PATH, 'migrations');
|
||||
|
||||
let customMigrationFiles =
|
||||
((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
|
||||
|
||||
migrationFiles = migrationFiles.filter(
|
||||
(file: string) => file.startsWith('run') === false && file.endsWith('.d.ts') === false
|
||||
);
|
||||
|
||||
customMigrationFiles = customMigrationFiles.filter((file: string) => file.endsWith('.js'));
|
||||
|
||||
migrationFiles.push(...customMigrationFiles);
|
||||
|
||||
const requiredVersions = migrationFiles.map((filePath) => filePath.split('-')[0]);
|
||||
const completedVersions = (await database.select('version').from('directus_migrations')).map(
|
||||
({ version }) => version
|
||||
);
|
||||
|
||||
return requiredVersions.every((version) => completedVersions.includes(version));
|
||||
} catch (error: any) {
|
||||
logger.error(`Database migrations cannot be found`);
|
||||
logger.error(error);
|
||||
throw process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These database extensions should be optional, so we don't throw or return any problem states when they don't
|
||||
*/
|
||||
export async function validateDatabaseExtensions(): Promise<void> {
|
||||
const database = getDatabase();
|
||||
const databaseClient = getDatabaseClient(database);
|
||||
|
||||
if (databaseClient === 'postgres') {
|
||||
let available = false;
|
||||
let installed = false;
|
||||
|
||||
const exists = await database.raw(`SELECT name FROM pg_available_extensions WHERE name = 'postgis';`);
|
||||
|
||||
if (exists.rows.length > 0) {
|
||||
available = true;
|
||||
}
|
||||
|
||||
if (available) {
|
||||
try {
|
||||
await database.raw(`SELECT PostGIS_version();`);
|
||||
installed = true;
|
||||
} catch {
|
||||
installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (available === false) {
|
||||
logger.warn(`PostGIS isn't installed. Geometry type support will be limited.`);
|
||||
} else if (available === true && installed === false) {
|
||||
logger.warn(
|
||||
`PostGIS is installed, but hasn't been activated on this database. Geometry type support will be limited.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Knex } from 'knex';
|
||||
// @ts-ignore
|
||||
import Client_Oracledb from 'knex/lib/dialects/oracledb';
|
||||
import env from '../../env';
|
||||
|
||||
async function oracleAlterUrl(knex: Knex, type: string): Promise<void> {
|
||||
await knex.raw('ALTER TABLE "directus_webhooks" ADD "url__temp" ?', [knex.raw(type)]);
|
||||
@@ -23,7 +22,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (env.DB_CLIENT === 'oracledb') {
|
||||
if (knex.client instanceof Client_Oracledb) {
|
||||
await oracleAlterUrl(knex, 'VARCHAR2(255)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Knex } from 'knex';
|
||||
// @ts-ignore
|
||||
import Client_Oracledb from 'knex/lib/dialects/oracledb';
|
||||
import env from '../../env';
|
||||
|
||||
async function oracleAlterCollections(knex: Knex, type: string): Promise<void> {
|
||||
await knex.raw('ALTER TABLE "directus_webhooks" ADD "collections__temp" ?', [knex.raw(type)]);
|
||||
@@ -23,7 +22,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (env.DB_CLIENT === 'oracledb') {
|
||||
if (knex.client instanceof Client_Oracledb) {
|
||||
await oracleAlterCollections(knex, 'VARCHAR2(255)');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await knex(constraint.many_collection)
|
||||
.update({ [constraint.many_field]: null })
|
||||
.whereIn(currentPrimaryKeyField, ids);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.error(
|
||||
`${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.`
|
||||
);
|
||||
@@ -111,7 +111,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
builder.onDelete('SET NULL');
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(
|
||||
`Couldn't add foreign key constraint for ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}`
|
||||
);
|
||||
@@ -140,7 +140,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(relation.many_collection, (table) => {
|
||||
table.dropForeign([relation.many_field]);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(
|
||||
`Couldn't drop foreign key constraint for ${relation.many_collection}.${relation.many_field}<->${relation.one_collection}`
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
table.dropForeign([constraint.column], existingForeignKey?.constraint_name || undefined);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
|
||||
logger.warn(err);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
// Knex uses a default convention for index names: `table_column_type`
|
||||
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(
|
||||
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
|
||||
);
|
||||
@@ -126,7 +126,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
table.foreign(constraint.column).references(constraint.references).onDelete(constraint.on_delete);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
|
||||
logger.warn(err);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
table.dropForeign([constraint.column]);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
|
||||
logger.warn(err);
|
||||
}
|
||||
@@ -156,7 +156,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
// Knex uses a default convention for index names: `table_column_type`
|
||||
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(
|
||||
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
|
||||
);
|
||||
@@ -168,7 +168,7 @@ export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
table.foreign(constraint.column).references(constraint.references);
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
|
||||
logger.warn(err);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { Knex } from 'knex';
|
||||
// @ts-ignore
|
||||
import Client_Oracledb from 'knex/lib/dialects/oracledb';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (knex.client instanceof Client_Oracledb) {
|
||||
return;
|
||||
}
|
||||
|
||||
await knex.schema.alterTable('directus_files', (table) => {
|
||||
table.bigInteger('filesize').nullable().defaultTo(null).alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (knex.client instanceof Client_Oracledb) {
|
||||
return;
|
||||
}
|
||||
|
||||
await knex.schema.alterTable('directus_files', (table) => {
|
||||
table.integer('filesize').nullable().defaultTo(null).alter();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
22
api/src/database/migrations/20210721A-add-default-folder.ts
Normal file
22
api/src/database/migrations/20210721A-add-default-folder.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Knex } from 'knex';
|
||||
import { getDefaultIndexName } from '../../utils/get-default-index-name';
|
||||
|
||||
const indexName = getDefaultIndexName('foreign', 'directus_settings', 'storage_default_folder');
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_settings', (table) => {
|
||||
table
|
||||
.uuid('storage_default_folder')
|
||||
.references('id')
|
||||
.inTable('directus_folders')
|
||||
.withKeyName(indexName)
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_files', (table) => {
|
||||
table.dropForeign(['storage_default_folder'], indexName);
|
||||
table.dropColumn('storage_default_folder');
|
||||
});
|
||||
}
|
||||
49
api/src/database/migrations/20210802A-replace-groups.ts
Normal file
49
api/src/database/migrations/20210802A-replace-groups.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Knex } from 'knex';
|
||||
import logger from '../../logger';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const dividerGroups = await knex.select('*').from('directus_fields').where('interface', '=', 'group-divider');
|
||||
|
||||
for (const dividerGroup of dividerGroups) {
|
||||
const newOptions: { showHeader: true; headerIcon?: string; headerColor?: string } = { showHeader: true };
|
||||
|
||||
if (dividerGroup.options) {
|
||||
try {
|
||||
const options =
|
||||
typeof dividerGroup.options === 'string' ? JSON.parse(dividerGroup.options) : dividerGroup.options;
|
||||
|
||||
if (options.icon) newOptions.headerIcon = options.icon;
|
||||
if (options.color) newOptions.headerColor = options.color;
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't convert previous options from field ${dividerGroup.collection}.${dividerGroup.field}`);
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await knex('directus_fields')
|
||||
.update({
|
||||
interface: 'group-standard',
|
||||
options: JSON.stringify(newOptions),
|
||||
})
|
||||
.where('id', '=', dividerGroup.id);
|
||||
} catch (err: any) {
|
||||
logger.warn(`Couldn't update ${dividerGroup.collection}.${dividerGroup.field} to new group interface`);
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
await knex('directus_fields')
|
||||
.update({
|
||||
interface: 'group-standard',
|
||||
})
|
||||
.where({ interface: 'group-raw' });
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('directus_fields')
|
||||
.update({
|
||||
interface: 'group-raw',
|
||||
})
|
||||
.where('interface', '=', 'group-standard');
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_fields', (table) => {
|
||||
table.boolean('required').defaultTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_fields', (table) => {
|
||||
table.dropColumn('required');
|
||||
});
|
||||
}
|
||||
35
api/src/database/migrations/20210805A-update-groups.ts
Normal file
35
api/src/database/migrations/20210805A-update-groups.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const groups = await knex.select('*').from('directus_fields').where({ interface: 'group-standard' });
|
||||
|
||||
const raw = [];
|
||||
const detail = [];
|
||||
|
||||
for (const group of groups) {
|
||||
const options = typeof group.options === 'string' ? JSON.parse(group.options) : group.options || {};
|
||||
|
||||
if (options.showHeader === true) {
|
||||
detail.push(group);
|
||||
} else {
|
||||
raw.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of raw) {
|
||||
await knex('directus_fields').update({ interface: 'group-raw' }).where({ id: field.id });
|
||||
}
|
||||
|
||||
for (const field of detail) {
|
||||
await knex('directus_fields').update({ interface: 'group-detail' }).where({ id: field.id });
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex('directus_fields')
|
||||
.update({
|
||||
interface: 'group-standard',
|
||||
})
|
||||
.where({ interface: 'group-detail' })
|
||||
.orWhere({ interface: 'group-raw' });
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
// Change image metadata structure to match the output from 'exifr'
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const files = await knex
|
||||
.select<{ id: number; metadata: string }[]>('id', 'metadata')
|
||||
.from('directus_files')
|
||||
.whereNotNull('metadata');
|
||||
|
||||
for (const { id, metadata } of files) {
|
||||
let prevMetadata;
|
||||
|
||||
try {
|
||||
prevMetadata = JSON.parse(metadata);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update only required if metadata has 'exif' data
|
||||
if (prevMetadata.exif) {
|
||||
// Get all data from 'exif' and rename the following keys:
|
||||
// - 'image' to 'ifd0'
|
||||
// - 'thumbnail to 'ifd1'
|
||||
// - 'interoperability' to 'interop'
|
||||
const newMetadata = prevMetadata.exif;
|
||||
|
||||
if (newMetadata.image) {
|
||||
newMetadata.ifd0 = newMetadata.image;
|
||||
delete newMetadata.image;
|
||||
}
|
||||
if (newMetadata.thumbnail) {
|
||||
newMetadata.ifd1 = newMetadata.thumbnail;
|
||||
delete newMetadata.thumbnail;
|
||||
}
|
||||
if (newMetadata.interoperability) {
|
||||
newMetadata.interop = newMetadata.interoperability;
|
||||
delete newMetadata.interoperability;
|
||||
}
|
||||
if (prevMetadata.icc) {
|
||||
newMetadata.icc = prevMetadata.icc;
|
||||
}
|
||||
if (prevMetadata.iptc) {
|
||||
newMetadata.iptc = prevMetadata.iptc;
|
||||
}
|
||||
|
||||
await knex('directus_files')
|
||||
.update({ metadata: JSON.stringify(newMetadata) })
|
||||
.where({ id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const files = await knex
|
||||
.select<{ id: number; metadata: string }[]>('id', 'metadata')
|
||||
.from('directus_files')
|
||||
.whereNotNull('metadata')
|
||||
.whereNot('metadata', '{}');
|
||||
|
||||
for (const { id, metadata } of files) {
|
||||
const prevMetadata = JSON.parse(metadata);
|
||||
|
||||
// Update only required if metadata has keys other than 'icc' and 'iptc'
|
||||
if (Object.keys(prevMetadata).filter((key) => key !== 'icc' && key !== 'iptc').length > 0) {
|
||||
// Put all data under 'exif' and rename/move keys afterwards
|
||||
const newMetadata: { exif: Record<string, unknown>; icc?: unknown; iptc?: unknown } = { exif: prevMetadata };
|
||||
|
||||
if (newMetadata.exif.ifd0) {
|
||||
newMetadata.exif.image = newMetadata.exif.ifd0;
|
||||
delete newMetadata.exif.ifd0;
|
||||
}
|
||||
if (newMetadata.exif.ifd1) {
|
||||
newMetadata.exif.thumbnail = newMetadata.exif.ifd1;
|
||||
delete newMetadata.exif.ifd1;
|
||||
}
|
||||
if (newMetadata.exif.interop) {
|
||||
newMetadata.exif.interoperability = newMetadata.exif.interop;
|
||||
delete newMetadata.exif.interop;
|
||||
}
|
||||
if (newMetadata.exif.icc) {
|
||||
newMetadata.icc = newMetadata.exif.icc;
|
||||
delete newMetadata.exif.icc;
|
||||
}
|
||||
if (newMetadata.exif.iptc) {
|
||||
newMetadata.iptc = newMetadata.exif.iptc;
|
||||
delete newMetadata.exif.iptc;
|
||||
}
|
||||
|
||||
await knex('directus_files')
|
||||
.update({ metadata: JSON.stringify(newMetadata) })
|
||||
.where({ id });
|
||||
}
|
||||
}
|
||||
}
|
||||
15
api/src/database/migrations/20210811A-add-geometry-config.ts
Normal file
15
api/src/database/migrations/20210811A-add-geometry-config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_settings', (table) => {
|
||||
table.json('basemaps');
|
||||
table.string('mapbox_key');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_settings', (table) => {
|
||||
table.dropColumn('basemaps');
|
||||
table.dropColumn('mapbox_key');
|
||||
});
|
||||
}
|
||||
13
api/src/database/migrations/20210831A-remove-limit-column.ts
Normal file
13
api/src/database/migrations/20210831A-remove-limit-column.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_permissions', (table) => {
|
||||
table.dropColumn('limit');
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_permissions', (table) => {
|
||||
table.integer('limit').unsigned();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_webhooks', (table) => {
|
||||
table.text('collections').notNullable().alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.alterTable('directus_webhooks', (table) => {
|
||||
table.text('collections').alter();
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import formatTitle from '@directus/format-title';
|
||||
import fse from 'fs-extra';
|
||||
import { Knex } from 'knex';
|
||||
import path from 'path';
|
||||
import env from '../../env';
|
||||
|
||||
type Migration = {
|
||||
version: string;
|
||||
name: string;
|
||||
timestamp: Date;
|
||||
};
|
||||
import logger from '../../logger';
|
||||
import { Migration } from '../../types';
|
||||
|
||||
export default async function run(database: Knex, direction: 'up' | 'down' | 'latest'): Promise<void> {
|
||||
let migrationFiles = await fse.readdir(__dirname);
|
||||
@@ -67,7 +61,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
|
||||
|
||||
const { up } = require(nextVersion.file);
|
||||
|
||||
console.log(`✨ Applying ${nextVersion.name}...`);
|
||||
logger.info(`Applying ${nextVersion.name}...`);
|
||||
|
||||
await up(database);
|
||||
await database.insert({ version: nextVersion.version, name: nextVersion.name }).into('directus_migrations');
|
||||
@@ -88,7 +82,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
|
||||
|
||||
const { down } = require(migration.file);
|
||||
|
||||
console.log(`✨ Undoing ${migration.name}...`);
|
||||
logger.info(`Undoing ${migration.name}...`);
|
||||
|
||||
await down(database);
|
||||
await database('directus_migrations').delete().where({ version: migration.version });
|
||||
@@ -99,7 +93,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
|
||||
if (migration.completed === false) {
|
||||
const { up } = require(migration.file);
|
||||
|
||||
console.log(`✨ Applying ${migration.name}...`);
|
||||
logger.info(`Applying ${migration.name}...`);
|
||||
|
||||
await up(database);
|
||||
await database.insert({ version: migration.version, name: migration.name }).into('directus_migrations');
|
||||
|
||||
@@ -2,13 +2,15 @@ import { Knex } from 'knex';
|
||||
import { clone, cloneDeep, pick, uniq } from 'lodash';
|
||||
import { PayloadService } from '../services/payload';
|
||||
import { Item, Query, SchemaOverview } from '../types';
|
||||
import { AST, FieldNode, NestedCollectionNode } from '../types/ast';
|
||||
import { AST, FieldNode, NestedCollectionNode, M2ONode } from '../types/ast';
|
||||
import { applyFunctionToColumnName } from '../utils/apply-function-to-column-name';
|
||||
import applyQuery from '../utils/apply-query';
|
||||
import { getColumn } from '../utils/get-column';
|
||||
import { stripFunction } from '../utils/strip-function';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { toArray } from '@directus/shared/utils';
|
||||
import getDatabase from './index';
|
||||
import { isNativeGeometry } from '../utils/geometry';
|
||||
import { getGeometryHelper } from '../database/helpers/geometry';
|
||||
|
||||
type RunASTOptions = {
|
||||
/**
|
||||
@@ -58,7 +60,7 @@ export default async function runAST(
|
||||
|
||||
async function run(collection: string, children: (NestedCollectionNode | FieldNode)[], query: Query) {
|
||||
// Retrieve the database columns to select in the current AST
|
||||
const { columnsToSelect, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(
|
||||
const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(
|
||||
schema,
|
||||
collection,
|
||||
children,
|
||||
@@ -66,7 +68,7 @@ export default async function runAST(
|
||||
);
|
||||
|
||||
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
||||
const dbQuery = await getDBQuery(schema, knex, collection, columnsToSelect, query, options?.nested);
|
||||
const dbQuery = await getDBQuery(schema, knex, collection, fieldNodes, query, options?.nested);
|
||||
|
||||
const rawItems: Item | Item[] = await dbQuery;
|
||||
|
||||
@@ -117,8 +119,17 @@ async function parseCurrentLevel(
|
||||
for (const child of children) {
|
||||
if (child.type === 'field') {
|
||||
const fieldKey = stripFunction(child.name);
|
||||
|
||||
if (columnsInCollection.includes(fieldKey) || fieldKey === '*') {
|
||||
columnsToSelectInternal.push(child.name); // maintain original name here (includes functions)
|
||||
|
||||
if (query.alias) {
|
||||
columnsToSelectInternal.push(
|
||||
...Object.entries(query.alias)
|
||||
.filter(([_key, value]) => value === child.name)
|
||||
.map(([key]) => key)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -127,7 +138,7 @@ async function parseCurrentLevel(
|
||||
if (!child.relation) continue;
|
||||
|
||||
if (child.type === 'm2o') {
|
||||
columnsToSelectInternal.push(child.relation.field);
|
||||
columnsToSelectInternal.push(child.fieldKey);
|
||||
}
|
||||
|
||||
if (child.type === 'm2a') {
|
||||
@@ -138,31 +149,62 @@ async function parseCurrentLevel(
|
||||
nestedCollectionNodes.push(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Always fetch primary key in case there's a nested relation that needs it, but only if there's
|
||||
* no aggregation / grouping going on
|
||||
const isAggregate = (query.aggregate && Object.keys(query.aggregate).length > 0) ?? false;
|
||||
|
||||
/** Always fetch primary key in case there's a nested relation that needs it. Aggregate payloads
|
||||
* can't have nested relational fields
|
||||
*/
|
||||
const hasAggregationOrGrouping = 'aggregate' in query || 'group' in query;
|
||||
if (columnsToSelectInternal.includes(primaryKeyField) === false && hasAggregationOrGrouping === false) {
|
||||
if (isAggregate === false && columnsToSelectInternal.includes(primaryKeyField) === false) {
|
||||
columnsToSelectInternal.push(primaryKeyField);
|
||||
}
|
||||
|
||||
/** Make sure select list has unique values */
|
||||
const columnsToSelect = [...new Set(columnsToSelectInternal)];
|
||||
|
||||
return { columnsToSelect, nestedCollectionNodes, primaryKeyField };
|
||||
const fieldNodes = columnsToSelect.map(
|
||||
(column: string) =>
|
||||
children.find((childNode) => childNode.fieldKey === column) ?? { type: 'field', name: column, fieldKey: column }
|
||||
) as FieldNode[];
|
||||
|
||||
return { fieldNodes, nestedCollectionNodes, primaryKeyField };
|
||||
}
|
||||
|
||||
function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string) {
|
||||
const helper = getGeometryHelper();
|
||||
|
||||
return function (fieldNode: FieldNode | M2ONode): Knex.Raw<string> {
|
||||
let field;
|
||||
|
||||
if (fieldNode.type === 'field') {
|
||||
field = schema.collections[table].fields[stripFunction(fieldNode.name)];
|
||||
} else {
|
||||
field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
|
||||
}
|
||||
|
||||
let alias = undefined;
|
||||
|
||||
if (fieldNode.name !== fieldNode.fieldKey) {
|
||||
alias = fieldNode.fieldKey;
|
||||
}
|
||||
|
||||
if (isNativeGeometry(field)) {
|
||||
return helper.asText(table, field.field);
|
||||
}
|
||||
|
||||
return getColumn(knex, table, fieldNode.name, alias);
|
||||
};
|
||||
}
|
||||
|
||||
function getDBQuery(
|
||||
schema: SchemaOverview,
|
||||
knex: Knex,
|
||||
table: string,
|
||||
columns: string[],
|
||||
fieldNodes: FieldNode[],
|
||||
query: Query,
|
||||
nested?: boolean
|
||||
): Knex.QueryBuilder {
|
||||
const dbQuery = knex.select(columns.map((column) => getColumn(knex, table, column))).from(table);
|
||||
|
||||
const preProcess = getColumnPreprocessor(knex, schema, table);
|
||||
const dbQuery = knex.select(fieldNodes.map(preProcess)).from(table);
|
||||
const queryCopy = clone(query);
|
||||
|
||||
queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : 100;
|
||||
@@ -205,11 +247,19 @@ function applyParentFilters(
|
||||
});
|
||||
|
||||
if (relatedM2OisFetched === false) {
|
||||
nestedNode.children.push({ type: 'field', name: nestedNode.relation.field });
|
||||
nestedNode.children.push({
|
||||
type: 'field',
|
||||
name: nestedNode.relation.field,
|
||||
fieldKey: nestedNode.relation.field,
|
||||
});
|
||||
}
|
||||
|
||||
if (nestedNode.relation.meta?.sort_field) {
|
||||
nestedNode.children.push({ type: 'field', name: nestedNode.relation.meta.sort_field });
|
||||
nestedNode.children.push({
|
||||
type: 'field',
|
||||
name: nestedNode.relation.meta.sort_field,
|
||||
fieldKey: nestedNode.relation.meta.sort_field,
|
||||
});
|
||||
}
|
||||
|
||||
nestedNode.query = {
|
||||
@@ -387,10 +437,9 @@ function removeTemporaryFields(
|
||||
const nestedCollectionNodes: NestedCollectionNode[] = [];
|
||||
|
||||
for (const child of ast.children) {
|
||||
if (child.type === 'field') {
|
||||
fields.push(child.name);
|
||||
} else {
|
||||
fields.push(child.fieldKey);
|
||||
fields.push(child.fieldKey);
|
||||
|
||||
if (child.type !== 'field') {
|
||||
nestedCollectionNodes.push(child);
|
||||
}
|
||||
}
|
||||
@@ -402,7 +451,7 @@ function removeTemporaryFields(
|
||||
|
||||
if (operation === 'count' && aggregateFields.includes('*')) fields.push('count');
|
||||
|
||||
fields.push(...aggregateFields.map((field) => `${field}_${operation}`));
|
||||
fields.push(...aggregateFields.map((field) => `${operation}.${field}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import yaml from 'js-yaml';
|
||||
import { Knex } from 'knex';
|
||||
import { isObject } from 'lodash';
|
||||
import path from 'path';
|
||||
import { types } from '../../types';
|
||||
import { Type, Field } from '@directus/shared/types';
|
||||
import { getGeometryHelper } from '../helpers/geometry';
|
||||
|
||||
type TableSeed = {
|
||||
table: string;
|
||||
columns: {
|
||||
[column: string]: {
|
||||
type?: typeof types[number];
|
||||
type?: Type;
|
||||
primary?: boolean;
|
||||
nullable?: boolean;
|
||||
default?: any;
|
||||
@@ -45,6 +46,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) {
|
||||
@@ -53,6 +56,9 @@ export default async function runSeed(database: Knex): Promise<void> {
|
||||
column = tableBuilder.string(columnName);
|
||||
} else if (columnInfo.type === 'hash') {
|
||||
column = tableBuilder.string(columnName, 255);
|
||||
} else if (columnInfo.type === 'geometry') {
|
||||
const helper = getGeometryHelper();
|
||||
column = helper.createColumn(tableBuilder, { field: columnName } as Field);
|
||||
} else {
|
||||
column = tableBuilder[columnInfo.type!](columnName);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ const defaults: Partial<Permission> = {
|
||||
validation: null,
|
||||
presets: null,
|
||||
fields: ['*'],
|
||||
limit: null,
|
||||
system: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,51 +8,52 @@ defaults:
|
||||
note: null
|
||||
translations: null
|
||||
display_template: null
|
||||
accountability: 'all'
|
||||
|
||||
data:
|
||||
- collection: directus_activity
|
||||
note: Accountability logs for all events
|
||||
note: $t:directus_collection.directus_activity
|
||||
- collection: directus_collections
|
||||
icon: list_alt
|
||||
note: Additional collection configuration and metadata
|
||||
note: $t:directus_collection.directus_collections
|
||||
- collection: directus_fields
|
||||
icon: input
|
||||
note: Additional field configuration and metadata
|
||||
note: $t:directus_collection.directus_fields
|
||||
- collection: directus_files
|
||||
icon: folder
|
||||
note: Metadata for all managed file assets
|
||||
note: $t:directus_collection.directus_files
|
||||
display_template: '{{ $thumbnail }} {{ title }}'
|
||||
- collection: directus_folders
|
||||
note: Provides virtual directories for files
|
||||
note: $t:directus_collection.directus_folders
|
||||
display_template: '{{ name }}'
|
||||
- collection: directus_migrations
|
||||
note: What version of the database you're using
|
||||
note: $t:directus_collection.directus_migrations
|
||||
- collection: directus_permissions
|
||||
icon: admin_panel_settings
|
||||
note: Access permissions for each role
|
||||
note: $t:directus_collection.directus_permissions
|
||||
- collection: directus_presets
|
||||
icon: bookmark_border
|
||||
note: Presets for collection defaults and bookmarks
|
||||
note: $t:directus_collection.directus_presets
|
||||
accountability: null
|
||||
- collection: directus_relations
|
||||
icon: merge_type
|
||||
note: Relationship configuration and metadata
|
||||
note: $t:directus_collection.directus_relations
|
||||
- collection: directus_revisions
|
||||
note: Data snapshots for all activity
|
||||
note: $t:directus_collection.directus_revisions
|
||||
- collection: directus_roles
|
||||
icon: supervised_user_circle
|
||||
note: Permission groups for system users
|
||||
note: $t:directus_collection.directus_roles
|
||||
- collection: directus_sessions
|
||||
note: User session information
|
||||
note: $t:directus_collection.directus_sessions
|
||||
- collection: directus_settings
|
||||
singleton: true
|
||||
note: Project configuration options
|
||||
note: $t:directus_collection.directus_settings
|
||||
- collection: directus_users
|
||||
archive_field: status
|
||||
archive_value: archived
|
||||
unarchive_value: draft
|
||||
icon: people_alt
|
||||
note: System users for the platform
|
||||
note: $t:directus_collection.directus_users
|
||||
display_template: '{{ first_name }} {{ last_name }}'
|
||||
- collection: directus_webhooks
|
||||
note: Configuration for event-based HTTP requests
|
||||
note: $t:directus_collection.directus_webhooks
|
||||
|
||||
@@ -13,19 +13,19 @@ fields:
|
||||
defaultForeground: 'var(--foreground-normal)'
|
||||
defaultBackground: 'var(--background-normal-alt)'
|
||||
choices:
|
||||
- text: Create
|
||||
- text: $t:field_options.directus_activity.create
|
||||
value: create
|
||||
foreground: 'var(--primary)'
|
||||
background: 'var(--primary-25)'
|
||||
- text: Update
|
||||
- text: $t:field_options.directus_activity.update
|
||||
value: update
|
||||
foreground: 'var(--blue)'
|
||||
background: 'var(--blue-25)'
|
||||
- text: Delete
|
||||
- text: $t:field_options.directus_activity.delete
|
||||
value: delete
|
||||
foreground: 'var(--danger)'
|
||||
background: 'var(--danger-25)'
|
||||
- text: Login
|
||||
- text: $t:field_options.directus_activity.login
|
||||
value: authenticate
|
||||
foreground: 'var(--purple)'
|
||||
background: 'var(--purple-25)'
|
||||
|
||||
@@ -8,7 +8,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: box
|
||||
title: Collection Setup
|
||||
title: $t:field_options.directus_collections.collection_setup
|
||||
width: full
|
||||
|
||||
- field: collection
|
||||
@@ -32,7 +32,7 @@ fields:
|
||||
- field: color
|
||||
interface: select-color
|
||||
options:
|
||||
placeholder: Choose a color...
|
||||
placeholder: $t:field_options.directus_collections.note_placeholder
|
||||
width: half
|
||||
|
||||
- field: display_template
|
||||
@@ -45,7 +45,7 @@ fields:
|
||||
special: boolean
|
||||
interface: boolean
|
||||
options:
|
||||
label: Hide within the App
|
||||
label: $t:field_options.directus_collections.hidden_label
|
||||
width: half
|
||||
|
||||
- field: singleton
|
||||
@@ -102,7 +102,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: archive
|
||||
title: Archive
|
||||
title: $t:field_options.directus_collections.archive_divider
|
||||
width: full
|
||||
|
||||
- field: archive_field
|
||||
@@ -110,14 +110,14 @@ fields:
|
||||
options:
|
||||
collectionField: collection
|
||||
allowNone: true
|
||||
placeholder: Choose a field...
|
||||
placeholder: $t:field_options.directus_collections.archive_field
|
||||
width: half
|
||||
|
||||
- field: archive_app_filter
|
||||
interface: boolean
|
||||
special: boolean
|
||||
options:
|
||||
label: Enable App Archive Filter
|
||||
label: $t:field_options.directus_collections.archive_app_filter
|
||||
width: half
|
||||
|
||||
- field: archive_value
|
||||
@@ -125,7 +125,7 @@ fields:
|
||||
options:
|
||||
font: monospace
|
||||
iconRight: archive
|
||||
placeholder: Value set when archiving...
|
||||
placeholder: $t:field_options.directus_collections.archive_value
|
||||
width: half
|
||||
|
||||
- field: unarchive_value
|
||||
@@ -133,7 +133,7 @@ fields:
|
||||
options:
|
||||
font: monospace
|
||||
iconRight: unarchive
|
||||
placeholder: Value set when unarchiving...
|
||||
placeholder: $t:field_options.directus_collections.unarchive_value
|
||||
width: half
|
||||
|
||||
- field: sort_divider
|
||||
@@ -143,14 +143,14 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: sort
|
||||
title: Sort
|
||||
title: $t:field_options.directus_collections.divider
|
||||
width: full
|
||||
|
||||
- field: sort_field
|
||||
interface: system-field
|
||||
options:
|
||||
collectionField: collection
|
||||
placeholder: Choose a field...
|
||||
placeholder: $t:field_options.directus_collections.sort_field
|
||||
typeAllowList:
|
||||
- float
|
||||
- decimal
|
||||
@@ -165,7 +165,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: admin_panel_settings
|
||||
title: Accountability
|
||||
title: $t:field_options.directus_collections.accountability_divider
|
||||
width: full
|
||||
|
||||
- field: accountability
|
||||
|
||||
@@ -52,6 +52,12 @@ fields:
|
||||
special: boolean
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: required
|
||||
hidden: true
|
||||
special: boolean
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: sort
|
||||
width: half
|
||||
@@ -73,3 +79,8 @@ fields:
|
||||
- collection: directus_fields
|
||||
field: note
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: conditions
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
@@ -10,14 +10,14 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: A unique title...
|
||||
placeholder: $t:field_options.directus_files.title
|
||||
width: full
|
||||
|
||||
- field: description
|
||||
interface: input-multiline
|
||||
width: full
|
||||
options:
|
||||
placeholder: An optional description...
|
||||
placeholder: $t:field_options.directus_files.description
|
||||
|
||||
- field: tags
|
||||
interface: tags
|
||||
@@ -35,7 +35,7 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: place
|
||||
placeholder: An optional location...
|
||||
placeholder: $t:field_options.directus_files.location
|
||||
width: half
|
||||
|
||||
- field: storage
|
||||
@@ -49,7 +49,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: insert_drive_file
|
||||
title: File Naming
|
||||
title: $t:field_options.directus_files.storage_divider
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -59,7 +59,7 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: publish
|
||||
placeholder: Name on disk storage...
|
||||
placeholder: $t:field_options.directus_files.filename_disk
|
||||
readonly: true
|
||||
width: half
|
||||
|
||||
@@ -67,7 +67,7 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: get_app
|
||||
placeholder: Name when downloading...
|
||||
placeholder: $t:field_options.directus_files.filename_download
|
||||
width: half
|
||||
|
||||
- field: metadata
|
||||
@@ -106,6 +106,7 @@ fields:
|
||||
display: user
|
||||
width: half
|
||||
hidden: true
|
||||
special: user-created
|
||||
|
||||
- field: uploaded_on
|
||||
display: datetime
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -15,9 +15,6 @@ fields:
|
||||
- field: role
|
||||
width: half
|
||||
|
||||
- field: limit
|
||||
width: half
|
||||
|
||||
- field: collection
|
||||
width: half
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ fields:
|
||||
- field: name
|
||||
interface: input
|
||||
options:
|
||||
placeholder: The unique name for this role...
|
||||
placeholder: $t:field_options.directus_roles.name
|
||||
width: half
|
||||
|
||||
- field: icon
|
||||
@@ -20,7 +20,7 @@ fields:
|
||||
- field: description
|
||||
interface: input
|
||||
options:
|
||||
placeholder: A description of this role...
|
||||
placeholder: $t:field_options.directus_roles.description
|
||||
width: full
|
||||
|
||||
- field: app_access
|
||||
@@ -36,7 +36,7 @@ fields:
|
||||
- field: ip_access
|
||||
interface: tags
|
||||
options:
|
||||
placeholder: Add allowed IP addresses, leave empty to allow all...
|
||||
placeholder: $t:field_options.directus_roles.ip_access
|
||||
special: csv
|
||||
width: full
|
||||
|
||||
@@ -60,13 +60,13 @@ fields:
|
||||
template: '{{ name }}'
|
||||
addLabel: Add New Module...
|
||||
fields:
|
||||
- name: Icon
|
||||
- name: $t:field_options.directus_roles.fields.icon_name
|
||||
field: icon
|
||||
type: string
|
||||
meta:
|
||||
interface: select-icon
|
||||
width: half
|
||||
- name: Name
|
||||
- name: $t:field_options.directus_roles.fields.name_name
|
||||
field: name
|
||||
type: string
|
||||
meta:
|
||||
@@ -74,8 +74,8 @@ fields:
|
||||
width: half
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: Enter a title...
|
||||
- name: Link
|
||||
placeholder:
|
||||
- name: $t:field_options.directus_roles.fields.link_name
|
||||
field: link
|
||||
type: string
|
||||
meta:
|
||||
@@ -83,7 +83,7 @@ fields:
|
||||
width: full
|
||||
options:
|
||||
iconRight: link
|
||||
placeholder: Relative or absolute URL...
|
||||
placeholder: $t:field_options.directus_roles.fields.link_placeholder
|
||||
special: json
|
||||
width: full
|
||||
|
||||
@@ -91,9 +91,9 @@ fields:
|
||||
interface: list
|
||||
options:
|
||||
template: '{{ group_name }}'
|
||||
addLabel: Add New Group...
|
||||
addLabel: $t:field_options.directus_roles.collection_list.group_name_addLabel
|
||||
fields:
|
||||
- name: Group Name
|
||||
- name: $t:field_options.directus_roles.collection_list.fields.group_name
|
||||
field: group_name
|
||||
type: string
|
||||
meta:
|
||||
@@ -101,10 +101,10 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: Label this group...
|
||||
placeholder: $t:field_options.directus_roles.collection_list.fields.group_placeholder
|
||||
schema:
|
||||
is_nullable: false
|
||||
- name: Type
|
||||
- name: $t:field_options.directus_roles.collection_list.fields.type_name
|
||||
field: accordion
|
||||
type: string
|
||||
schema:
|
||||
@@ -115,21 +115,21 @@ fields:
|
||||
options:
|
||||
choices:
|
||||
- value: always_open
|
||||
text: Always Open
|
||||
text: $t:field_options.directus_roles.collection_list.fields.choices_always
|
||||
- value: start_open
|
||||
text: Start Open
|
||||
text: $t:field_options.directus_roles.collection_list.fields.choices_start_open
|
||||
- value: start_collapsed
|
||||
text: Start Collapsed
|
||||
- name: Collections
|
||||
text: $t:field_options.directus_roles.collection_list.fields.choices_start_collapsed
|
||||
- name: $t:field_options.directus_roles.collections_name
|
||||
field: collections
|
||||
type: JSON
|
||||
meta:
|
||||
interface: list
|
||||
options:
|
||||
addLabel: Add New Collection...
|
||||
addLabel: $t:field_options.directus_roles.collections_addLabel
|
||||
template: '{{ collection }}'
|
||||
fields:
|
||||
- name: Collection
|
||||
- name: $t:field_options.directus_roles.collections_name
|
||||
field: collection
|
||||
type: string
|
||||
meta:
|
||||
|
||||
@@ -8,7 +8,7 @@ fields:
|
||||
interface: input
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: My project...
|
||||
placeholder: $t:field_options.directus_settings.project_name_placeholder
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Name
|
||||
@@ -26,7 +26,7 @@ fields:
|
||||
|
||||
- field: project_color
|
||||
interface: select-color
|
||||
note: Login & Logo Background
|
||||
note: $t:field_options.directus_settings.project_logo_note
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Brand Color
|
||||
@@ -44,7 +44,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: public
|
||||
title: Public Pages
|
||||
title: $t:fields.directus_settings.public_pages
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -67,14 +67,14 @@ fields:
|
||||
- field: public_note
|
||||
interface: input-multiline
|
||||
options:
|
||||
placeholder: A short, public message that supports markdown formatting...
|
||||
placeholder: $t:field_options.directus_settings.public_note_placeholder
|
||||
width: full
|
||||
|
||||
- field: security_divider
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: security
|
||||
title: Security
|
||||
title: $t:security
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -85,11 +85,11 @@ fields:
|
||||
options:
|
||||
choices:
|
||||
- value: null
|
||||
text: None – Not Recommended
|
||||
text: $t:field_options.directus_settings.auth_password_policy.none_text
|
||||
- value: '/^.{8,}$/'
|
||||
text: Weak – Minimum 8 Characters
|
||||
text: $t:field_options.directus_settings.auth_password_policy.weak_text
|
||||
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
|
||||
text: Strong – Upper / Lowercase / Numbers / Special
|
||||
text: $t:field_options.directus_settings.auth_password_policy.strong_text
|
||||
allowOther: true
|
||||
width: half
|
||||
|
||||
@@ -104,7 +104,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: storage
|
||||
title: Files & Thumbnails
|
||||
title: $t:fields.directus_settings.files_and_thumbnails
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -115,7 +115,7 @@ fields:
|
||||
options:
|
||||
fields:
|
||||
- field: key
|
||||
name: Key
|
||||
name: $t:key
|
||||
type: string
|
||||
schema:
|
||||
is_nullable: false
|
||||
@@ -124,7 +124,7 @@ fields:
|
||||
options:
|
||||
slug: true
|
||||
onlyOnCreate: false
|
||||
width: half
|
||||
width: full
|
||||
- field: fit
|
||||
name: Fit
|
||||
type: string
|
||||
@@ -135,16 +135,16 @@ fields:
|
||||
options:
|
||||
choices:
|
||||
- value: contain
|
||||
text: Contain (preserve aspect ratio)
|
||||
text: $t:field_options.directus_settings.storage_asset_presets.fit.contain_text
|
||||
- value: cover
|
||||
text: Cover (forces exact size)
|
||||
text: $t:field_options.directus_settings.storage_asset_presets.fit.cover_text
|
||||
- value: inside
|
||||
text: Fit inside
|
||||
text: $t:field_options.directus_settings.storage_asset_presets.fit.fit_text
|
||||
- value: outside
|
||||
text: Fit outside
|
||||
text: $t:field_options.directus_settings.storage_asset_presets.fit.outside_text
|
||||
width: half
|
||||
- field: width
|
||||
name: Width
|
||||
name: $t:width
|
||||
type: integer
|
||||
schema:
|
||||
is_nullable: false
|
||||
@@ -152,7 +152,7 @@ fields:
|
||||
interface: input
|
||||
width: half
|
||||
- field: height
|
||||
name: Height
|
||||
name: $t:height
|
||||
type: integer
|
||||
schema:
|
||||
is_nullable: false
|
||||
@@ -161,7 +161,7 @@ fields:
|
||||
width: half
|
||||
- field: quality
|
||||
type: integer
|
||||
name: Quality
|
||||
name: $t:quality
|
||||
schema:
|
||||
default_value: 80
|
||||
is_nullable: false
|
||||
@@ -173,6 +173,7 @@ fields:
|
||||
step: 1
|
||||
width: half
|
||||
- field: withoutEnlargement
|
||||
name: Upscaling
|
||||
type: boolean
|
||||
schema:
|
||||
default_value: false
|
||||
@@ -180,7 +181,51 @@ fields:
|
||||
interface: boolean
|
||||
width: half
|
||||
options:
|
||||
label: Don't upscale images
|
||||
label: $t:no_upscale
|
||||
- field: format
|
||||
name: Format
|
||||
type: string
|
||||
schema:
|
||||
is_nullable: false
|
||||
default_value: ''
|
||||
meta:
|
||||
interface: select-dropdown
|
||||
options:
|
||||
allowNone: true
|
||||
choices:
|
||||
- value: jpeg
|
||||
text: JPEG
|
||||
- value: png
|
||||
text: PNG
|
||||
- value: webp
|
||||
text: WebP
|
||||
- value: tiff
|
||||
text: Tiff
|
||||
width: half
|
||||
- field: transforms
|
||||
name: $t:field_options.directus_settings.additional_transforms
|
||||
type: json
|
||||
schema:
|
||||
is_nullable: false
|
||||
default_value: []
|
||||
meta:
|
||||
note: $t:field_options.directus_settings.transforms_note
|
||||
|
||||
interface: json
|
||||
options:
|
||||
template: >
|
||||
[
|
||||
["blur", 45],
|
||||
["grayscale"],
|
||||
["extend", { "right": 500, "background": "rgb(255, 0, 0)" }]
|
||||
]
|
||||
placeholder: >
|
||||
[
|
||||
["blur", 45],
|
||||
["grayscale"],
|
||||
["extend", { "right": 500, "background": "rgb(255, 0, 0)" }]
|
||||
]
|
||||
width: full
|
||||
template: '{{key}}'
|
||||
special: json
|
||||
width: full
|
||||
@@ -190,18 +235,23 @@ fields:
|
||||
options:
|
||||
choices:
|
||||
- value: all
|
||||
text: All
|
||||
text: $t:all
|
||||
- value: none
|
||||
text: None
|
||||
text: $t:none
|
||||
- value: presets
|
||||
text: Presets Only
|
||||
text: $t:presets_only
|
||||
width: half
|
||||
|
||||
- field: storage_default_folder
|
||||
interface: system-folder
|
||||
width: half
|
||||
note: $t:interfaces.system-folder.field_hint
|
||||
|
||||
- field: overrides_divider
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: brush
|
||||
title: App Overrides
|
||||
title: $t:fields.directus_settings.overrides
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -213,3 +263,72 @@ fields:
|
||||
language: css
|
||||
lineNumber: true
|
||||
width: full
|
||||
|
||||
- field: map_divider
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: map
|
||||
title: $t:maps
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
width: full
|
||||
|
||||
- field: mapbox_key
|
||||
interface: input
|
||||
options:
|
||||
icon: key
|
||||
title: $t:field_options.directus_settings.mapbox_key
|
||||
placeholder: $t:field_options.directus_settings.mapbox_placeholder
|
||||
iconLeft: vpn_key
|
||||
font: monospace
|
||||
width: half
|
||||
|
||||
- field: basemaps
|
||||
interface: list
|
||||
special: json
|
||||
options:
|
||||
template: '{{name}}'
|
||||
fields:
|
||||
- field: name
|
||||
name: $t:name
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: text-input
|
||||
options:
|
||||
placeholder: Enter the basemap name...
|
||||
- field: type
|
||||
name: $t:type
|
||||
meta:
|
||||
interface: select-dropdown
|
||||
options:
|
||||
choices:
|
||||
- value: raster
|
||||
text: $t:field_options.directus_settings.basemaps_raster
|
||||
- value: tile
|
||||
text: $t:field_options.directus_settings.basemaps_tile
|
||||
- value: style
|
||||
text: $t:field_options.directus_settings.basemaps_style
|
||||
- field: url
|
||||
name: $t:url
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: text-input
|
||||
options:
|
||||
placeholder: http://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||
- field: tileSize
|
||||
name: $t:tile_size
|
||||
schema:
|
||||
is_nullable: true
|
||||
meta:
|
||||
interface: input
|
||||
options:
|
||||
placeholder: '512'
|
||||
conditions:
|
||||
- name: typeNeqRaster
|
||||
rule:
|
||||
type:
|
||||
_neq: 'raster'
|
||||
hidden: true
|
||||
|
||||
@@ -64,7 +64,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: face
|
||||
title: User Preferences
|
||||
title: $t:fields.directus_users.user_preferences
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -79,11 +79,11 @@ fields:
|
||||
options:
|
||||
choices:
|
||||
- value: auto
|
||||
text: Automatic (Based on System)
|
||||
text: $t:fields.directus_users.theme_auto
|
||||
- value: light
|
||||
text: Light Mode
|
||||
text: $t:fields.directus_users.theme_light
|
||||
- value: dark
|
||||
text: Dark Mode
|
||||
text: $t:fields.directus_users.theme_dark
|
||||
width: half
|
||||
|
||||
- field: tfa_secret
|
||||
@@ -95,7 +95,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: verified_user
|
||||
title: Admin Options
|
||||
title: $t:fields.directus_users.admin_options
|
||||
color: '#E35169'
|
||||
special:
|
||||
- alias
|
||||
@@ -106,15 +106,15 @@ fields:
|
||||
interface: select-dropdown
|
||||
options:
|
||||
choices:
|
||||
- text: Draft
|
||||
- text: $t:fields.directus_users.status_draft
|
||||
value: draft
|
||||
- text: Invited
|
||||
- text: $t:fields.directus_users.status_invited
|
||||
value: invited
|
||||
- text: Active
|
||||
- text: $t:fields.directus_users.status_active
|
||||
value: active
|
||||
- text: Suspended
|
||||
- text: $t:fields.directus_users.status_suspended
|
||||
value: suspended
|
||||
- text: Archived
|
||||
- text: $t:fields.directus_users.status_archived
|
||||
value: archived
|
||||
width: half
|
||||
|
||||
@@ -132,7 +132,7 @@ fields:
|
||||
interface: token
|
||||
options:
|
||||
iconRight: vpn_key
|
||||
placeholder: Enter a secure access token...
|
||||
placeholder: $t:fields.directus_users.token_placeholder
|
||||
width: full
|
||||
|
||||
- field: id
|
||||
|
||||
@@ -38,26 +38,26 @@ fields:
|
||||
defaultBackground: 'var(--background-normal-alt)'
|
||||
showAsDot: true
|
||||
choices:
|
||||
- text: Active
|
||||
- text: $t:active
|
||||
value: active
|
||||
foreground: 'var(--primary-10)'
|
||||
background: 'var(--primary)'
|
||||
- text: Inactive
|
||||
- text: $t:inactive
|
||||
value: inactive
|
||||
foreground: 'var(--foreground-normal)'
|
||||
background: 'var(--background-normal-alt)'
|
||||
options:
|
||||
choices:
|
||||
- text: Active
|
||||
- text: $t:active
|
||||
value: active
|
||||
- text: Inactive
|
||||
- text: $t:inactive
|
||||
value: inactive
|
||||
width: half
|
||||
|
||||
- field: data
|
||||
interface: boolean
|
||||
options:
|
||||
label: Send Event Data
|
||||
label: $t:fields.directus_webhooks.data_label
|
||||
special: boolean
|
||||
width: half
|
||||
display: boolean
|
||||
@@ -66,7 +66,7 @@ fields:
|
||||
interface: presentation-divider
|
||||
options:
|
||||
icon: api
|
||||
title: Triggers
|
||||
title: $t:fields.directus_webhooks.triggers
|
||||
special:
|
||||
- alias
|
||||
- no-data
|
||||
@@ -76,11 +76,11 @@ fields:
|
||||
interface: select-multiple-checkbox
|
||||
options:
|
||||
choices:
|
||||
- text: Create
|
||||
- text: $t:create
|
||||
value: create
|
||||
- text: Update
|
||||
- text: $t:update
|
||||
value: update
|
||||
- text: Delete
|
||||
- text: $t:delete_label
|
||||
value: delete
|
||||
special: csv
|
||||
width: full
|
||||
@@ -89,19 +89,19 @@ fields:
|
||||
defaultForeground: 'var(--foreground-normal)'
|
||||
defaultBackground: 'var(--background-normal-alt)'
|
||||
choices:
|
||||
- text: Create
|
||||
- text: $t:create
|
||||
value: create
|
||||
foreground: 'var(--primary)'
|
||||
background: 'var(--primary-25)'
|
||||
- text: Update
|
||||
- text: $t:update
|
||||
value: update
|
||||
foreground: 'var(--blue)'
|
||||
background: 'var(--blue-25)'
|
||||
- text: Delete
|
||||
- text: $t:delete_label
|
||||
value: delete
|
||||
foreground: 'var(--danger)'
|
||||
background: 'var(--danger-25)'
|
||||
- text: Login
|
||||
- text: $t:login
|
||||
value: authenticate
|
||||
foreground: 'var(--purple)'
|
||||
background: 'var(--purple-25)'
|
||||
|
||||
Reference in New Issue
Block a user