From 67247a2e0e472b612cb1d3ae62dd8fd28a4bc992 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Fri, 11 Sep 2020 14:16:10 -0400 Subject: [PATCH 1/7] Allow overriding v list item content font --- app/src/components/v-list/v-list-item-content.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/components/v-list/v-list-item-content.vue b/app/src/components/v-list/v-list-item-content.vue index 39a97218a5..0ef58f843d 100644 --- a/app/src/components/v-list/v-list-item-content.vue +++ b/app/src/components/v-list/v-list-item-content.vue @@ -7,6 +7,7 @@ @@ -21,6 +22,7 @@ body { align-self: center; padding: var(--v-list-item-content-padding); overflow: hidden; + font-family: var(--v-list-item-content-font-family); .v-list.three-line &, .v-list-item.three-line & { From 3df728fbe56256d2756138ed4bcf5ba358830d53 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Fri, 11 Sep 2020 14:16:26 -0400 Subject: [PATCH 2/7] Allow dynamic m2o registration --- .../components/relationship-m2m.vue | 3 +- .../components/relationship-m2o.vue | 58 ++++++++++++------- .../data-model/field-detail/field-detail.vue | 15 ++++- .../routes/data-model/field-detail/store.ts | 50 ++++++++++++++-- 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2m.vue b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2m.vue index e02040b447..1e82e1106e 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2m.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2m.vue @@ -13,6 +13,7 @@ v-model="junctionCollection" :placeholder="$t('select_one')" :disabled="isExisting" + allow-other />
@@ -111,7 +112,7 @@ export default defineComponent({ value: field.field, disabled: state.relations[0].many_field === field.field || - field.schema?.is_primary_key || + field.schema?.is_primary_key || state.relations[1].many_field === field.field, })); }); diff --git a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2o.vue b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2o.vue index b894e02238..b4ca1aaad5 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2o.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-m2o.vue @@ -8,16 +8,32 @@
{{ $t('related_collection') }}
- + + +
- - - + + + @@ -31,7 +47,7 @@
{{ $t('corresponding_field_name') }}
- + @@ -66,17 +82,21 @@ export default defineComponent({ const collectionsStore = useCollectionsStore(); const fieldsStore = useFieldsStore(); - const { items, relatedPrimary } = useRelation(); + const { items } = useRelation(); const { hasCorresponding, correspondingField, correspondingLabel } = useCorresponding(); + const isNewCollection = computed(() => { + return collectionsStore.getCollection(state.relations[0].one_collection) === null; + }); + return { relations: state.relations, items, - relatedPrimary, hasCorresponding, correspondingField, correspondingLabel, fieldData: state.fieldData, + isNewCollection, }; function useRelation() { @@ -97,13 +117,7 @@ export default defineComponent({ })) ); - const relatedPrimary = computed(() => { - return state.relations[0].one_collection - ? fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].one_collection)?.field - : null; - }); - - return { items, relatedPrimary }; + return { items }; } function useCorresponding() { @@ -175,7 +189,7 @@ export default defineComponent({ gap: 20px 32px; margin-top: 48px; - .v-icon { + .arrow { --v-icon-color: var(--foreground-subdued); position: absolute; @@ -185,6 +199,10 @@ export default defineComponent({ } } +.v-list { + --v-list-item-content-font-family: var(--family-monospace); +} + .v-divider { margin: 48px 0; } diff --git a/app/src/modules/settings/routes/data-model/field-detail/field-detail.vue b/app/src/modules/settings/routes/data-model/field-detail/field-detail.vue index e3ea5445b6..b6c5bc1a40 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/field-detail.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/field-detail.vue @@ -66,8 +66,8 @@ import SetupDisplay from './components/display.vue'; import { i18n } from '@/lang'; import { isEmpty } from 'lodash'; import api from '@/api'; -import { Relation } from '@/types'; -import { useFieldsStore, useRelationsStore } from '@/stores/'; +import { Relation, Collection } from '@/types'; +import { useFieldsStore, useRelationsStore, useCollectionsStore } from '@/stores/'; import { Field } from '@/types'; import router from '@/router'; import useCollection from '@/composables/use-collection'; @@ -99,6 +99,7 @@ export default defineComponent({ }, }, setup(props) { + const collectionsStore = useCollectionsStore(); const fieldsStore = useFieldsStore(); const relationsStore = useRelationsStore(); @@ -190,7 +191,8 @@ export default defineComponent({ state.relations.length === 0 || isEmpty(state.relations[0].many_collection) || isEmpty(state.relations[0].many_field) || - isEmpty(state.relations[0].one_collection) + isEmpty(state.relations[0].one_collection) || + isEmpty(state.relations[0].one_primary) ); } @@ -224,6 +226,12 @@ export default defineComponent({ await api.post(`/fields/${props.collection}`, state.fieldData); } + await Promise.all( + state.newCollections.map((newCollection: Partial) => { + return api.post(`/collections`, newCollection); + }) + ); + await Promise.all( state.newFields.map((newField: Partial) => { return api.post(`/fields/${newField.collection}`, newField); @@ -240,6 +248,7 @@ export default defineComponent({ }) ); + await collectionsStore.hydrate(); await fieldsStore.hydrate(); await relationsStore.hydrate(); diff --git a/app/src/modules/settings/routes/data-model/field-detail/store.ts b/app/src/modules/settings/routes/data-model/field-detail/store.ts index e3b31e02c2..ff8898e863 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/store.ts +++ b/app/src/modules/settings/routes/data-model/field-detail/store.ts @@ -5,9 +5,9 @@ * It's reset every time the modal opens and shouldn't be used outside of the field-detail flow. */ -import { useFieldsStore, useRelationsStore } from '@/stores/'; +import { useFieldsStore, useRelationsStore, useCollectionsStore } from '@/stores/'; import { reactive, watch, computed, ComputedRef } from '@vue/composition-api'; -import { clone } from 'lodash'; +import { clone, throttle } from 'lodash'; import { getInterfaces } from '@/interfaces'; import { getDisplays } from '@/displays'; import { InterfaceConfig } from '@/interfaces/types'; @@ -27,6 +27,7 @@ function initLocalStore( field: string, type: 'standard' | 'file' | 'files' | 'm2o' | 'o2m' | 'm2m' | 'presentation' ) { + const collectionsStore = useCollectionsStore(); const interfaces = getInterfaces(); const displays = getDisplays(); @@ -51,6 +52,7 @@ function initLocalStore( }, }, relations: [], + newCollections: [], newFields: [], }); @@ -141,7 +143,33 @@ function initLocalStore( } if (type === 'm2o') { - if (!isExisting) { + const syncNewCollectionsM2O = throttle(() => { + const collectionName = state.relations[0].one_collection; + + if (collectionExists(collectionName)) { + state.newCollections = []; + } else { + state.newCollections = [ + { + collection: collectionName, + fields: [ + { + field: state.relations[0].one_primary, + type: 'integer', + schema: { + has_auto_increment: true, + }, + system: { + interface: 'text-input', + } + } + ] + } + ]; + } + }, 50); + + if (isExisting === false) { state.relations = [ { many_collection: collection, @@ -165,9 +193,13 @@ function initLocalStore( watch( () => state.relations[0].one_collection, () => { - const field = fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].one_collection); - state.fieldData.type = field.type; - state.relations[0].one_primary = field.field; + if (collectionExists(state.relations[0].one_collection)) { + const field = fieldsStore.getPrimaryKeyFieldForCollection(state.relations[0].one_collection); + state.fieldData.type = field.type; + state.relations[0].one_primary = field.field; + } else { + state.fieldData.type = 'integer'; + } } ); @@ -180,6 +212,8 @@ function initLocalStore( } } ); + + watch([() => state.relations[0].one_collection, () => state.relations[0].one_primary], syncNewCollectionsM2O); } if (type === 'o2m') { @@ -322,6 +356,10 @@ function initLocalStore( } ); } + + function collectionExists(collection: string) { + return collectionsStore.getCollection(collection) !== null; + } } function clearLocalStore() { From 609f609cbaa8053a4dcc1df7a95863f6e454af1d Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Fri, 11 Sep 2020 14:27:58 -0400 Subject: [PATCH 3/7] Fix not being able to delete o2m fields Fixes #222 --- .../routes/data-model/fields/components/field-select.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/modules/settings/routes/data-model/fields/components/field-select.vue b/app/src/modules/settings/routes/data-model/fields/components/field-select.vue index 5ef482a3d8..830b078d46 100644 --- a/app/src/modules/settings/routes/data-model/fields/components/field-select.vue +++ b/app/src/modules/settings/routes/data-model/fields/components/field-select.vue @@ -89,7 +89,7 @@ @@ -126,10 +126,10 @@ - Are you sure you want to delete this field? + {{ $t('delete_field_are_you_sure', { field: field.field }) }} - Cancel - Delete + {{ $t('cancel') }} + {{ $t('delete') }} From 09518f78161f1160494fd4217f36d40385826464 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Fri, 11 Sep 2020 15:02:59 -0400 Subject: [PATCH 4/7] Allow auto-generating o2m collection/field --- api/src/controllers/fields.ts | 17 +++-- api/src/middleware/error-handler.ts | 14 ++-- api/src/services/fields.ts | 4 +- app/src/lang/en-US/index.json | 1 + .../components/relationship-o2m.vue | 74 ++++++++++++++++--- .../data-model/field-detail/field-detail.vue | 2 + .../routes/data-model/field-detail/store.ts | 70 +++++++++++++++++- app/src/styles/_tooltip.scss | 2 +- 8 files changed, 152 insertions(+), 32 deletions(-) diff --git a/api/src/controllers/fields.ts b/api/src/controllers/fields.ts index 53b573aa18..1dd0b697e7 100644 --- a/api/src/controllers/fields.ts +++ b/api/src/controllers/fields.ts @@ -18,7 +18,7 @@ router.get( res.locals.payload = { data: fields || null }; return next(); - }), + }) ); router.get( @@ -30,7 +30,7 @@ router.get( res.locals.payload = { data: fields || null }; return next(); - }), + }) ); router.get( @@ -46,7 +46,7 @@ router.get( res.locals.payload = { data: field || null }; return next(); - }), + }) ); const newFieldSchema = Joi.object({ @@ -67,6 +67,9 @@ router.post( '/:collection', validateCollection, asyncHandler(async (req, res, next) => { + if (!req.body.schema && !req.body.meta) + throw new InvalidPayloadException(`"schema" or "meta" is required`); + const service = new FieldsService({ accountability: req.accountability }); const { error } = newFieldSchema.validate(req.body); @@ -83,7 +86,7 @@ router.post( res.locals.payload = { data: createdField || null }; return next(); - }), + }) ); router.patch( @@ -107,7 +110,7 @@ router.patch( res.locals.payload = { data: results || null }; return next(); - }), + }) ); router.patch( @@ -126,7 +129,7 @@ router.patch( res.locals.payload = { data: updatedField || null }; return next(); - }), + }) ); router.delete( @@ -136,7 +139,7 @@ router.delete( const service = new FieldsService({ accountability: req.accountability }); await service.deleteField(req.params.collection, req.params.field); return next(); - }), + }) ); export default router; diff --git a/api/src/middleware/error-handler.ts b/api/src/middleware/error-handler.ts index ee285a387c..e88c62a973 100644 --- a/api/src/middleware/error-handler.ts +++ b/api/src/middleware/error-handler.ts @@ -27,14 +27,14 @@ const errorHandler: ErrorRequestHandler = (err, req, res, next) => { } for (const err of errors) { - if (err instanceof BaseException) { - if (env.NODE_ENV === 'development') { - err.extensions = { - ...(err.extensions || {}), - stack: err.stack, - }; - } + if (env.NODE_ENV === 'development') { + err.extensions = { + ...(err.extensions || {}), + stack: err.stack, + }; + } + if (err instanceof BaseException) { logger.debug(err); res.status(err.status); diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 3cafb703f6..30c797f62c 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -157,7 +157,9 @@ export default class FieldsService { .where({ collection, field }) .first(); - fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[]; + if (fieldInfo) { + fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[]; + } try { column = await schemaInspector.columnInfo(collection, field); diff --git a/app/src/lang/en-US/index.json b/app/src/lang/en-US/index.json index 7d68a52c9a..f45bcc3ff8 100644 --- a/app/src/lang/en-US/index.json +++ b/app/src/lang/en-US/index.json @@ -232,6 +232,7 @@ "hide_on_browse": "Hide on Browse", "unique": "Unique", "primary_key": "Primary Key", + "foreign_key": "Foreign Key", "validation_regex": "Validation RegEx", "validation_message": "Validation Message", "directus_type": "Directus Type", diff --git a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-o2m.vue b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-o2m.vue index 62de1f907c..9c3ad50f6b 100644 --- a/app/src/modules/settings/routes/data-model/field-detail/components/relationship-o2m.vue +++ b/app/src/modules/settings/routes/data-model/field-detail/components/relationship-o2m.vue @@ -8,21 +8,64 @@
{{ $t('related_collection') }}
- + > + +
- - + :disabled="isExisting" + :placeholder="$t('foreign_key')" + > + + + @@ -118,13 +161,16 @@ export default defineComponent({