Fix type errors

This commit is contained in:
rijkvanzanten
2020-07-03 15:10:40 -04:00
parent be20289ae5
commit b275de46e9
49 changed files with 370 additions and 407 deletions

View File

@@ -9,11 +9,11 @@
<component
v-if="interfaceExists"
:is="`interface-${field.interface}`"
v-bind="field.options"
:is="`interface-${field.system.interface}`"
v-bind="field.system.options"
:disabled="disabled"
:value="value === undefined ? field.default_value : value"
:width="field.width"
:value="value === undefined ? field.database.default_value : value"
:width="field.system.width"
:type="field.type"
:collection="field.collection"
:field="field.field"
@@ -66,7 +66,7 @@ export default defineComponent({
},
setup(props) {
const interfaceExists = computed(() => {
return !!interfaces.find((inter) => inter.id === props.field.interface);
return !!interfaces.find((inter) => inter.id === props.field.system.interface);
});
return { interfaceExists };

View File

@@ -1,5 +1,5 @@
<template>
<div class="field" :key="field.field" :class="field.width">
<div class="field" :key="field.field" :class="field.system.width">
<v-menu
v-if="field.hideLabel !== true"
placement="bottom-start"
@@ -90,7 +90,7 @@ export default defineComponent({
setup(props) {
const isDisabled = computed(() => {
if (props.disabled) return true;
if (props.field.readonly) return true;
if (props.field.system.readonly) return true;
if (props.batchMode && props.batchActive === false) return true;
return false;
});
@@ -98,7 +98,7 @@ export default defineComponent({
const _value = computed(() => {
if (props.value !== undefined) return props.value;
if (props.initialValue !== undefined) return props.initialValue;
return props.field.default_value;
return props.field.database?.default_value;
});
return { isDisabled, marked, _value };

View File

@@ -1,7 +1,7 @@
import { Field } from '@/stores/fields/types';
import { TranslateResult } from 'vue-i18n';
export type FormField = Partial<Field> & {
export type FormField = DeepPartial<Field> & {
field: string;
name: string | TranslateResult;
hideLabel?: boolean;

View File

@@ -1,130 +0,0 @@
import Vue from 'vue';
import markdown from './readme.md';
import withPadding from '../../../.storybook/decorators/with-padding';
import VForm from './v-form.vue';
import { defineComponent, ref } from '@vue/composition-api';
import { useFieldsStore } from '@/stores/fields';
import { FormField } from './types';
import { i18n } from '@/lang';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import RawValue from '../../../.storybook/raw-value.vue';
Vue.component('v-form', VForm);
export default {
title: 'Components / Form',
parameters: {
notes: markdown,
},
decorators: [withPadding, withKnobs],
};
export const collection = () =>
defineComponent({
i18n,
setup() {
const fieldsStore = useFieldsStore({});
fieldsStore.state.fields = [
{
collection: 'articles',
field: 'title',
datatype: 'VARCHAR',
unique: false,
primary_key: false,
auto_increment: false,
default_value: null,
note: '',
signed: true,
id: 197,
type: 'string',
sort: 2,
interface: 'text-input',
hidden_detail: false,
hidden_browse: false,
required: false,
options: {
font: 'monospace',
},
locked: false,
translation: null,
readonly: false,
width: 'full',
validation: null,
group: null,
length: '65535',
name: 'Title',
},
] as any;
},
template: `
<v-form
collection="articles"
:initial-values="{
title: 'Hello World!'
}"
/>
`,
});
export const fields = () =>
defineComponent({
i18n,
components: { RawValue },
props: {
loading: {
default: boolean('Loading', false),
},
batchMode: {
default: boolean('Batch', false),
},
},
setup() {
const fields: FormField[] = [
{
field: 'field',
name: 'My Field',
interface: 'text-input',
width: 'half',
options: { placeholder: 'First Field' },
sort: 1,
},
{
field: 'another-field',
name: 'Another Field',
interface: 'text-input',
width: 'half',
options: null,
note: 'I am required',
sort: 2,
required: true,
},
{
field: 'third-field',
name: 'A Third Field',
interface: 'text-input',
width: 'full',
options: null,
sort: 3,
default_value: 'This is my default value',
},
];
const edits = ref({});
return { fields, edits };
},
template: `
<div>
<v-form
v-model="edits"
:loading="loading"
:batch-mode="batchMode"
:fields="fields"
:initial-values="{
'third-field': 'Hello World!'
}"
/>
<raw-value>{{ edits }}</raw-value>
</div>
`,
});

View File

@@ -130,7 +130,7 @@ export default defineComponent({
return (
props.loading ||
props.disabled === true ||
field.readonly === true ||
field.system.readonly === true ||
(props.batchMode && batchActiveFields.value.includes(field.field) === false)
);
}

View File

@@ -19,7 +19,7 @@ export function useCollection(collection: Ref<string>) {
// 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.database.is_primary_key === true
(field) => field.collection === collection.value && field.database?.is_primary_key === true
)!;
});

View File

@@ -45,7 +45,7 @@ export default function useFormFields(fields: Ref<Field[]>) {
const interfaceExists = interfaceUsed !== undefined;
if (interfaceExists === false) {
field.system!.interface = getDefaultInterfaceForType(field.system!.type);
field.system!.interface = getDefaultInterfaceForType(field.type);
}
if (interfaceUsed?.hideLabel === true) {

View File

@@ -7,5 +7,5 @@ export default defineDisplay(({ i18n }) => ({
icon: 'query_builder',
handler: DisplayDateTime,
options: [],
types: ['datetime', 'datetime_created', 'datetime_updated', 'date', 'time'],
types: ['datetime', 'date', 'time', 'timestamp'],
}));

View File

@@ -6,7 +6,7 @@ export default defineDisplay(({ i18n }) => ({
name: i18n.t('file'),
icon: 'insert_photo',
handler: DisplayFile,
types: ['file'],
types: ['string'],
options: [],
fields: ['data', 'type', 'title'],
}));

View File

@@ -4,7 +4,7 @@ import DisplayImage from './image.vue';
export default defineDisplay(({ i18n }) => ({
id: 'image',
name: i18n.t('image'),
types: ['file'],
types: ['string'],
icon: 'insert_photo',
handler: DisplayImage,
options: [

View File

@@ -4,7 +4,7 @@ import DisplayStatusBadge from './status-badge.vue';
export default defineDisplay(({ i18n }) => ({
id: 'status-badge',
name: i18n.t('status_badge'),
types: ['status'],
types: ['string'],
icon: 'flag',
handler: DisplayStatusBadge,
options: null,

View File

@@ -4,7 +4,7 @@ import DisplayStatusDot from './status-dot.vue';
export default defineDisplay(({ i18n }) => ({
id: 'status-dot',
name: i18n.t('status_dot'),
types: ['status'],
types: ['string'],
icon: 'flag',
handler: DisplayStatusDot,
options: null,

View File

@@ -4,7 +4,7 @@ import DisplayTags from './tags.vue';
export default defineDisplay(({ i18n }) => ({
id: 'tags',
name: i18n.t('tags'),
types: ['array'],
types: ['json'],
icon: 'label',
handler: DisplayTags,
options: [

View File

@@ -23,7 +23,7 @@ export default defineDisplay(({ i18n }) => ({
width: 'full',
},
],
types: ['m2o', 'o2m', 'm2m'],
types: ['alias', 'string', 'integer', 'bigInteger', 'text'],
fields: (options: Options, { field, collection }) => {
const relatedCollection = getRelatedCollection(collection, field);
const { primaryKeyField } = useCollection(ref(relatedCollection as string));

View File

@@ -4,7 +4,7 @@ import DisplayUser from './user.vue';
export default defineDisplay(({ i18n }) => ({
id: 'user',
name: i18n.t('user'),
types: ['user', 'user_created', 'user_updated'],
types: ['string'],
icon: 'person',
handler: DisplayUser,
options: [

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('checkboxes'),
icon: 'radio_button_checked',
component: InterfaceCheckboxes,
types: ['array'],
types: ['json'],
options: [
{
field: 'choices',

View File

@@ -15,7 +15,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('code'),
icon: 'code',
component: InterfaceCode,
types: ['string', 'json', 'array'],
types: ['string', 'json', 'text'],
options: [
{
field: 'template',

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('datetime'),
icon: 'today',
component: InterfaceDateTime,
types: ['datetime', 'datetime_created', 'datetime_updated', 'date', 'time'],
types: ['datetime', 'date', 'time', 'timestamp'],
options: [
{
field: 'includeSeconds',

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('dropdown_multiple'),
icon: 'arrow_drop_down_circle',
component: InterfaceDropdownMultiselect,
types: ['array'],
types: ['json'],
options: [
{
field: 'choices',

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('file'),
icon: 'note_add',
component: InterfaceFile,
types: ['file'],
types: ['string'],
options: [],
}));

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('files'),
icon: 'note_add',
component: InterfaceFiles,
types: ['files'],
types: ['alias'],
options: [],
}));

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('image'),
icon: 'insert_photo',
component: InterfaceImage,
types: ['file'],
types: ['string'],
options: [],
}));

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('many_to_many'),
icon: 'note_add',
component: InterfaceManyToMany,
types: ['m2m'],
types: ['alias'],
options: [],
}));

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('many_to_one'),
icon: 'arrow_right_alt',
component: InterfaceManyToOne,
types: ['m2o'],
types: ['string', 'text', 'integer', 'bigInteger'],
options: [
{
field: 'template',

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('numeric'),
icon: 'dialpad',
component: InterfaceNumeric,
types: ['integer', 'decimal'],
types: ['integer', 'decimal', 'float'],
options: [
{
field: 'placeholder',

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('one_to_many'),
icon: 'arrow_right_alt',
component: InterfaceOneToMany,
types: ['o2m'],
types: ['alias'],
options: [],
}));

View File

@@ -99,7 +99,7 @@ export default defineComponent({
props.fields.forEach((field) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newDefaults[field.field!] = field.default_value;
newDefaults[field.field!] = field.database?.default_value;
});
if (props.value !== null) {

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('slider'),
icon: 'linear_scale',
component: InterfaceSlider,
types: ['integer', 'decimal'],
types: ['integer', 'decimal', 'float'],
options: [
{
field: 'minValue',

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('status'),
icon: 'bubble_chart',
component: InterfaceStatus,
types: ['status'],
types: ['string'],
options: [
{
field: 'status_mapping',

View File

@@ -6,7 +6,7 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('tags'),
icon: 'local_offer',
component: InterfaceTags,
types: ['array'],
types: ['json'],
options: [
{
field: 'placeholder',

View File

@@ -5,7 +5,7 @@ export default defineInterface(({ i18n }) => ({
id: 'translations',
name: i18n.t('translations'),
icon: 'replay',
types: ['translation'],
types: ['alias'],
component: InterfaceTranslations,
options: [],
}));

View File

@@ -6,6 +6,6 @@ export default defineInterface(({ i18n }) => ({
name: i18n.t('user'),
icon: 'person',
component: InterfaceUser,
types: ['user', 'user_created', 'user_updated'],
types: ['integer', 'bigInteger', 'string', 'text'],
options: [],
}));

View File

@@ -190,7 +190,7 @@ export default defineComponent({
},
detailRoute: {
type: String,
default: `/{{project}}/collections/{{collection}}/{{primaryKey}}`,
default: `/collections/{{collection}}/{{primaryKey}}`,
},
file: {
type: Object as PropType<File>,
@@ -219,11 +219,11 @@ export default defineComponent({
const { info, primaryKeyField, fields: fieldsInCollection } = useCollection(collection);
const availableFields = computed(() =>
fieldsInCollection.value.filter((field) => field.hidden_browse === false)
fieldsInCollection.value.filter((field) => field.system.hidden_browse !== true)
);
const fileFields = computed(() => {
return [...availableFields.value.filter((field) => field.type === 'file')];
return [...availableFields.value.filter((field) => field.system.special === 'file')];
});
const { size, icon, imageSource, title, subtitle, imageFit } = useViewOptions();

View File

@@ -206,7 +206,7 @@ export default defineComponent({
},
detailRoute: {
type: String,
default: `/{{project}}/collections/{{collection}}/{{primaryKey}}`,
default: `/collections/{{collection}}/{{primaryKey}}`,
},
readonly: {
type: Boolean,
@@ -369,7 +369,7 @@ export default defineComponent({
_viewQuery.value?.fields ||
availableFields.value
.filter((field: Field) => {
return field.primary_key === false && field.type !== 'sort';
return field.database?.is_primary_key === false && field.system.special !== 'sort';
})
.slice(0, 4)
.map(({ field }) => field);
@@ -425,10 +425,10 @@ export default defineComponent({
value: field.field,
width: localWidths.value[field.field] || _viewOptions.value?.widths?.[field.field] || null,
field: {
display: field.display,
displayOptions: field.display_options,
interface: field.interface,
interfaceOptions: field.options,
display: field.system.display,
displayOptions: field.system.display_options,
interface: field.system.interface,
interfaceOptions: field.system.options,
type: field.type,
field: field.field,
},
@@ -516,11 +516,11 @@ export default defineComponent({
const field = availableFields.value.find((field) => field.field === fieldKey);
if (field === undefined) return null;
if (!field.display) return null;
if (!field.system.display) return null;
return {
display: field.display,
options: field.display_options,
display: field.system.display,
options: field.system.display_options,
};
}
}

View File

@@ -160,7 +160,7 @@ const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
const primaryKeyField = fieldsStore.getPrimaryKeyFieldForCollection(to.params.collection);
const item = await api.get(`/${to.params.project}/items/${to.params.collection}`, {
const item = await api.get(`/items/${to.params.collection}`, {
params: {
limit: 1,
fields: primaryKeyField.field,
@@ -170,7 +170,7 @@ const redirectIfNeeded: NavigationGuard = async (to, from, next) => {
const primaryKey = item.data.data[primaryKeyField.field];
return next(`/${to.params.project}/collections/${to.params.collection}/${primaryKey}`);
return next(`/collections/${to.params.collection}/${primaryKey}`);
}
return next();

View File

@@ -217,17 +217,19 @@ export default defineComponent({
}
function getPrimaryKeyField() {
const field: Partial<Field> = {
auto_increment: true,
const field: DeepPartial<Field> = {
field: primaryKeyFieldName.value,
hidden_browse: false,
hidden_detail: false,
interface: 'numeric',
length: 15,
primary_key: true,
type: 'integer',
datatype: 'INT',
readonly: true,
system: {
hidden_browse: false,
hidden_detail: false,
interface: 'numeric',
readonly: true,
},
database: {
has_auto_increment: true,
is_primary_key: true,
type: 'INT',
},
};
if (primaryKeyFieldType.value === 'uuid') {
@@ -256,127 +258,150 @@ export default defineComponent({
}
function getSystemFields() {
const fields: Partial<Field>[] = [];
const fields: DeepPartial<Field>[] = [];
if (systemFields[0].enabled === true) {
fields.push({
type: 'status',
datatype: 'VARCHAR',
length: 20,
field: systemFields[0].name,
interface: 'status',
default_value: 'draft',
width: 'full',
required: true,
options: {
status_mapping: {
published: {
name: 'Published',
value: 'published',
text_color: 'white',
background_color: 'accent',
browse_subdued: false,
browse_badge: true,
soft_delete: false,
published: true,
required_fields: true,
},
draft: {
name: 'Draft',
value: 'draft',
text_color: 'white',
background_color: 'blue-grey-100',
browse_subdued: true,
browse_badge: true,
soft_delete: false,
published: false,
required_fields: false,
},
deleted: {
name: 'Deleted',
value: 'deleted',
text_color: 'white',
background_color: 'red',
browse_subdued: true,
browse_badge: true,
soft_delete: true,
published: false,
required_fields: false,
system: {
width: 'full',
required: true,
options: {
status_mapping: {
published: {
name: 'Published',
value: 'published',
text_color: 'white',
background_color: 'accent',
browse_subdued: false,
browse_badge: true,
soft_delete: false,
published: true,
required_fields: true,
},
draft: {
name: 'Draft',
value: 'draft',
text_color: 'white',
background_color: 'blue-grey-100',
browse_subdued: true,
browse_badge: true,
soft_delete: false,
published: false,
required_fields: false,
},
deleted: {
name: 'Deleted',
value: 'deleted',
text_color: 'white',
background_color: 'red',
browse_subdued: true,
browse_badge: true,
soft_delete: true,
published: false,
required_fields: false,
},
},
},
special: 'status',
interface: 'status',
},
database: {
type: 'VARCHAR',
default_value: 'draft',
},
});
}
if (systemFields[1].enabled === true) {
fields.push({
type: 'sort',
datatype: 'INT',
field: systemFields[1].name,
interface: 'sort',
hidden_detail: true,
hidden_browse: true,
width: 'full',
system: {
interface: 'sort',
hidden_detail: true,
hidden_browse: true,
width: 'full',
special: 'sort',
},
database: {
type: 'INT',
},
});
}
if (systemFields[2].enabled === true) {
fields.push({
type: 'user_created',
datatype: 'INT',
field: systemFields[2].name,
interface: 'owner',
options: {
template: '{{first_name}} {{last_name}}',
display: 'both',
system: {
special: 'user_created',
interface: 'owner',
options: {
template: '{{first_name}} {{last_name}}',
display: 'both',
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
},
database: {
type: 'INT' /** @todo make these vendor based */,
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
});
}
if (systemFields[3].enabled === true) {
fields.push({
type: 'datetime_created',
datatype: 'DATETIME',
field: systemFields[3].name,
interface: 'datetime-created',
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
system: {
special: 'datetime_created',
interface: 'datetime-created',
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
},
database: {
type: 'DATETIME',
},
});
}
if (systemFields[4].enabled === true) {
fields.push({
type: 'user_updated',
datatype: 'INT',
field: systemFields[4].name,
interface: 'user-updated',
options: {
template: '{{first_name}} {{last_name}}',
display: 'both',
system: {
special: 'user_updated',
interface: 'user-updated',
options: {
template: '{{first_name}} {{last_name}}',
display: 'both',
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
},
database: {
type: 'INT',
},
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
});
}
if (systemFields[5].enabled === true) {
fields.push({
type: 'datetime_updated',
datatype: 'DATETIME',
field: systemFields[5].name,
interface: 'datetime-updated',
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
system: {
special: 'datetime_updated',
interface: 'datetime-updated',
readonly: true,
hidden_detail: true,
hidden_browse: true,
width: 'full',
},
database: {
type: 'DATETIME',
},
});
}

View File

@@ -128,7 +128,7 @@ export default defineComponent({
const { duplicateActive, duplicateName, collections, duplicateTo, saveDuplicate, duplicating } = useDuplicate();
const interfaceName = computed(() => {
return interfaces.find((inter) => inter.id === props.field.interface)?.name;
return interfaces.find((inter) => inter.id === props.field.system.interface)?.name;
});
return {
@@ -194,8 +194,8 @@ export default defineComponent({
collection: duplicateTo.value,
};
delete newField.id;
delete newField.sort;
delete newField.system.id;
delete newField.system.sort;
delete newField.name;
duplicating.value = true;

View File

@@ -69,7 +69,7 @@ export default defineComponent({
});
const selectedDisplay = computed(() => {
return displays.find((inter) => inter.id === props.value.display) || null;
return displays.find((inter) => inter.id === props.value.system.display) || null;
});
return { emitValue, items, selectedDisplay };

View File

@@ -71,7 +71,7 @@ export default defineComponent({
});
const selectedInterface = computed(() => {
return interfaces.find((inter) => inter.id === props.value.interface) || null;
return interfaces.find((inter) => inter.id === props.value.system.interface) || null;
});
return { emitValue, items, selectedInterface, setInterface };

View File

@@ -25,7 +25,7 @@ export default defineComponent({
},
},
setup(props) {
const selectedInterface = computed(() => interfaces.find((inter) => inter.id === props.value.interface));
const selectedInterface = computed(() => interfaces.find((inter) => inter.id === props.value.system.interface));
const typeChoices = computed(() => {
let availableTypes = types;
@@ -45,115 +45,145 @@ export default defineComponent({
{
field: 'field',
name: i18n.t('database_column_name'),
interface: 'slug',
width: 'half',
options: null,
readonly: props.isNew === false,
system: {
interface: 'slug',
width: 'half',
options: null,
readonly: props.isNew === false,
},
},
{
field: 'note',
name: i18n.t('note'),
interface: 'text-input',
width: 'full',
options: {
placeholder: i18n.t('add_helpful_note'),
system: {
interface: 'text-input',
width: 'full',
options: {
placeholder: i18n.t('add_helpful_note'),
},
},
},
{
field: 'translation',
name: i18n.t('translations'),
interface: 'key-value',
width: 'full',
options: null,
system: {
interface: 'key-value',
width: 'full',
options: null,
},
},
{
field: 'default_value',
name: i18n.t('default_value'),
interface: 'text-input' /** @TODO base on selected datatype */,
width: 'half',
options: {
placeholder: i18n.t('enter_value'),
system: {
interface: 'text-input' /** @TODO base on selected datatype */,
width: 'half',
options: {
placeholder: i18n.t('enter_value'),
},
},
},
{
field: 'length',
name: i18n.t('length'),
interface: 'numeric',
width: 'half',
options: null,
system: {
interface: 'numeric',
width: 'half',
options: null,
},
},
{
field: 'required',
name: i18n.t('required'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'readonly',
name: i18n.t('readonly'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'hidden_detail',
name: i18n.t('hide_on_detail'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'hidden_browse',
name: i18n.t('hide_on_browse'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'unique',
name: i18n.t('unique'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'primary_key',
name: i18n.t('primary_key'),
interface: 'toggle',
width: 'half',
options: null,
system: {
interface: 'toggle',
width: 'half',
options: null,
},
},
{
field: 'validation',
name: i18n.t('validation_regex'),
interface: 'text-input',
width: 'half',
options: {
font: 'monospace',
placeholder: 'eg: /^[A-Z]+$/',
system: {
interface: 'text-input',
width: 'half',
options: {
font: 'monospace',
placeholder: 'eg: /^[A-Z]+$/',
},
},
},
{
field: 'validation_message',
name: i18n.t('validation_message'),
interface: 'text-input',
width: 'half',
system: {
interface: 'text-input',
width: 'half',
},
},
{
field: 'type',
name: i18n.t('directus_type'),
interface: 'dropdown',
width: 'half',
options: {
choices: typeChoices.value,
system: {
interface: 'dropdown',
width: 'half',
options: {
choices: typeChoices.value,
},
},
},
{
field: 'datatype',
name: i18n.t('database_type'),
interface: 'text-input',
width: 'half',
system: {
interface: 'text-input',
width: 'half',
},
},
];

View File

@@ -2,7 +2,10 @@ import FieldSetup from './field-setup.vue';
import { types, Type } from '@/stores/fields/types';
import { LocalType } from './types';
const localTypeGroups: Record<LocalType, Type[]> = {
/**
* @todo fix local type groups in settings
*/
const localTypeGroups: Record<LocalType, string[]> = {
relational: ['m2o', 'o2m', 'm2m', 'translation'],
file: ['file'],
files: ['files'],

View File

@@ -13,11 +13,11 @@ export default function useValidation(field: Ref<Field>, localType: Ref<LocalTyp
});
const interfaceComplete = computed<boolean>(() => {
return notEmpty(field.value.interface);
return notEmpty(field.value.system.interface);
});
const displayComplete = computed<boolean>(() => {
return notEmpty(field.value.display);
return notEmpty(field.value.system.display);
});
const schemaComplete = computed<boolean>(() => {

View File

@@ -105,15 +105,15 @@ export default defineComponent({
const sortedVisibleFields = computed(() =>
sortBy(
[...fields.value].filter(({ hidden_detail }) => hidden_detail === false),
(field) => field.sort || Infinity
[...fields.value].filter((field) => field.system.hidden_detail === false),
(field) => field.system.sort || Infinity
)
);
const sortedHiddenFields = computed(() =>
sortBy(
[...fields.value].filter(({ hidden_detail }) => hidden_detail === true),
(field) => field.sort || Infinity
[...fields.value].filter((field) => field.system.hidden_detail === true),
(field) => field.system.sort || Infinity
)
);
@@ -157,9 +157,10 @@ export default defineComponent({
const fieldsInGroup = location === 'visible' ? sortedVisibleFields.value : sortedHiddenFields.value;
const updates: Partial<Field>[] = fieldsInGroup.slice(newIndex).map((field) => {
const updates: DeepPartial<Field>[] = fieldsInGroup.slice(newIndex).map((field) => {
const sortValue =
field.sort || fieldsInGroup.findIndex((existingField) => existingField.field === field.field);
field.system.sort ||
fieldsInGroup.findIndex((existingField) => existingField.field === field.field);
return {
field: field.field,
@@ -169,11 +170,11 @@ export default defineComponent({
const addedToEnd = newIndex === fieldsInGroup.length;
let newSortValue = fieldsInGroup[newIndex]?.sort;
let newSortValue = fieldsInGroup[newIndex]?.system.sort;
if (!newSortValue && addedToEnd) {
const previousItem = fieldsInGroup[newIndex - 1];
if (previousItem && previousItem.sort) newSortValue = previousItem.sort + 1;
if (previousItem && previousItem.system.sort) newSortValue = previousItem.system.sort + 1;
}
if (!newSortValue) {
@@ -182,8 +183,10 @@ export default defineComponent({
updates.push({
field: element.field,
sort: newSortValue,
hidden_detail: location === 'hidden',
system: {
hidden_detail: location === 'hidden',
sort: newSortValue,
},
});
fieldsStore.updateFields(element.collection, updates);
@@ -197,11 +200,11 @@ export default defineComponent({
const fields = location === 'visible' ? sortedVisibleFields.value : sortedHiddenFields.value;
const updates: Partial<Field>[] = fields.slice(...selectionRange).map((field) => {
const updates: DeepPartial<Field>[] = fields.slice(...selectionRange).map((field) => {
// If field.sort isn't set yet, base it on the index of the array. That way, the
// new sort value will match what's visible on the screen
const sortValue =
field.sort || fields.findIndex((existingField) => existingField.field === field.field);
field.system.sort || fields.findIndex((existingField) => existingField.field === field.field);
return {
field: field.field,
@@ -209,10 +212,12 @@ export default defineComponent({
};
});
const sortOfItemOnNewIndex = fields[newIndex].sort || newIndex;
const sortOfItemOnNewIndex = fields[newIndex].system.sort || newIndex;
updates.push({
field: element.field,
sort: sortOfItemOnNewIndex,
system: {
sort: sortOfItemOnNewIndex,
},
});
fieldsStore.updateFields(element.collection, updates);

View File

@@ -251,9 +251,9 @@ export default defineComponent({
};
const statuses = computed<Status[] | null>(() => {
if (statusField.value && statusField.value.options) {
return Object.keys(statusField.value.options.status_mapping).map((key: string) => ({
...statusField.value?.options?.status_mapping[key],
if (statusField.value && statusField.value.system.options) {
return Object.keys(statusField.value.system.options.status_mapping).map((key: string) => ({
...statusField.value?.system.options?.status_mapping[key],
value: key,
}));
}

View File

@@ -1,6 +1,6 @@
<template>
<v-list nav>
<v-list-item :to="`/${project}/users/all`">
<v-list-item to="/users/all">
<v-list-item-icon><v-icon name="people" /></v-list-item-icon>
<v-list-item-content>{{ $t('all_users') }}</v-list-item-content>
</v-list-item>
@@ -13,7 +13,7 @@
</v-list-item>
</template>
<v-list-item v-for="{ name, id } in roles" :key="id" :to="`/${project}/users/${id}`">
<v-list-item v-for="{ name, id } in roles" :key="id" :to="`/users/${id}`">
<v-list-item-icon><v-icon name="people" /></v-list-item-icon>
<v-list-item-content>{{ name }}</v-list-item-content>
</v-list-item>

38
src/shims.d.ts vendored
View File

@@ -42,3 +42,41 @@ declare module 'js-yaml' {
const x: any;
export default x;
}
type Primitive = string | number | boolean | bigint | symbol | undefined | null;
type Builtin = Primitive | Function | Date | Error | RegExp;
type IsTuple<T> = T extends [infer A]
? T
: T extends [infer A, infer B]
? T
: T extends [infer A, infer B, infer C]
? T
: T extends [infer A, infer B, infer C, infer D]
? T
: T extends [infer A, infer B, infer C, infer D, infer E]
? T
: never;
type DeepPartial<T> = T extends Primitive | Builtin
? T
: T extends Map<infer K, infer V>
? Map<DeepPartial<K>, DeepPartial<V>>
: T extends ReadonlyMap<infer K, infer V>
? ReadonlyMap<DeepPartial<K>, DeepPartial<V>>
: T extends WeakMap<infer K, infer V>
? WeakMap<DeepPartial<K>, DeepPartial<V>>
: T extends Set<infer U>
? Set<DeepPartial<U>>
: T extends ReadonlySet<infer U>
? ReadonlySet<DeepPartial<U>>
: T extends WeakSet<infer U>
? WeakSet<DeepPartial<U>>
: T extends Array<infer U>
? T extends IsTuple<T>
? { [K in keyof T]?: DeepPartial<T[K]> }
: Array<DeepPartial<U>>
: T extends Promise<infer U>
? Promise<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

View File

@@ -7,23 +7,41 @@ type Translation = {
export type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill';
export type LocalType =
export type Type =
| 'alias'
| 'bigInteger'
| 'binary'
| 'binary'
| 'boolean'
| 'date'
| 'datetime'
| 'decimal'
| 'float'
| 'integer'
| 'json'
| 'string'
| 'text'
| 'time'
| 'timestamp'
| 'unknown';
export const types: Type[] = [
'alias',
'bigInteger',
'binary',
'boolean',
'date',
'datetime',
'decimal',
'float',
'integer',
'json',
'string',
'text',
'time',
'timestamp',
'unknown',
];
export type DatabaseColumn = {
/** @todo import this from knex-schema-inspector when that's launched */
name: string;
@@ -73,6 +91,6 @@ export interface FieldRaw {
export interface Field extends FieldRaw {
name: string | TranslateResult;
type: LocalType;
type: Type;
system: SystemField;
}

View File

@@ -1,51 +1,23 @@
export default function getDefaultInterfaceForType(type: string | null | undefined) {
switch (type) {
case 'datetime':
case 'date':
case 'time':
case 'datetime_created':
case 'datetime_updated':
return 'datetime';
import { Type } from '@/stores/fields/types';
case 'owner':
case 'user_updated':
case 'user':
return 'user';
const defaultInterfaceMap = {
alias: 'text-input',
bigInteger: 'numeric',
binary: 'text-input',
boolean: 'toggle',
date: 'datetime',
datetime: 'datetime',
decimal: 'numeric',
float: 'numeric',
integer: 'numeric',
json: 'json',
string: 'text-input',
text: 'textarea',
time: 'datetime',
timestamp: 'datetime',
unknown: 'text-input',
};
case 'file':
return 'file';
case 'integer':
case 'sort':
case 'decimal':
return 'numeric';
case 'status':
return 'status';
case 'slug':
return 'slug';
case 'm2o':
return 'many-to-one';
case 'json':
return 'code';
case 'array':
return 'tags';
case 'hash':
case 'group':
case 'lang':
case 'translation':
case 'uuid':
case 'string':
case 'alias':
case 'binary':
case 'boolean':
case 'o2m':
default:
return 'text-input';
}
export default function getDefaultInterfaceForType(type: Type) {
return defaultInterfaceMap[type] || 'text-input';
}

View File

@@ -1,9 +1,9 @@
import { LocalType } from '@/stores/fields/types';
import { Type } from '@/stores/fields/types';
/**
* Typemap graciously provided by @gpetrov
*/
const localTypeMap: Record<string, { type: LocalType; useTz?: boolean }> = {
const localTypeMap: Record<string, { type: Type; useTimezone?: boolean }> = {
// Shared
boolean: { type: 'boolean' },
tinyint: { type: 'boolean' },
@@ -47,7 +47,7 @@ const localTypeMap: Record<string, { type: LocalType; useTz?: boolean }> = {
bit: { type: 'boolean' },
smallmoney: { type: 'float' },
money: { type: 'float' },
datetimeoffset: { type: 'datetime', useTz: true },
datetimeoffset: { type: 'datetime', useTimezone: true },
datetime2: { type: 'datetime' },
smalldatetime: { type: 'datetime' },
nchar: { type: 'text' },
@@ -55,6 +55,8 @@ const localTypeMap: Record<string, { type: LocalType; useTz?: boolean }> = {
varbinary: { type: 'binary' },
// Postgres
json: { type: 'json' },
uuid: { type: 'string' },
int2: { type: 'integer' },
serial4: { type: 'integer' },
int4: { type: 'integer' },
@@ -67,16 +69,16 @@ const localTypeMap: Record<string, { type: LocalType; useTz?: boolean }> = {
_varchar: { type: 'string' },
bpchar: { type: 'string' },
timestamptz: { type: 'timestamp' },
'timestamp with time zone': { type: 'timestamp', useTz: true },
'timestamp with time zone': { type: 'timestamp', useTimezone: true },
'timestamp without thime zone': { type: 'timestamp' },
timetz: { type: 'time' },
'time with time zone': { type: 'time', useTz: true },
'time with time zone': { type: 'time', useTimezone: true },
'time without time zone': { type: 'time' },
float4: { type: 'float' },
float8: { type: 'float' },
};
export default function getLocalType(databaseType: string) {
export default function getLocalType(databaseType: string): Type {
const type = localTypeMap[databaseType];
if (type) {