diff --git a/src/composables/use-collection/use-collection.ts b/src/composables/use-collection/use-collection.ts index 8762ca22e8..8aeae0dabb 100644 --- a/src/composables/use-collection/use-collection.ts +++ b/src/composables/use-collection/use-collection.ts @@ -18,19 +18,21 @@ export function useCollection(collection: Ref) { const primaryKeyField = computed(() => { // Every collection has a primary key; rules of the land // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return fields.value?.find((field) => field.collection === collection.value && field.primary_key === true)!; + return fields.value?.find( + (field) => field.collection === collection.value && field.database.is_primary_key === true + )!; }); const userCreatedField = computed(() => { - return fields.value?.find((field) => field.type === 'user_created') || null; + return fields.value?.find((field) => field.system?.special === 'user_created') || null; }); const statusField = computed(() => { - return fields.value?.find((field) => field.type === 'status') || null; + return fields.value?.find((field) => field.system?.special === 'status') || null; }); const sortField = computed(() => { - return fields.value?.find((field) => field.type === 'sort') || null; + return fields.value?.find((field) => field.system?.special === 'sort') || null; }); type Status = { @@ -48,7 +50,7 @@ export function useCollection(collection: Ref) { const softDeleteStatus = computed(() => { if (statusField.value === null) return null; - const statuses = Object.values(statusField.value?.options?.status_mapping || {}); + const statuses = Object.values(statusField.value?.system?.options?.status_mapping || {}); return ( (statuses.find((status) => (status as Status).soft_delete === true) as Status | undefined)?.value || null ); diff --git a/src/composables/use-field-tree/use-field-tree.ts b/src/composables/use-field-tree/use-field-tree.ts index 7bea0bda2f..91b9ec00cc 100644 --- a/src/composables/use-field-tree/use-field-tree.ts +++ b/src/composables/use-field-tree/use-field-tree.ts @@ -12,7 +12,10 @@ export default function useFieldTree(collection: Ref) { const tree = computed(() => { return fieldsStore .getFieldsForCollection(collection.value) - .filter((field: Field) => field.hidden_browse === false && field.type.toLowerCase() !== 'alias') + .filter( + (field: Field) => + field.system?.hidden_browse === false && field.system?.special?.toLowerCase() !== 'alias' + ) .map((field: Field) => parseField(field, [])); function parseField(field: Field, parents: Field[]) { @@ -40,7 +43,9 @@ export default function useFieldTree(collection: Ref) { return fieldsStore .getFieldsForCollection(relatedCollection) .filter( - (field: Field) => field.hidden_browse === false && field.type.toLowerCase() !== 'alias' + (field: Field) => + field.system?.hidden_browse === false && + field.system?.special?.toLowerCase() !== 'alias' ); }) .flat() diff --git a/src/composables/use-form-fields/use-form-fields.ts b/src/composables/use-form-fields/use-form-fields.ts index a5eb4b63ff..ab3d02a95f 100644 --- a/src/composables/use-form-fields/use-form-fields.ts +++ b/src/composables/use-form-fields/use-form-fields.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + import { computed, Ref } from '@vue/composition-api'; import { isEmpty } from '@/utils/is-empty'; import getDefaultInterfaceForType from '@/utils/get-default-interface-for-type'; @@ -14,37 +16,36 @@ export default function useFormFields(fields: Ref) { * * This can be optimized by combining a bunch of these maps and filters */ - // Filter out the fields that are marked hidden on detail formFields = formFields.filter((field) => { - const hiddenDetail = field.hidden_detail; + const hiddenDetail = field.system?.hidden_detail; if (isEmpty(hiddenDetail)) return true; return hiddenDetail === false; }); // Sort the fields on the sort column value formFields = formFields.sort((a, b) => { - if (a.sort == b.sort) return 0; - if (a.sort === null || a.sort === undefined) return 1; - if (b.sort === null || b.sort === undefined) return -1; - return a.sort > b.sort ? 1 : -1; + if (a.system!.sort == b.system!.sort) return 0; + if (a.system!.sort === null || a.system!.sort === undefined) return 1; + if (b.system!.sort === null || b.system!.sort === undefined) return -1; + return a.system!.sort > b.system!.sort ? 1 : -1; }); // Make sure all form fields have a width associated with it formFields = formFields.map((field) => { - if (!field.width) { - field.width = 'full'; + if (!field.system!.width) { + field.system!.width = 'full'; } return field; }); formFields = formFields.map((field) => { - const interfaceUsed = interfaces.find((int) => int.id === field.interface); + const interfaceUsed = interfaces.find((int) => int.id === field.system!.interface); const interfaceExists = interfaceUsed !== undefined; if (interfaceExists === false) { - field.interface = getDefaultInterfaceForType(field.type); + field.system!.interface = getDefaultInterfaceForType(field.system!.type); } if (interfaceUsed?.hideLabel === true) { @@ -63,11 +64,11 @@ export default function useFormFields(fields: Ref) { formFields = formFields.map((field, index, formFields) => { if (index === 0) return field; - if (field.width === 'half') { + if (field.system!.width === 'half') { const prevField = formFields[index - 1]; - if (prevField.width === 'half') { - field.width = 'half-right'; + if (prevField.system!.width === 'half') { + field.system!.width = 'half-right'; } } diff --git a/src/layouts/tabular/tabular.vue b/src/layouts/tabular/tabular.vue index 3a3fef4a7b..b555b960e1 100644 --- a/src/layouts/tabular/tabular.vue +++ b/src/layouts/tabular/tabular.vue @@ -227,7 +227,7 @@ export default defineComponent({ const { info, primaryKeyField, fields: fieldsInCollection, sortField } = useCollection(collection); const availableFields = computed(() => - fieldsInCollection.value.filter(({ hidden_browse }) => hidden_browse === false) + fieldsInCollection.value.filter((field) => field.system?.hidden_browse === false) ); const { sort, limit, page, fields, fieldsWithRelational } = useItemOptions(); diff --git a/src/modules/define.ts b/src/modules/define.ts index 99f20cf8c6..28d43d1cbb 100644 --- a/src/modules/define.ts +++ b/src/modules/define.ts @@ -14,11 +14,11 @@ export function defineModule(config: ModuleDefineParam | ((context: ModuleContex if (options.routes !== undefined) { options.routes = options.routes.map((route) => { if (route.path) { - route.path = `/:project/${options.id}${route.path}`; + route.path = `/${options.id}${route.path}`; } if (route.redirect) { - route.redirect = `/:project/${options.id}${route.redirect}`; + route.redirect = `/${options.id}${route.redirect}`; } return route; diff --git a/src/modules/register.ts b/src/modules/register.ts index 7b05eb472a..5f2c98538f 100644 --- a/src/modules/register.ts +++ b/src/modules/register.ts @@ -10,7 +10,7 @@ const moduleRoutes = modules replaceRoutes((routes) => insertBeforeProjectWildcard(routes, moduleRoutes)); export function insertBeforeProjectWildcard(currentRoutes: RouteConfig[], routesToBeAdded: RouteConfig[]) { - // Find the index of the /:project/* route, so we can insert the module routes right above that - const wildcardIndex = currentRoutes.findIndex((route) => route.path === '/:project/*'); + // Find the index of the /* route, so we can insert the module routes right above that + const wildcardIndex = currentRoutes.findIndex((route) => route.path === '/*'); return [...currentRoutes.slice(0, wildcardIndex), ...routesToBeAdded, ...currentRoutes.slice(wildcardIndex)]; } diff --git a/src/stores/fields/fields.ts b/src/stores/fields/fields.ts index dc4b4651b1..b7a820a47f 100644 --- a/src/stores/fields/fields.ts +++ b/src/stores/fields/fields.ts @@ -8,37 +8,57 @@ import formatTitle from '@directus/format-title'; import notify from '@/utils/notify'; import useRelationsStore from '@/stores/relations'; import { Relation } from '@/stores/relations/types'; +import getLocalType from '@/utils/get-local-type'; const fakeFilesField: Field = { - id: -1, collection: 'directus_files', field: '$file', + database: null, name: i18n.t('file'), - datatype: null, - type: 'file', - unique: false, - primary_key: false, - default_value: null, - auto_increment: false, - note: null, - signed: false, - sort: 0, - interface: null, - options: null, - display: 'file', - display_options: null, - hidden_detail: true, - hidden_browse: false, - locked: true, - required: false, - translation: null, - readonly: true, - width: 'full', - validation: null, - group: null, - length: null, + type: 'integer', + system: { + id: -1, + collection: 'directus_files', + field: '$file', + sort: null, + special: null, + interface: null, + options: null, + display: 'file', + display_options: null, + hidden_detail: true, + hidden_browse: false, + locked: true, + required: false, + translation: null, + readonly: true, + width: 'full', + group: null, + }, }; +function getSystemDefault(collection: string, field: string): Field['system'] { + return { + id: -1, + collection, + field, + group: null, + hidden_browse: false, + hidden_detail: false, + interface: null, + display: null, + display_options: null, + locked: false, + options: null, + readonly: false, + required: false, + sort: null, + special: null, + translation: null, + width: 'full', + }; +} + export const useFieldsStore = createStore({ id: 'fieldsStore', state: () => ({ @@ -62,17 +82,20 @@ export const useFieldsStore = createStore({ * is a fake m2o to itself. */ - this.state.fields = [...fields.map(this.addTranslationsForField), fakeFilesField]; + this.state.fields = [...fields.map(this.parseField), fakeFilesField]; }, async dehydrate() { this.reset(); }, - addTranslationsForField(field: Field) { + parseField(field: FieldRaw): Field { let name: string | VueI18n.TranslateResult; - if (notEmpty(field.translation) && field.translation.length > 0) { - for (let i = 0; i < field.translation.length; i++) { - const { locale, translation } = field.translation[i]; + const type = field.database === null ? 'alias' : getLocalType(field.database.type); + const system = field.system === null ? getSystemDefault(field.collection, field.field) : field.system; + + if (notEmpty(system.translation) && system.translation.length > 0) { + for (let i = 0; i < system.translation.length; i++) { + const { locale, translation } = system.translation[i]; i18n.mergeLocaleMessage(locale, { fields: { @@ -89,6 +112,8 @@ export const useFieldsStore = createStore({ return { ...field, name, + type, + system, }; }, async createField(collectionKey: string, newField: Field) { @@ -247,7 +272,7 @@ export const useFieldsStore = createStore({ /** @NOTE it's safe to assume every collection has a primary key */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const primaryKeyField = this.state.fields.find( - (field) => field.collection === collection && field.primary_key === true + (field) => field.collection === collection && field.database?.is_primary_key === true ); return primaryKeyField; diff --git a/src/stores/fields/types.ts b/src/stores/fields/types.ts index 9c04d22a9b..03ff42e759 100644 --- a/src/stores/fields/types.ts +++ b/src/stores/fields/types.ts @@ -7,98 +7,72 @@ type Translation = { export type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill'; -export type Type = +export type LocalType = | 'alias' - | 'array' - | 'boolean' + | 'bigInteger' | 'binary' - | 'datetime' + | 'binary' + | 'boolean' | 'date' - | 'time' - | 'file' - | 'files' - | 'hash' - | 'group' - | 'integer' + | 'datetime' | 'decimal' - | 'json' - | 'lang' - | 'm2o' - | 'o2m' - | 'm2m' - | 'slug' - | 'sort' - | 'status' + | 'float' + | 'integer' | 'string' - | 'translation' - | 'uuid' - | 'datetime_created' - | 'datetime_updated' - | 'user_created' - | 'user_updated' - | 'user'; + | 'text' + | 'time' + | 'timestamp' + | 'unknown'; -export const types: Type[] = [ - 'alias', - 'array', - 'boolean', - 'binary', - 'datetime', - 'date', - 'time', - 'file', - 'files', - 'hash', - 'group', - 'integer', - 'decimal', - 'json', - 'lang', - 'm2o', - 'o2m', - 'm2m', - 'slug', - 'sort', - 'status', - 'string', - 'translation', - 'uuid', - 'datetime_created', - 'datetime_updated', - 'user_created', - 'user_updated', - 'user', -]; +export type DatabaseColumn = { + /** @todo import this from knex-schema-inspector when that's launched */ + name: string; + table: string; + type: string; + default_value: any | null; + max_length: number | null; + is_nullable: boolean; + is_primary_key: boolean; + has_auto_increment: boolean; + foreign_key_column: string | null; + foreign_key_table: string | null; + comment: string | null; -export interface FieldRaw { + // Postgres Only + schema?: string; + foreign_key_schema?: string | null; +}; + +export type SystemField = { id: number; collection: string; field: string; - datatype: string | null; - unique: boolean; - primary_key: boolean; - auto_increment: boolean; - default_value: any; // eslint-disable-line @typescript-eslint/no-explicit-any - note: string | TranslateResult | null; - signed: boolean; - type: Type; - sort: null | number; - interface: string | null; - options: null | { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any - display: string | null; - display_options: null | { [key: string]: any }; // eslint-disable-line @typescript-eslint/no-explicit-any - hidden_detail: boolean; - hidden_browse: boolean; - required: boolean; - locked: boolean; - translation: null | Translation[]; - readonly: boolean; - width: null | Width; - validation: string | null; group: number | null; - length: string | number | null; + hidden_browse: boolean; + hidden_detail: boolean; + locked: boolean; + interface: string | null; + display: string | null; + options: null | Record; + display_options: null | Record; + readonly: boolean; + required: boolean; + sort: number | null; + special: string | null; + translation: null | Translation[]; + width: Width | null; +}; + +export interface FieldRaw { + collection: string; + field: string; + + database: DatabaseColumn | null; + system: SystemField | null; } export interface Field extends FieldRaw { name: string | TranslateResult; + type: LocalType; + system: SystemField; } diff --git a/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts b/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts index d7520325f2..a0460ca79d 100644 --- a/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts +++ b/src/utils/adjust-fields-for-displays/adjust-fields-for-displays.ts @@ -10,9 +10,9 @@ export default function adjustFieldsForDisplays(fields: readonly string[], paren const field: Field = fieldsStore.getField(parentCollection, fieldKey); if (!field) return fieldKey; - if (field.display === null) return fieldKey; + if (field.system?.display === null) return fieldKey; - const display = displays.find((d) => d.id === field.display); + const display = displays.find((d) => d.id === field.system?.display); if (!display) return fieldKey; if (!display?.fields) return fieldKey; @@ -23,7 +23,7 @@ export default function adjustFieldsForDisplays(fields: readonly string[], paren if (typeof display.fields === 'function') { return display - .fields(field.display_options, { + .fields(field.system?.display_options, { collection: field.collection, field: field.field, type: field.type, diff --git a/src/utils/get-local-type/get-local-type.ts b/src/utils/get-local-type/get-local-type.ts new file mode 100644 index 0000000000..4abf1cf28b --- /dev/null +++ b/src/utils/get-local-type/get-local-type.ts @@ -0,0 +1,87 @@ +import { LocalType } from '@/stores/fields/types'; + +/** + * Typemap graciously provided by @gpetrov + */ +const localTypeMap: Record = { + // Shared + boolean: { type: 'boolean' }, + tinyint: { type: 'boolean' }, + smallint: { type: 'integer' }, + mediumint: { type: 'integer' }, + int: { type: 'integer' }, + integer: { type: 'integer' }, + serial: { type: 'integer' }, + bigint: { type: 'bigInteger' }, + bigserial: { type: 'bigInteger' }, + clob: { type: 'text' }, + tinytext: { type: 'text' }, + mediumtext: { type: 'text' }, + longtext: { type: 'text' }, + text: { type: 'text' }, + varchar: { type: 'string' }, + longvarchar: { type: 'string' }, + varchar2: { type: 'string' }, + nvarchar: { type: 'string' }, + image: { type: 'binary' }, + ntext: { type: 'text' }, + char: { type: 'string' }, + date: { type: 'date' }, + datetime: { type: 'datetime' }, + timestamp: { type: 'timestamp' }, + time: { type: 'time' }, + float: { type: 'float' }, + double: { type: 'float' }, + 'double precision': { type: 'float' }, + real: { type: 'float' }, + decimal: { type: 'decimal' }, + numeric: { type: 'integer' }, + + // MySQL + string: { type: 'text' }, + year: { type: 'integer' }, + blob: { type: 'binary' }, + mediumblob: { type: 'binary' }, + + // MS SQL + bit: { type: 'boolean' }, + smallmoney: { type: 'float' }, + money: { type: 'float' }, + datetimeoffset: { type: 'datetime', useTz: true }, + datetime2: { type: 'datetime' }, + smalldatetime: { type: 'datetime' }, + nchar: { type: 'text' }, + binary: { type: 'binary' }, + varbinary: { type: 'binary' }, + + // Postgres + int2: { type: 'integer' }, + serial4: { type: 'integer' }, + int4: { type: 'integer' }, + serial8: { type: 'integer' }, + int8: { type: 'integer' }, + bool: { type: 'boolean' }, + 'character varying': { type: 'string' }, + character: { type: 'string' }, + interval: { type: 'string' }, + _varchar: { type: 'string' }, + bpchar: { type: 'string' }, + timestamptz: { type: 'timestamp' }, + 'timestamp with time zone': { type: 'timestamp', useTz: true }, + 'timestamp without thime zone': { type: 'timestamp' }, + timetz: { type: 'time' }, + 'time with time zone': { type: 'time', useTz: true }, + 'time without time zone': { type: 'time' }, + float4: { type: 'float' }, + float8: { type: 'float' }, +}; + +export default function getLocalType(databaseType: string) { + const type = localTypeMap[databaseType]; + + if (type) { + return type.type; + } + + return 'unknown'; +} diff --git a/src/utils/get-local-type/index.ts b/src/utils/get-local-type/index.ts new file mode 100644 index 0000000000..0417bf9aa0 --- /dev/null +++ b/src/utils/get-local-type/index.ts @@ -0,0 +1,4 @@ +import getLocalType from './get-local-type'; + +export { getLocalType }; +export default getLocalType; diff --git a/src/views/private/components/filter-drawer-detail/filter-drawer-detail.vue b/src/views/private/components/filter-drawer-detail/filter-drawer-detail.vue index dd89b7a6c6..33a7e85b4a 100644 --- a/src/views/private/components/filter-drawer-detail/filter-drawer-detail.vue +++ b/src/views/private/components/filter-drawer-detail/filter-drawer-detail.vue @@ -72,7 +72,10 @@ export default defineComponent({ const fieldTree = computed(() => { return fieldsStore .getFieldsForCollection(props.collection) - .filter((field: Field) => field.hidden_browse === false && field.type.toLowerCase() !== 'alias') + .filter( + (field: Field) => + field.system?.hidden_browse !== true && field.system?.special?.toLowerCase() !== 'alias' + ) .map((field: Field) => parseField(field, [])); function parseField(field: Field, parents: Field[]) { @@ -101,7 +104,8 @@ export default defineComponent({ .getFieldsForCollection(relatedCollection) .filter( (field: Field) => - field.hidden_browse === false && field.type.toLowerCase() !== 'alias' + field.system?.hidden_browse !== true && + field.system?.special?.toLowerCase() !== 'alias' ); }) .flat() diff --git a/src/views/private/components/render-template/render-template.vue b/src/views/private/components/render-template/render-template.vue index 9d65e401ed..34acae15ed 100644 --- a/src/views/private/components/render-template/render-template.vue +++ b/src/views/private/components/render-template/render-template.vue @@ -64,9 +64,9 @@ export default defineComponent({ if (value === undefined) return '???'; // If no display is configured, we can render the raw value - if (field.display === null) return value; + if (field.system?.display === null) return value; - const displayInfo = displays.find((display) => display.id === field.display); + const displayInfo = displays.find((display) => display.id === field.system?.display); // If used display doesn't exist in the current project, return raw value if (!displayInfo) return value; @@ -74,16 +74,16 @@ export default defineComponent({ // If the display handler is a function, we parse the value and return the result if (typeof displayInfo.handler === 'function') { const handler = displayInfo.handler as Function; - return handler(value, field.display_options); + return handler(value, field.system?.display_options); } return { - component: field.display, - options: field.display_options, + component: field.system?.display, + options: field.system?.display_options, value: value, - interface: field.interface, - interfaceOptions: field.options, - type: field.type, + interface: field.system?.interface, + interfaceOptions: field.system?.options, + type: field.system?.special /** @todo check what this is used for */, }; }) .map((p) => p)