From 4ef8cac28ee231afd5db4f9f2140734c4dc78bcb Mon Sep 17 00:00:00 2001 From: e01 Date: Sun, 18 Oct 2020 18:58:32 +0300 Subject: [PATCH 001/639] Assets improvements --- api/src/constants.ts | 32 +++++++++---------- api/src/controllers/assets.ts | 8 ++--- .../database/seeds/03-fields/09-settings.yaml | 15 ++++++++- api/src/services/assets.ts | 8 +++-- api/src/types/assets.ts | 7 ++-- app/.storybook/mock-data/fields.json | 18 +++++++++-- docs/guides/files.md | 13 ++++---- packages/spec/specs/components/setting.yaml | 5 +++ packages/spec/specs/paths/assets/assets.yaml | 15 ++++++--- 9 files changed, 81 insertions(+), 40 deletions(-) diff --git a/api/src/constants.ts b/api/src/constants.ts index c14a251650..0e6f62b393 100644 --- a/api/src/constants.ts +++ b/api/src/constants.ts @@ -3,37 +3,37 @@ import { Transformation } from './types/assets'; export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [ { key: 'system-small-cover', - w: 64, - h: 64, - f: 'cover', + width: 64, + height: 64, + fit: 'cover', }, { key: 'system-small-contain', - w: 64, - f: 'contain', + width: 64, + fit: 'contain', }, { key: 'system-medium-cover', - w: 300, - h: 300, - f: 'cover', + width: 300, + height: 300, + fit: 'cover', }, { key: 'system-medium-contain', - w: 300, - f: 'contain', + width: 300, + fit: 'contain', }, { key: 'system-large-cover', - w: 800, - h: 600, - f: 'cover', + width: 800, + height: 600, + fit: 'cover', }, { key: 'system-large-contain', - w: 800, - f: 'contain', + width: 800, + fit: 'contain', }, ]; -export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'w', 'h', 'f']; +export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'width', 'height', 'fit', 'noupscale']; diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index bd36b4b650..7effe71bf6 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -79,18 +79,17 @@ router.get( ]; // For use in the next request handler - res.locals.shortcuts = [...SYSTEM_ASSET_ALLOW_LIST, assetSettings.storage_asset_presets]; + res.locals.shortcuts = [...SYSTEM_ASSET_ALLOW_LIST, ...assetSettings.storage_asset_presets]; res.locals.transformation = transformation; if (Object.keys(transformation).length === 0) { return next(); } - - if (assetSettings.asset_generation === 'all') { + if (assetSettings.storage_asset_transform === 'all') { if (transformation.key && allKeys.includes(transformation.key as string) === false) throw new InvalidQueryException(`Key "${transformation.key}" isn't configured.`); return next(); - } else if (assetSettings.asset_generation === 'shortcut') { + } else if (assetSettings.storage_asset_transform === 'shortcut') { if (allKeys.includes(transformation.key as string)) return next(); throw new InvalidQueryException( `Only configured shortcuts can be used in asset generation.` @@ -118,6 +117,7 @@ router.get( res.attachment(file.filename_download); res.setHeader('Content-Type', file.type); + res.removeHeader('Content-Disposition'); stream.pipe(res); }) diff --git a/api/src/database/seeds/03-fields/09-settings.yaml b/api/src/database/seeds/03-fields/09-settings.yaml index 3cdc8df1fa..a9ab40b0cd 100644 --- a/api/src/database/seeds/03-fields/09-settings.yaml +++ b/api/src/database/seeds/03-fields/09-settings.yaml @@ -152,6 +152,10 @@ fields: text: Contain (preserve aspect ratio) - value: cover text: Cover (forces exact size) + - value: inside + text: Fit inside + - value: outside + text: Fit outside required: true width: half - field: width @@ -168,6 +172,15 @@ fields: interface: numeric required: true width: half + - field: noupscale + type: boolean + schema: + default_value: false + meta: + interface: toggle + width: half + options: + label: No image upscale - field: quality type: integer name: Quality @@ -180,7 +193,7 @@ fields: min: 0 step: 1 required: true - width: full + width: half template: '{{key}}' special: json sort: 13 diff --git a/api/src/services/assets.ts b/api/src/services/assets.ts index bfbf93f550..81c059885f 100644 --- a/api/src/services/assets.ts +++ b/api/src/services/assets.ts @@ -48,9 +48,11 @@ export class AssetsService { private parseTransformation(transformation: Transformation): ResizeOptions { const resizeOptions: ResizeOptions = {}; - if (transformation.w) resizeOptions.width = Number(transformation.w); - if (transformation.h) resizeOptions.height = Number(transformation.h); - if (transformation.f) resizeOptions.fit = transformation.f; + if (transformation.width) resizeOptions.width = Number(transformation.width); + if (transformation.height) resizeOptions.height = Number(transformation.height); + if (transformation.fit) resizeOptions.fit = transformation.fit; + if (transformation.noupscale) + resizeOptions.withoutEnlargement = Boolean(transformation.noupscale); return resizeOptions; } diff --git a/api/src/types/assets.ts b/api/src/types/assets.ts index 8d832b5b37..29a0fd036b 100644 --- a/api/src/types/assets.ts +++ b/api/src/types/assets.ts @@ -1,8 +1,9 @@ export type Transformation = { key?: string; - w?: number; // width - h?: number; // height - f?: 'cover' | 'contain'; // fit + width?: number; // width + height?: number; // height + fit?: 'cover' | 'contain' | 'inside' | 'outside'; // fit + noupscale?: boolean; // Without Enlargement }; // @NOTE Keys used in Transformation should match ASSET_GENERATION_QUERY_KEYS in constants.ts diff --git a/app/.storybook/mock-data/fields.json b/app/.storybook/mock-data/fields.json index eee4710903..ea2d8697c4 100644 --- a/app/.storybook/mock-data/fields.json +++ b/app/.storybook/mock-data/fields.json @@ -11364,7 +11364,9 @@ "options": { "choices": { "contain": "Contain (preserve aspect ratio)", - "crop": "Crop (forces exact size)" + "crop": "Crop (forces exact size)", + "inside": "Fit inside", + "outside": "Fit outside" } }, "required": true, @@ -11387,6 +11389,18 @@ "type": "integer", "width": "half" }, + { + "default_value": false, + "field": "noupscale", + "interface": "toggle", + "name": "No image upscale", + "required": false, + "type": "boolean", + "width": "half", + "options": { + "label": "No image upscale" + } + }, { "default_value": 80, "field": "quality", @@ -11399,7 +11413,7 @@ }, "required": true, "type": "integer", - "width": "full" + "width": "half" } ], "template": "{{key}}" diff --git a/docs/guides/files.md b/docs/guides/files.md index 43f8ae7645..3d168fe12d 100644 --- a/docs/guides/files.md +++ b/docs/guides/files.md @@ -40,14 +40,15 @@ The **Storage Asset Transform** can be used in conjunction with the presets to f Fetching thumbnails is as easy as adding query parameters to the original file's URL. If a requested thumbnail doesn't yet exist, it is dynamically generated and immediately returned. When requesting a thumbnail, the following parameters are all required. -* **`f`** — The **fit** of the thumbnail, either `crop` or `contain` -* **`w`** — The **width** of the thumbnail in pixels -* **`h`** — The **height** of the thumbnail in pixels -* **`q`** — The **quality** of the thumbnail (`0` to `100`) +* **`fit`** — The **fit** of the thumbnail, either `crop` or `contain` +* **`width`** — The **width** of the thumbnail in pixels +* **`height`** — The **height** of the thumbnail in pixels +* **`quality`** — The **quality** of the thumbnail (`0` to `100`) +* **`noupscale`** — Disable image up-scaling ``` -example.com/assets/?f=&w=&h=&q= -example.com/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?f=crop&w=200&h=200&q=80 +example.com/assets/?fit=&width=&height=&quality= +example.com/assets/1ac73658-8b62-4dea-b6da-529fbc9d01a4?fit=crop&width=200&height=200&quality=80 ``` Alternatively, you can reference a specific thumbnail by its preset key. diff --git a/packages/spec/specs/components/setting.yaml b/packages/spec/specs/components/setting.yaml index ac08973738..c54b21f494 100644 --- a/packages/spec/specs/components/setting.yaml +++ b/packages/spec/specs/components/setting.yaml @@ -71,12 +71,17 @@ properties: enum: - cover - contain + - inside + - outside width: description: Width of the thumbnail. type: integer height: description: Height of the thumbnail. type: integer + noupscale: + description: No image upscale + type: boolean quality: description: Quality of the compression used. type: integer diff --git a/packages/spec/specs/paths/assets/assets.yaml b/packages/spec/specs/paths/assets/assets.yaml index 8a7873a6b7..fca10a170e 100644 --- a/packages/spec/specs/paths/assets/assets.yaml +++ b/packages/spec/specs/paths/assets/assets.yaml @@ -17,23 +17,28 @@ get: description: The key of the asset size configured in settings. schema: type: string - - name: w + - name: width in: query description: Width of the file in pixels. schema: type: integer - - name: h + - name: height in: query description: Height of the file in pixels. schema: type: integer - - name: f + - name: fit in: query description: Fit of the file schema: type: string - enum: [crop, contain] - - name: q + enum: [crop, contain, inside, outside] + - name: noupscale + in: query + description: No image upscale. + schema: + type: boolean + - name: quality in: query description: Quality of compression. schema: From 8e4904f9c970552778d6459176715559b36ebb3a Mon Sep 17 00:00:00 2001 From: e01 Date: Mon, 19 Oct 2020 20:55:34 +0300 Subject: [PATCH 002/639] Use withoutEnlargement for more consistency --- app/package-lock.json | 162 +++++++++++++++++++++--------------------- package-lock.json | 141 ++++++++++++------------------------ 2 files changed, 125 insertions(+), 178 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index cc1b6b1812..7dcd398b6e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -6601,6 +6601,51 @@ "tslint": "^5.20.1", "webpack": "^4.0.0", "yorkie": "^2.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz", + "integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==", + "dev": true, + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + } + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + } } }, "@vue/cli-plugin-unit-jest": { @@ -6740,6 +6785,17 @@ "unique-filename": "^1.1.1" } }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, "cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -6823,6 +6879,18 @@ "graceful-fs": "^4.1.6" } }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "optional": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6936,6 +7004,18 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, + "vue-loader-v16": { + "version": "npm:vue-loader@16.0.0-beta.8", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz", + "integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==", + "dev": true, + "optional": true, + "requires": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -11664,51 +11744,6 @@ } } }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz", - "integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==", - "dev": true, - "optional": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "optional": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - } - } - }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -20342,43 +20377,6 @@ } } }, - "vue-loader-v16": { - "version": "npm:vue-loader@16.0.0-beta.8", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz", - "integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==", - "dev": true, - "optional": true, - "requires": { - "chalk": "^4.1.0", - "hash-sum": "^2.0.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "optional": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } - } - }, "vue-router": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz", diff --git a/package-lock.json b/package-lock.json index a434845797..2bcc1aa84e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8216,13 +8216,6 @@ "supports-color": "^5.3.0" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "optional": true - }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -8260,83 +8253,6 @@ "worker-rpc": "^0.1.0" } }, - "fork-ts-checker-webpack-plugin-v5": { - "version": "npm:fork-ts-checker-webpack-plugin@5.2.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz", - "integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==", - "dev": true, - "optional": true, - "requires": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "optional": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "optional": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "optional": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true, - "optional": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "optional": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "globby": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", @@ -8382,18 +8298,6 @@ } } }, - "schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", - "dev": true, - "optional": true, - "requires": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -18575,6 +18479,51 @@ } } }, + "fork-ts-checker-webpack-plugin-v5": { + "version": "npm:fork-ts-checker-webpack-plugin@5.2.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz", + "integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==", + "dev": true, + "optional": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "optional": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dev": true, + "optional": true, + "requires": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + } + } + } + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", From d776bb826dcd3306f841b4101ebc496fe06571e2 Mon Sep 17 00:00:00 2001 From: e01 Date: Mon, 19 Oct 2020 20:56:14 +0300 Subject: [PATCH 003/639] Use withoutEnlargement for more consistency --- api/src/constants.ts | 2 +- api/src/controllers/assets.ts | 1 - api/src/database/seeds/03-fields/09-settings.yaml | 2 +- api/src/services/assets.ts | 4 ++-- api/src/types/assets.ts | 2 +- app/.storybook/mock-data/fields.json | 2 +- docs/guides/files.md | 3 ++- packages/spec/specs/components/setting.yaml | 2 +- packages/spec/specs/paths/assets/assets.yaml | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/src/constants.ts b/api/src/constants.ts index 0e6f62b393..ac1d7636aa 100644 --- a/api/src/constants.ts +++ b/api/src/constants.ts @@ -36,4 +36,4 @@ export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [ }, ]; -export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'width', 'height', 'fit', 'noupscale']; +export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'width', 'height', 'fit', 'withoutEnlargement']; diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index af069df8d9..17c58815fa 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -117,7 +117,6 @@ router.get( res.attachment(file.filename_download); res.setHeader('Content-Type', file.type); - res.removeHeader('Content-Disposition'); if (req.query.hasOwnProperty('download') === false) { res.removeHeader('Content-Disposition'); diff --git a/api/src/database/seeds/03-fields/09-settings.yaml b/api/src/database/seeds/03-fields/09-settings.yaml index a9ab40b0cd..d2b3bddd07 100644 --- a/api/src/database/seeds/03-fields/09-settings.yaml +++ b/api/src/database/seeds/03-fields/09-settings.yaml @@ -172,7 +172,7 @@ fields: interface: numeric required: true width: half - - field: noupscale + - field: withoutEnlargement type: boolean schema: default_value: false diff --git a/api/src/services/assets.ts b/api/src/services/assets.ts index 81c059885f..c94e7f014c 100644 --- a/api/src/services/assets.ts +++ b/api/src/services/assets.ts @@ -51,8 +51,8 @@ export class AssetsService { if (transformation.width) resizeOptions.width = Number(transformation.width); if (transformation.height) resizeOptions.height = Number(transformation.height); if (transformation.fit) resizeOptions.fit = transformation.fit; - if (transformation.noupscale) - resizeOptions.withoutEnlargement = Boolean(transformation.noupscale); + if (transformation.withoutEnlargement) + resizeOptions.withoutEnlargement = Boolean(transformation.withoutEnlargement); return resizeOptions; } diff --git a/api/src/types/assets.ts b/api/src/types/assets.ts index 29a0fd036b..920acc6137 100644 --- a/api/src/types/assets.ts +++ b/api/src/types/assets.ts @@ -3,7 +3,7 @@ export type Transformation = { width?: number; // width height?: number; // height fit?: 'cover' | 'contain' | 'inside' | 'outside'; // fit - noupscale?: boolean; // Without Enlargement + withoutEnlargement?: boolean; // Without Enlargement }; // @NOTE Keys used in Transformation should match ASSET_GENERATION_QUERY_KEYS in constants.ts diff --git a/app/.storybook/mock-data/fields.json b/app/.storybook/mock-data/fields.json index ea2d8697c4..a21690a0d1 100644 --- a/app/.storybook/mock-data/fields.json +++ b/app/.storybook/mock-data/fields.json @@ -11391,7 +11391,7 @@ }, { "default_value": false, - "field": "noupscale", + "field": "withoutEnlargement", "interface": "toggle", "name": "No image upscale", "required": false, diff --git a/docs/guides/files.md b/docs/guides/files.md index 3d168fe12d..9bd3cf5ad4 100644 --- a/docs/guides/files.md +++ b/docs/guides/files.md @@ -44,7 +44,8 @@ Fetching thumbnails is as easy as adding query parameters to the original file's * **`width`** — The **width** of the thumbnail in pixels * **`height`** — The **height** of the thumbnail in pixels * **`quality`** — The **quality** of the thumbnail (`0` to `100`) -* **`noupscale`** — Disable image up-scaling +* **`withoutEnlargement`** — Disable image up-scaling +* **`download`** — Add `Content-Disposition` header and force browser to download file ``` example.com/assets/?fit=&width=&height=&quality= diff --git a/packages/spec/specs/components/setting.yaml b/packages/spec/specs/components/setting.yaml index c54b21f494..6fde821916 100644 --- a/packages/spec/specs/components/setting.yaml +++ b/packages/spec/specs/components/setting.yaml @@ -79,7 +79,7 @@ properties: height: description: Height of the thumbnail. type: integer - noupscale: + withoutEnlargement: description: No image upscale type: boolean quality: diff --git a/packages/spec/specs/paths/assets/assets.yaml b/packages/spec/specs/paths/assets/assets.yaml index 95a5fb9561..f01c392125 100644 --- a/packages/spec/specs/paths/assets/assets.yaml +++ b/packages/spec/specs/paths/assets/assets.yaml @@ -33,7 +33,7 @@ get: schema: type: string enum: [crop, contain, inside, outside] - - name: noupscale + - name: withoutEnlargement in: query description: No image upscale. schema: From febdd0adbdcb639f72706c3a830786b125d4a672 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 11:02:02 -0400 Subject: [PATCH 004/639] Attempt at resolving joins in nested where queries --- api/src/services/meta.ts | 2 +- api/src/utils/apply-query.ts | 299 ++++++++++++++++++----------------- 2 files changed, 157 insertions(+), 144 deletions(-) diff --git a/api/src/services/meta.ts b/api/src/services/meta.ts index 9764dcdcee..fad8c127b8 100644 --- a/api/src/services/meta.ts +++ b/api/src/services/meta.ts @@ -40,7 +40,7 @@ export class MetaService { const dbQuery = database(collection).count('*', { as: 'count' }); if (query.filter) { - applyFilter(dbQuery, query.filter, collection); + await applyFilter(dbQuery, query.filter, collection); } const records = await dbQuery; diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index d0ead782c3..77bd8adece 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -46,116 +46,179 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild } } -export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collection: string) { - for (const [key, value] of Object.entries(filter)) { - if (key === '_or') { - value.forEach((subFilter: Record) => { - dbQuery.orWhere((subQuery) => applyFilter(subQuery, subFilter, collection)); - }); +export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) { + const relations = await database.select('*').from('directus_relations'); - continue; + parseLevel(rootQuery, rootFilter, collection); + + function parseLevel(dbQuery: QueryBuilder, filter: Filter, collection: string) { + for (const [key, value] of Object.entries(filter)) { + if (key === '_or') { + dbQuery.orWhere((subQuery) => { + value.forEach((subFilter: Record) => { + parseLevel(subQuery, subFilter, collection); + }); + }); + + continue; + } + + if (key === '_and') { + dbQuery.andWhere((subQuery) => { + value.forEach((subFilter: Record) => { + parseLevel(subQuery, subFilter, collection); + }); + }); + + continue; + } + + const filterPath = getFilterPath(key, value); + const { operator: filterOperator, value: filterValue } = getOperation(key, value); + + const column = + filterPath.length > 1 + ? applyJoins(filterPath, collection) + : `${collection}.${filterPath[0]}`; + + applyFilterToQuery(column, filterOperator, filterValue); } - if (key === '_and') { - value.forEach((subFilter: Record) => { - dbQuery.andWhere((subQuery) => applyFilter(subQuery, subFilter, collection)); - }); + function applyFilterToQuery(key: string, operator: string, compareValue: any) { + if (operator === '_eq') { + dbQuery.where({ [key]: compareValue }); + } - continue; + if (operator === '_neq') { + dbQuery.whereNot({ [key]: compareValue }); + } + + if (operator === '_contains') { + dbQuery.where(key, 'like', `%${compareValue}%`); + } + + if (operator === '_ncontains') { + dbQuery.where(key, 'like', `%${compareValue}%`); + } + + if (operator === '_gt') { + dbQuery.where(key, '>', compareValue); + } + + if (operator === '_gte') { + dbQuery.where(key, '>=', compareValue); + } + + if (operator === '_lt') { + dbQuery.where(key, '<', compareValue); + } + + if (operator === '_lte') { + dbQuery.where(key, '<=', compareValue); + } + + if (operator === '_in') { + let value = compareValue; + if (typeof value === 'string') value = value.split(','); + + dbQuery.whereIn(key, value as string[]); + } + + if (operator === '_nin') { + let value = compareValue; + if (typeof value === 'string') value = value.split(','); + + dbQuery.whereNotIn(key, value as string[]); + } + + if (operator === '_null') { + dbQuery.whereNull(key); + } + + if (operator === '_nnull') { + dbQuery.whereNotNull(key); + } + + if (operator === '_empty') { + dbQuery.andWhere((query) => { + query.whereNull(key); + query.orWhere(key, '=', ''); + }); + } + + if (operator === '_nempty') { + dbQuery.andWhere((query) => { + query.whereNotNull(key); + query.orWhere(key, '!=', ''); + }); + } + + if (operator === '_between') { + let value = compareValue; + if (typeof value === 'string') value = value.split(','); + + dbQuery.whereBetween(key, value); + } + + if (operator === '_nbetween') { + let value = compareValue; + if (typeof value === 'string') value = value.split(','); + + dbQuery.whereNotBetween(key, value); + } } - - const filterPath = getFilterPath(key, value); - const { operator: filterOperator, value: filterValue } = getOperation(key, value); - - const column = - filterPath.length > 1 - ? await applyJoins(dbQuery, filterPath, collection) - : `${collection}.${filterPath[0]}`; - - applyFilterToQuery(column, filterOperator, filterValue); } - function applyFilterToQuery(key: string, operator: string, compareValue: any) { - if (operator === '_eq') { - dbQuery.where({ [key]: compareValue }); - } + function applyJoins(path: string[], collection: string) { + path = clone(path); - if (operator === '_neq') { - dbQuery.whereNot({ [key]: compareValue }); - } + let keyName = ''; - if (operator === '_contains') { - dbQuery.where(key, 'like', `%${compareValue}%`); - } + addJoins(path); - if (operator === '_ncontains') { - dbQuery.where(key, 'like', `%${compareValue}%`); - } + return keyName; - if (operator === '_gt') { - dbQuery.where(key, '>', compareValue); - } - - if (operator === '_gte') { - dbQuery.where(key, '>=', compareValue); - } - - if (operator === '_lt') { - dbQuery.where(key, '<', compareValue); - } - - if (operator === '_lte') { - dbQuery.where(key, '<=', compareValue); - } - - if (operator === '_in') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); - - dbQuery.whereIn(key, value as string[]); - } - - if (operator === '_nin') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); - - dbQuery.whereNotIn(key, value as string[]); - } - - if (operator === '_null') { - dbQuery.whereNull(key); - } - - if (operator === '_nnull') { - dbQuery.whereNotNull(key); - } - - if (operator === '_empty') { - dbQuery.andWhere((query) => { - query.whereNull(key); - query.orWhere(key, '=', ''); + function addJoins(pathParts: string[], parentCollection: string = collection) { + const relation = relations.find((relation) => { + return ( + (relation.many_collection === parentCollection && + relation.many_field === pathParts[0]) || + (relation.one_collection === parentCollection && + relation.one_field === pathParts[0]) + ); }); - } - if (operator === '_nempty') { - dbQuery.andWhere((query) => { - query.whereNotNull(key); - query.orWhere(key, '!=', ''); - }); - } + if (!relation) return; - if (operator === '_between') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); + const isM2O = + relation.many_collection === parentCollection && + relation.many_field === pathParts[0]; - dbQuery.whereBetween(key, value); - } + if (isM2O) { + rootQuery.leftJoin( + relation.one_collection!, + `${parentCollection}.${relation.many_field}`, + `${relation.one_collection}.${relation.one_primary}` + ); + } else { + rootQuery.leftJoin( + relation.many_collection, + `${parentCollection}.${relation.one_primary}`, + `${relation.many_collection}.${relation.many_field}` + ); + } - if (operator === '_nbetween') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); + pathParts.shift(); - dbQuery.whereNotBetween(key, value); + const parent = isM2O ? relation.one_collection! : relation.many_collection; + + if (pathParts.length === 1) { + keyName = `${parent}.${pathParts[0]}`; + } + + if (pathParts.length) { + addJoins(pathParts, parent); + } } } } @@ -175,53 +238,3 @@ function getOperation(key: string, value: Record): { operator: stri return { operator: key as string, value }; return getOperation(Object.keys(value)[0], Object.values(value)[0]); } - -async function applyJoins(dbQuery: QueryBuilder, path: string[], collection: string) { - path = clone(path); - - let keyName = ''; - - await addJoins(path); - - return keyName; - - async function addJoins(pathParts: string[], parentCollection: string = collection) { - const relation = await database - .select('*') - .from('directus_relations') - .where({ one_collection: parentCollection, one_field: pathParts[0] }) - .orWhere({ many_collection: parentCollection, many_field: pathParts[0] }) - .first(); - - if (!relation) return; - - const isM2O = - relation.many_collection === parentCollection && relation.many_field === pathParts[0]; - - if (isM2O) { - dbQuery.leftJoin( - relation.one_collection, - `${parentCollection}.${relation.many_field}`, - `${relation.one_collection}.${relation.one_primary}` - ); - } else { - dbQuery.leftJoin( - relation.many_collection, - `${relation.one_collection}.${relation.one_primary}`, - `${relation.many_collection}.${relation.many_field}` - ); - } - - pathParts.shift(); - - const parent = isM2O ? relation.one_collection : relation.many_collection; - - if (pathParts.length === 1) { - keyName = `${parent}.${pathParts[0]}`; - } - - if (pathParts.length) { - await addJoins(pathParts, parent); - } - } -} From 2d12e73871b62e408fed3a242cf6c85d3a720576 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 16:32:10 -0400 Subject: [PATCH 005/639] Fix item loading on first open --- app/src/composables/use-items/use-items.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/composables/use-items/use-items.ts b/app/src/composables/use-items/use-items.ts index efc64617a9..088413af7a 100644 --- a/app/src/composables/use-items/use-items.ts +++ b/app/src/composables/use-items/use-items.ts @@ -20,6 +20,8 @@ type Query = { export function useItems(collection: Ref, query: Query) { const { primaryKeyField, sortField } = useCollection(collection); + let loadingTimeout: any = null; + const { limit, fields, sort, page, filters, searchQuery } = query; const endpoint = computed(() => { @@ -100,8 +102,6 @@ export function useItems(collection: Ref, query: Query) { } }); - let loadingTimeout: any = null; - return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems }; async function getItems() { From cd676119c2e6d77fa7fd82a56404bd331b9689fb Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 16:38:52 -0400 Subject: [PATCH 006/639] Fix rendering assets with no settings --- api/src/controllers/assets.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index 17c58815fa..9331335887 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -79,7 +79,10 @@ router.get( ]; // For use in the next request handler - res.locals.shortcuts = [...SYSTEM_ASSET_ALLOW_LIST, ...assetSettings.storage_asset_presets]; + res.locals.shortcuts = [ + ...SYSTEM_ASSET_ALLOW_LIST, + ...(assetSettings.storage_asset_presets || []), + ]; res.locals.transformation = transformation; if (Object.keys(transformation).length === 0) { From c3f1b271b8d5ebd9bcb37f81655c2f9c5e319e8e Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 16:41:26 -0400 Subject: [PATCH 007/639] Remove unused import --- api/src/controllers/assets.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index 9331335887..f025995ac9 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -9,7 +9,6 @@ import { Transformation } from '../types/assets'; import storage from '../storage'; import { PayloadService, AssetsService } from '../services'; import useCollection from '../middleware/use-collection'; -import { respond } from '../middleware/respond'; const router = Router(); From b529134e4b86c683ce4b5d8dc721058c64c9f15f Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 16:41:31 -0400 Subject: [PATCH 008/639] Add migration for setting change --- .../migrations/20201020A-asset-settings.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 api/src/database/migrations/20201020A-asset-settings.ts diff --git a/api/src/database/migrations/20201020A-asset-settings.ts b/api/src/database/migrations/20201020A-asset-settings.ts new file mode 100644 index 0000000000..80034c045e --- /dev/null +++ b/api/src/database/migrations/20201020A-asset-settings.ts @@ -0,0 +1,193 @@ +import Knex from 'knex'; + +export async function up(knex: Knex) { + await knex('directus_fields') + .update({ + options: { + fields: [ + { + field: 'key', + name: 'Key', + type: 'string', + meta: { + interface: 'slug', + options: { + onlyOnCreate: false, + }, + required: true, + width: 'half', + }, + }, + { + field: 'fit', + name: 'Fit', + type: 'string', + meta: { + interface: 'dropdown', + options: { + choices: [ + { + value: 'contain', + text: 'Contain (preserve aspect ratio)', + }, + { + value: 'cover', + text: 'Cover (forces exact size)', + }, + { + value: 'inside', + text: 'Fit inside', + }, + { + value: 'outside', + text: 'Fit outside', + }, + ], + }, + required: true, + width: 'half', + }, + }, + { + field: 'width', + name: 'Width', + type: 'integer', + meta: { + interface: 'numeric', + required: true, + width: 'half', + }, + }, + { + field: 'height', + name: 'Height', + type: 'integer', + meta: { + interface: 'numeric', + required: true, + width: 'half', + }, + }, + { + field: 'withoutEnlargement', + type: 'boolean', + schema: { + default_value: false, + }, + meta: { + interface: 'toggle', + width: 'half', + options: { + label: 'No image upscale', + }, + }, + }, + { + field: 'quality', + type: 'integer', + name: 'Quality', + schema: { + default_value: 80, + }, + meta: { + interface: 'slider', + options: { + max: 100, + min: 0, + step: 1, + }, + required: true, + width: 'half', + }, + }, + ], + template: '{{key}}', + }, + }) + .where({ field: 'storage_asset_presets', collection: 'directus_settings' }); +} + +export async function down(knex: Knex) { + await knex('directus_fields') + .update({ + options: { + fields: [ + { + field: 'key', + name: 'Key', + type: 'string', + meta: { + interface: 'slug', + options: { + onlyOnCreate: false, + }, + required: true, + width: 'half', + }, + }, + { + field: 'fit', + name: 'Fit', + type: 'string', + meta: { + interface: 'dropdown', + options: { + choices: [ + { + value: 'contain', + text: 'Contain (preserve aspect ratio)', + }, + { + value: 'cover', + text: 'Cover (forces exact size)', + }, + ], + }, + required: true, + width: 'half', + }, + }, + { + field: 'width', + name: 'Width', + type: 'integer', + meta: { + interface: 'numeric', + required: true, + width: 'half', + }, + }, + { + field: 'height', + name: 'Height', + type: 'integer', + meta: { + interface: 'numeric', + required: true, + width: 'half', + }, + }, + { + field: 'quality', + type: 'integer', + name: 'Quality', + schema: { + default_value: 80, + }, + meta: { + interface: 'slider', + options: { + max: 100, + min: 0, + step: 1, + }, + required: true, + width: 'full', + }, + }, + ], + template: '{{key}}', + }, + }) + .where({ field: 'storage_asset_presets', collection: 'directus_settings' }); +} From 86e9bb7d07166b1aa2a2ae3111cd792981a9feff Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 13:02:04 +0200 Subject: [PATCH 009/639] Start on v9 sdk --- packages/sdk-js/package-lock.json | 14 ++++++++++++++ packages/sdk-js/package.json | 24 ++++++++++++++++++++++++ packages/sdk-js/readme.md | 3 +++ packages/sdk-js/src/index.ts | 0 packages/sdk-js/tsconfig.json | 11 +++++++++++ 5 files changed, 52 insertions(+) create mode 100644 packages/sdk-js/package-lock.json create mode 100644 packages/sdk-js/package.json create mode 100644 packages/sdk-js/readme.md create mode 100644 packages/sdk-js/src/index.ts create mode 100644 packages/sdk-js/tsconfig.json diff --git a/packages/sdk-js/package-lock.json b/packages/sdk-js/package-lock.json new file mode 100644 index 0000000000..43281b7af1 --- /dev/null +++ b/packages/sdk-js/package-lock.json @@ -0,0 +1,14 @@ +{ + "name": "@directus/sdk-js", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==", + "dev": true + } + } +} diff --git a/packages/sdk-js/package.json b/packages/sdk-js/package.json new file mode 100644 index 0000000000..b222ef5d4a --- /dev/null +++ b/packages/sdk-js/package.json @@ -0,0 +1,24 @@ +{ + "name": "@directus/sdk-js", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "tsc" + }, + "keywords": [ + "api", + "client", + "cms", + "directus", + "headless", + "javascript", + "node", + "sdk" + ], + "author": "Rijk van Zanten ", + "license": "MIT", + "devDependencies": { + "typescript": "^4.0.3" + } +} diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md new file mode 100644 index 0000000000..14fe3378fb --- /dev/null +++ b/packages/sdk-js/readme.md @@ -0,0 +1,3 @@ +# Directus JS SDK + +[WIP] v9 diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/sdk-js/tsconfig.json b/packages/sdk-js/tsconfig.json new file mode 100644 index 0000000000..503de332b3 --- /dev/null +++ b/packages/sdk-js/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "strict": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} From c2106307b55f28955b7c55a583e820fd22586b32 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:08:00 +0200 Subject: [PATCH 010/639] Start on reference / to-do --- api/src/controllers/assets.ts | 1 - packages/sdk-js/package.json | 3 + packages/sdk-js/readme.md | 239 ++++++++++++++++++++++++++++++++++ packages/sdk-js/src/index.ts | 24 ++++ packages/sdk-js/src/items.ts | 55 ++++++++ packages/sdk-js/src/types.ts | 41 ++++++ 6 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 packages/sdk-js/src/items.ts create mode 100644 packages/sdk-js/src/types.ts diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index 5aa5bc0865..9cdfcfca41 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -9,7 +9,6 @@ import { Transformation } from '../types/assets'; import storage from '../storage'; import { PayloadService, AssetsService } from '../services'; import useCollection from '../middleware/use-collection'; -import { respond } from '../middleware/respond'; const router = Router(); diff --git a/packages/sdk-js/package.json b/packages/sdk-js/package.json index b222ef5d4a..013c467a26 100644 --- a/packages/sdk-js/package.json +++ b/packages/sdk-js/package.json @@ -18,6 +18,9 @@ ], "author": "Rijk van Zanten ", "license": "MIT", + "dependencies": { + "axios": "^0.19.2" + }, "devDependencies": { "typescript": "^4.0.3" } diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md index 14fe3378fb..122ad33be1 100644 --- a/packages/sdk-js/readme.md +++ b/packages/sdk-js/readme.md @@ -1,3 +1,242 @@ # Directus JS SDK [WIP] v9 + +## To-Do + +- [ ] Docs + +- [ ] Items + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Activity + - [ ] Read + - [ ] Create Comment + - [ ] Update Comment + - [ ] Delete Comment +- [ ] Assets??? (not sure if needed) + - [ ] Read +- [ ] Auth + - [ ] Login + - [ ] Refresh + - [ ] Logout + - [ ] Request Password Reset + - [ ] Reset Password + - [ ] oAuth + - [ ] Get available providers + - [ ] Login with provider +- [ ] Collections + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Extensions??? (not sure if needed) + - [ ] Read +- [ ] Fields + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Files + - [ ] Create (upload) + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Folders + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] GraphQL + - [ ] Run +- [ ] Permissions + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Presets + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Relations + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Revisions + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Roles + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete +- [ ] Server + - [ ] Specs + - [ ] OAS + - [ ] Ping + - [ ] Info +- [ ] Settings + - [ ] Read + - [ ] Update +- [ ] Users + - [ ] Create + - [ ] Read + - [ ] Update + - [ ] Delete + - [ ] Invite + - [ ] Accept Invite + - [ ] Enable TFA + - [ ] Disable TFA +- [ ] Utils + - [ ] Get random string + - [ ] Hash a value + - [ ] Verify a hashed value + - [ ] Sort in collection + - [ ] Revert revision + + +## Installation + +``` +npm install @directus/sdk-js +``` + +## Usage + +```js +import DirectusSDK from '@directus/sdk-js'; + +const directus = new DirectusSDK('https://api.example.com/'); + +directus.items('articles').read(15); +``` + +## Docs + +**NOTE** All methods return promises. Make sure to await methods, for example: + +```js +import DirectusSDK from '@directus/sdk-js'; + +const directus = new DirectusSDK('https://api.example.com/'); + +async function getData() { + await directus.auth.login({ email: 'admin@example.com', password: 'password' }); + return await directus.items('articles').read(); +} +``` + +### Global SDK + +#### Initialize + +```js +import DirectusSDK from '@directus/sdk-js'; + +const directus = new DirectusSDK('https://api.example.com/'); +``` + +#### Get / set API URL + +```js +// Get the used API base URL +console.log(directus.url); +// => https://api.example.com/ + +// Set the API base URL +directus.url = 'https://api2.example.com'; +``` + +--- + +### Items + +#### Create + +```js +// Create an item +directus.items('articles').create({ + title: 'My New Article' +}); + +// Create multiple items +directus.items('articles').create([ + { + title: 'My First Article' + }, + { + title: 'My Second Article' + }, +]); +``` + +#### Read + +```js +// Get all +directus.items('articles').read(); + +// Get item by ID (w/ optional query) +directus.items('articles').read(15); +directus.items('articles').read(15, { fields: ['title'] }); + +// Get items by IDs (w/ optional query) +directus.items('articles').read([15, 42]); +directus.items('articles').read([15, 42], { fields: ['title' ]}); + +// Get items by search query params +directus.items('articles').read({ + search: 'Directus', + filter: { + date_published: { + _gte: '$NOW' + } + } +}); +``` + +#### Update + +```js +// Update an item (w/ optional query) +directus.items('articles').update(15, { + title: 'An Updated title' +}); +directus.items('articles').update( + 15, + { title: 'An Updated title' }, + { fields: ['title'] } +); + +// Update multiple items (w/ optional query) +directus.items('articles').update([15, 42], { + title: 'An Updated title' +}); +directus.items('articles').update( + [15, 42], + { title: 'An Updated title' }, + { fields: ['title'] } +); + +// Update by query +directus.items('articles').update( + { title: 'An Updated title' }, + { filter: { title: 'An Old Title' }} +); +``` + +#### Delete + +```js +// Delete an item +directus.items('articles').delete(15); + +// Delete multiple items +directus.items('articles').delete([15, 42]); +``` diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index e69de29bb2..1fe8b50e0d 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -0,0 +1,24 @@ +import axios, { AxiosInstance } from 'axios'; +import { Items } from './items'; + +export default class DirectusSDK { + axios: AxiosInstance; + + constructor(url: string) { + this.axios = axios.create({ + baseURL: url, + }); + } + + get url() { + return this.axios.defaults.baseURL!; + } + + set url(val: string) { + this.axios.defaults.baseURL = val; + } + + items(collection: string) { + return new Items(collection, this.axios); + } +} diff --git a/packages/sdk-js/src/items.ts b/packages/sdk-js/src/items.ts new file mode 100644 index 0000000000..1482e64de6 --- /dev/null +++ b/packages/sdk-js/src/items.ts @@ -0,0 +1,55 @@ +import { Query, Item } from './types'; +import { AxiosInstance } from 'axios'; + +export class Items { + collection: string; + axios: AxiosInstance; + + constructor(collection: string, axios: AxiosInstance) { + this.collection = collection; + this.axios = axios; + } + + async read(query?: Query): Promise; + async read(query: Query & { single: true }): Promise; + async read(key: string | number, query?: Query): Promise; + async read(keys: (string | number)[], query?: Query): Promise; + async read(keys: (string | number)[], query: Query & { single: true }): Promise; + async read( + keysOrQuery?: string | number | (string | number)[] | Query, + query?: Query & { single: boolean } + ): Promise { + let keys: string | number | (string | number)[] | null = null; + + if ( + keysOrQuery && + (Array.isArray(keysOrQuery) || + typeof keysOrQuery === 'string' || + typeof keysOrQuery === 'number') + ) { + keys = keysOrQuery; + } + + let params: Query = {}; + + if (query) { + params = query; + } else if ( + !query && + typeof keysOrQuery === 'object' && + Array.isArray(keysOrQuery) === false + ) { + params = keysOrQuery as Query; + } + + let endpoint = `/items/`; + + if (keys) { + endpoint += keys; + } + + const result = await this.axios.get(endpoint, { params }); + + return result.data; + } +} diff --git a/packages/sdk-js/src/types.ts b/packages/sdk-js/src/types.ts new file mode 100644 index 0000000000..8877572c6f --- /dev/null +++ b/packages/sdk-js/src/types.ts @@ -0,0 +1,41 @@ +export type Item = Record; +export type Payload = Record; + +export enum Meta { + TOTAL_COUNT = 'total_count', + FILTER_COUNT = 'filter_count', +} + +export type Query = { + fields?: string | string[]; + sort?: string; + filter?: Filter; + limit?: number; + offset?: number; + page?: number; + single?: boolean; + meta?: Meta[]; + search?: string; + export?: 'json' | 'csv'; + deep?: Record; +}; + +export type Filter = { + [keyOrOperator: string]: Filter | string | boolean | number | string[]; +}; + +export type FilterOperator = + | '_eq' + | '_neq' + | '_contains' + | '_ncontains' + | '_in' + | '_nin' + | '_gt' + | '_gte' + | '_lt' + | '_lte' + | '_null' + | '_nnull' + | '_empty' + | '_nempty'; From 72c5c32546446b1866e339363468ca2fd48745dc Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:14:25 +0200 Subject: [PATCH 011/639] Add create method --- packages/sdk-js/src/items.ts | 23 +++++++++++++++-------- packages/sdk-js/src/types.ts | 5 +++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/sdk-js/src/items.ts b/packages/sdk-js/src/items.ts index 1482e64de6..3f7b6de302 100644 --- a/packages/sdk-js/src/items.ts +++ b/packages/sdk-js/src/items.ts @@ -1,4 +1,4 @@ -import { Query, Item } from './types'; +import { Query, Item, Payload, Response } from './types'; import { AxiosInstance } from 'axios'; export class Items { @@ -10,15 +10,22 @@ export class Items { this.axios = axios; } - async read(query?: Query): Promise; - async read(query: Query & { single: true }): Promise; - async read(key: string | number, query?: Query): Promise; - async read(keys: (string | number)[], query?: Query): Promise; - async read(keys: (string | number)[], query: Query & { single: true }): Promise; + async create(payload: Payload, query?: Query): Promise>; + async create(payloads: Payload[], query?: Query): Promise>; + async create(payloads: Payload | Payload[], query?: Query): Promise> { + const result = await this.axios.post(`/items/${this.collection}/`, payloads, { + params: query, + }); + return result.data; + } + + async read(query?: Query): Promise>; + async read(key: string | number, query?: Query): Promise>; + async read(keys: (string | number)[], query?: Query): Promise>; async read( keysOrQuery?: string | number | (string | number)[] | Query, query?: Query & { single: boolean } - ): Promise { + ): Promise> { let keys: string | number | (string | number)[] | null = null; if ( @@ -42,7 +49,7 @@ export class Items { params = keysOrQuery as Query; } - let endpoint = `/items/`; + let endpoint = `/items/${this.collection}/`; if (keys) { endpoint += keys; diff --git a/packages/sdk-js/src/types.ts b/packages/sdk-js/src/types.ts index 8877572c6f..17172e7927 100644 --- a/packages/sdk-js/src/types.ts +++ b/packages/sdk-js/src/types.ts @@ -6,6 +6,11 @@ export enum Meta { FILTER_COUNT = 'filter_count', } +export type Response = { + data: T | null; + meta?: Record; +}; + export type Query = { fields?: string | string[]; sort?: string; From 02c0c06ae53d61aeb63a9aea1d39bcff8fbbc850 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:14:45 +0200 Subject: [PATCH 012/639] Update to-do --- packages/sdk-js/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md index 122ad33be1..bc4f01d0dd 100644 --- a/packages/sdk-js/readme.md +++ b/packages/sdk-js/readme.md @@ -7,8 +7,8 @@ - [ ] Docs - [ ] Items - - [ ] Create - - [ ] Read + - [x] Create + - [x] Read - [ ] Update - [ ] Delete - [ ] Activity From be7d544796f019fad7ba3899cb8af9c9f7659a45 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:38:51 +0200 Subject: [PATCH 013/639] Finish items handler --- packages/sdk-js/readme.md | 28 ++++++++++-- packages/sdk-js/src/{ => handlers}/items.ts | 49 ++++++++++++++++++++- packages/sdk-js/src/index.ts | 2 +- 3 files changed, 74 insertions(+), 5 deletions(-) rename packages/sdk-js/src/{ => handlers}/items.ts (50%) diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md index bc4f01d0dd..ece0951fe7 100644 --- a/packages/sdk-js/readme.md +++ b/packages/sdk-js/readme.md @@ -9,8 +9,8 @@ - [ ] Items - [x] Create - [x] Read - - [ ] Update - - [ ] Delete + - [x] Update + - [x] Delete - [ ] Activity - [ ] Read - [ ] Create Comment @@ -214,7 +214,7 @@ directus.items('articles').update( { fields: ['title'] } ); -// Update multiple items (w/ optional query) +// Update multiple items to the same value (w/ optional query) directus.items('articles').update([15, 42], { title: 'An Updated title' }); @@ -224,6 +224,28 @@ directus.items('articles').update( { fields: ['title'] } ); +// Update multiple items to multiple values (w/ optional query) +directus.items('articles').update([ + { + id: 15, + title: 'Article 15', + }, + { + id: 42, + title: 'Article 42', + }, +]); +directus.items('articles').update([ + { + id: 15, + title: 'Article 15', + }, + { + id: 42, + title: 'Article 42', + }, +], { fields: ['title'] }); + // Update by query directus.items('articles').update( { title: 'An Updated title' }, diff --git a/packages/sdk-js/src/items.ts b/packages/sdk-js/src/handlers/items.ts similarity index 50% rename from packages/sdk-js/src/items.ts rename to packages/sdk-js/src/handlers/items.ts index 3f7b6de302..ff855ff8cc 100644 --- a/packages/sdk-js/src/items.ts +++ b/packages/sdk-js/src/handlers/items.ts @@ -1,4 +1,4 @@ -import { Query, Item, Payload, Response } from './types'; +import { Query, Item, Payload, Response } from '../types'; import { AxiosInstance } from 'axios'; export class Items { @@ -10,12 +10,19 @@ export class Items { this.axios = axios; } + /** + * Create a single new item + */ async create(payload: Payload, query?: Query): Promise>; + /** + * Create multiple new items at once + */ async create(payloads: Payload[], query?: Query): Promise>; async create(payloads: Payload | Payload[], query?: Query): Promise> { const result = await this.axios.post(`/items/${this.collection}/`, payloads, { params: query, }); + return result.data; } @@ -59,4 +66,44 @@ export class Items { return result.data; } + + async update(key: string | number, payload: Payload, query?: Query): Promise>; + async update( + keys: (string | number)[], + payload: Payload, + query?: Query + ): Promise>; + async update(payload: Payload[], query?: Query): Promise>; + async update(payload: Payload, query: Query): Promise>; + async update( + keyOrPayload: string | number | (string | number)[] | Payload | Payload[], + payloadOrQuery?: Payload | Query, + query?: Query + ): Promise> { + if ( + typeof keyOrPayload === 'string' || + typeof keyOrPayload === 'number' || + (Array.isArray(keyOrPayload) && + (keyOrPayload as any[]).every((key) => ['string', 'number'].includes(typeof key))) + ) { + const key = keyOrPayload as string | number | (string | number)[]; + const payload = payloadOrQuery as Payload; + + const result = await this.axios.patch(`/items/${this.collection}/${key}`, payload, { + params: query, + }); + return result.data; + } else { + const result = await this.axios.patch(`/items/${this.collection}/`, keyOrPayload, { + params: payloadOrQuery, + }); + return result.data; + } + } + + async delete(key: string | number): Promise; + async delete(keys: (string | number)[]): Promise; + async delete(keys: string | number | (string | number)[]): Promise { + await this.axios.delete(`/items/${this.collection}/${keys}`); + } } diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index 1fe8b50e0d..8aa1b45da6 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance } from 'axios'; -import { Items } from './items'; +import { Items } from './handlers/items'; export default class DirectusSDK { axios: AxiosInstance; From cb5c7eae685c5e7b2d1d82d1e14a0f8287f8357c Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:57:46 +0200 Subject: [PATCH 014/639] Add server and utils --- packages/sdk-js/readme.md | 98 ++++++++++++++++++++++---- packages/sdk-js/src/handlers/index.ts | 3 + packages/sdk-js/src/handlers/items.ts | 6 +- packages/sdk-js/src/handlers/server.ts | 26 +++++++ packages/sdk-js/src/handlers/utils.ts | 36 ++++++++++ packages/sdk-js/src/index.ts | 14 +++- packages/sdk-js/src/types.ts | 1 + 7 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 packages/sdk-js/src/handlers/index.ts create mode 100644 packages/sdk-js/src/handlers/server.ts create mode 100644 packages/sdk-js/src/handlers/utils.ts diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md index ece0951fe7..2302233a5e 100644 --- a/packages/sdk-js/readme.md +++ b/packages/sdk-js/readme.md @@ -4,9 +4,7 @@ ## To-Do -- [ ] Docs - -- [ ] Items +- [x] Items - [x] Create - [x] Read - [x] Update @@ -76,11 +74,11 @@ - [ ] Read - [ ] Update - [ ] Delete -- [ ] Server - - [ ] Specs - - [ ] OAS - - [ ] Ping - - [ ] Info +- [x] Server + - [x] Specs + - [x] OAS + - [x] Ping + - [x] Info - [ ] Settings - [ ] Read - [ ] Update @@ -93,12 +91,12 @@ - [ ] Accept Invite - [ ] Enable TFA - [ ] Disable TFA -- [ ] Utils - - [ ] Get random string - - [ ] Hash a value - - [ ] Verify a hashed value - - [ ] Sort in collection - - [ ] Revert revision +- [x] Utils + - [x] Get random string + - [x] Hash a value + - [x] Verify a hashed value + - [x] Sort in collection + - [x] Revert revision ## Installation @@ -262,3 +260,75 @@ directus.items('articles').delete(15); // Delete multiple items directus.items('articles').delete([15, 42]); ``` + + +--- + +Bunch of others go here + +--- + +### Server + +#### Specs + +##### OAS + +```js +// Get the OAS specs for the current API instance +directus.server.specs.oas(); +``` + +#### Ping + +```js +directus.server.ping(); +``` + +#### Info + +```js +directus.server.info(); +``` + +--- + +Settings + +Users + +--- + +### Utils + +#### Random + +```js +// Get a random string (w/ optional length) +directus.utils.random.string(); +directus.utils.random.string(42); +``` + +#### Hash + +```js +// Generate a hash for a string +directus.utils.hash.generate('Hello World'); + +// Verify if a hash is valid +directus.utils.hash.verify('$argon2.hash', 'Hello World'); +``` + +#### Sort + +```js +// Re-sort a collection based on a start / end position +directus.utils.sort('articles', 15, 42); +``` + +#### Revert + +```js +// Revert a given revision +directus.utils.revert(13); +``` diff --git a/packages/sdk-js/src/handlers/index.ts b/packages/sdk-js/src/handlers/index.ts new file mode 100644 index 0000000000..94151a0330 --- /dev/null +++ b/packages/sdk-js/src/handlers/index.ts @@ -0,0 +1,3 @@ +export * from './items'; +export * from './server'; +export * from './utils'; diff --git a/packages/sdk-js/src/handlers/items.ts b/packages/sdk-js/src/handlers/items.ts index ff855ff8cc..be66f68436 100644 --- a/packages/sdk-js/src/handlers/items.ts +++ b/packages/sdk-js/src/handlers/items.ts @@ -1,9 +1,9 @@ import { Query, Item, Payload, Response } from '../types'; import { AxiosInstance } from 'axios'; -export class Items { - collection: string; - axios: AxiosInstance; +export class ItemsHandler { + private collection: string; + private axios: AxiosInstance; constructor(collection: string, axios: AxiosInstance) { this.collection = collection; diff --git a/packages/sdk-js/src/handlers/server.ts b/packages/sdk-js/src/handlers/server.ts new file mode 100644 index 0000000000..bcc6a1de8d --- /dev/null +++ b/packages/sdk-js/src/handlers/server.ts @@ -0,0 +1,26 @@ +import { AxiosInstance } from 'axios'; + +export class ServerHandler { + private axios: AxiosInstance; + + constructor(axios: AxiosInstance) { + this.axios = axios; + } + + specs = { + oas: async () => { + const result = await this.axios.get('/server/specs/oas'); + return result.data; + }, + }; + + async ping() { + await this.axios.get('/server/ping'); + return 'pong'; + } + + async info() { + const result = await this.axios.get('/server/info'); + return result.data; + } +} diff --git a/packages/sdk-js/src/handlers/utils.ts b/packages/sdk-js/src/handlers/utils.ts new file mode 100644 index 0000000000..fe06e34c94 --- /dev/null +++ b/packages/sdk-js/src/handlers/utils.ts @@ -0,0 +1,36 @@ +import { AxiosInstance } from 'axios'; +import { PrimaryKey } from '../types'; + +export class UtilsHandler { + private axios: AxiosInstance; + + constructor(axios: AxiosInstance) { + this.axios = axios; + } + + random = { + string: async (length: number = 32) => { + const result = await this.axios.get('/utils/random/string', { params: { length } }); + return result.data; + }, + }; + + hash = { + generate: async (string: string) => { + const result = await this.axios.post('/utils/hash/generate', { string }); + return result.data; + }, + verify: async (string: string, hash: string) => { + const result = await this.axios.post('/utils/hash/generate', { string, hash }); + return result.data; + }, + }; + + async sort(collection: string, item: PrimaryKey, to: PrimaryKey) { + await this.axios.post(`/utils/sort/${collection}`, { item, to }); + } + + async revert(revision: PrimaryKey) { + await this.axios.post(`/utils/revert/${revision}`); + } +} diff --git a/packages/sdk-js/src/index.ts b/packages/sdk-js/src/index.ts index 8aa1b45da6..7715d019a5 100644 --- a/packages/sdk-js/src/index.ts +++ b/packages/sdk-js/src/index.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance } from 'axios'; -import { Items } from './handlers/items'; +import { ItemsHandler, ServerHandler, UtilsHandler } from './handlers'; export default class DirectusSDK { axios: AxiosInstance; @@ -19,6 +19,16 @@ export default class DirectusSDK { } items(collection: string) { - return new Items(collection, this.axios); + return new ItemsHandler(collection, this.axios); + } + + get server() { + return new ServerHandler(this.axios); + } + + get utils() { + return new UtilsHandler(this.axios); } } + +const directus = new DirectusSDK('https://example.com'); diff --git a/packages/sdk-js/src/types.ts b/packages/sdk-js/src/types.ts index 17172e7927..cad5c69f93 100644 --- a/packages/sdk-js/src/types.ts +++ b/packages/sdk-js/src/types.ts @@ -1,5 +1,6 @@ export type Item = Record; export type Payload = Record; +export type PrimaryKey = string | number; export enum Meta { TOTAL_COUNT = 'total_count', From edbf68c5811b42deeb41840bbb08502fca4b2223 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:58:19 +0200 Subject: [PATCH 015/639] Use primarykey type --- packages/sdk-js/src/handlers/items.ts | 28 ++++++++++++--------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/sdk-js/src/handlers/items.ts b/packages/sdk-js/src/handlers/items.ts index be66f68436..2728413ceb 100644 --- a/packages/sdk-js/src/handlers/items.ts +++ b/packages/sdk-js/src/handlers/items.ts @@ -1,4 +1,4 @@ -import { Query, Item, Payload, Response } from '../types'; +import { Query, Item, Payload, Response, PrimaryKey } from '../types'; import { AxiosInstance } from 'axios'; export class ItemsHandler { @@ -27,13 +27,13 @@ export class ItemsHandler { } async read(query?: Query): Promise>; - async read(key: string | number, query?: Query): Promise>; - async read(keys: (string | number)[], query?: Query): Promise>; + async read(key: PrimaryKey, query?: Query): Promise>; + async read(keys: PrimaryKey[], query?: Query): Promise>; async read( - keysOrQuery?: string | number | (string | number)[] | Query, + keysOrQuery?: PrimaryKey | PrimaryKey[] | Query, query?: Query & { single: boolean } ): Promise> { - let keys: string | number | (string | number)[] | null = null; + let keys: PrimaryKey | PrimaryKey[] | null = null; if ( keysOrQuery && @@ -67,16 +67,12 @@ export class ItemsHandler { return result.data; } - async update(key: string | number, payload: Payload, query?: Query): Promise>; - async update( - keys: (string | number)[], - payload: Payload, - query?: Query - ): Promise>; + async update(key: PrimaryKey, payload: Payload, query?: Query): Promise>; + async update(keys: PrimaryKey[], payload: Payload, query?: Query): Promise>; async update(payload: Payload[], query?: Query): Promise>; async update(payload: Payload, query: Query): Promise>; async update( - keyOrPayload: string | number | (string | number)[] | Payload | Payload[], + keyOrPayload: PrimaryKey | PrimaryKey[] | Payload | Payload[], payloadOrQuery?: Payload | Query, query?: Query ): Promise> { @@ -86,7 +82,7 @@ export class ItemsHandler { (Array.isArray(keyOrPayload) && (keyOrPayload as any[]).every((key) => ['string', 'number'].includes(typeof key))) ) { - const key = keyOrPayload as string | number | (string | number)[]; + const key = keyOrPayload as PrimaryKey | PrimaryKey[]; const payload = payloadOrQuery as Payload; const result = await this.axios.patch(`/items/${this.collection}/${key}`, payload, { @@ -101,9 +97,9 @@ export class ItemsHandler { } } - async delete(key: string | number): Promise; - async delete(keys: (string | number)[]): Promise; - async delete(keys: string | number | (string | number)[]): Promise { + async delete(key: PrimaryKey): Promise; + async delete(keys: PrimaryKey[]): Promise; + async delete(keys: PrimaryKey | PrimaryKey[]): Promise { await this.axios.delete(`/items/${this.collection}/${keys}`); } } From ea62106d61061ea6030c7b055d1992bd25ef0edd Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 15:58:27 +0200 Subject: [PATCH 016/639] Move generate hash into dedicated endpoint --- api/src/controllers/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/controllers/utils.ts b/api/src/controllers/utils.ts index 646371d93d..9b678e8dc2 100644 --- a/api/src/controllers/utils.ts +++ b/api/src/controllers/utils.ts @@ -24,7 +24,7 @@ router.get( ); router.post( - '/hash', + '/hash/generate', asyncHandler(async (req, res) => { if (!req.body?.string) { throw new InvalidPayloadException(`"string" is required`); From aafc9d95f1252260a8d19d25cb1ed34e336e963f Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 21 Oct 2020 18:21:05 +0200 Subject: [PATCH 017/639] Add note on gql --- packages/sdk-js/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-js/readme.md b/packages/sdk-js/readme.md index 2302233a5e..175515535e 100644 --- a/packages/sdk-js/readme.md +++ b/packages/sdk-js/readme.md @@ -47,7 +47,7 @@ - [ ] Read - [ ] Update - [ ] Delete -- [ ] GraphQL +- [ ] GraphQL??? (not sure if needed) - [ ] Run - [ ] Permissions - [ ] Create From 2dd509e48acda5a40be8e8e3419c3b71ec9a4371 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Thu, 22 Oct 2020 17:26:37 +0200 Subject: [PATCH 018/639] add proper error handling --- app/src/components/v-upload/v-upload.vue | 3 + app/src/composables/use-item/use-item.ts | 43 +- app/src/composables/use-items/use-items.ts | 9 + .../_system/tfa-setup/tfa-setup.vue | 15 + app/src/interfaces/file/file.vue | 9 + app/src/interfaces/files/files.vue | 1 + app/src/interfaces/image/image.vue | 9 +- .../interfaces/many-to-many/use-preview.ts | 10 +- .../interfaces/many-to-one/many-to-one.vue | 16 +- .../interfaces/one-to-many/one-to-many.vue | 10 +- .../interfaces/translations/translations.vue | 16 +- app/src/interfaces/user/user.vue | 15 + app/src/lang/en-US/index.json | 1847 ++++++++--------- app/src/modules/activity/routes/item.vue | 8 + .../components/navigation-bookmark.vue | 16 +- .../modules/collections/routes/collection.vue | 22 +- .../modules/files/components/add-folder.vue | 9 + .../files/components/folder-picker.vue | 9 + .../files/components/navigation-folder.vue | 22 +- .../modules/files/composables/use-folders.ts | 8 + app/src/modules/files/routes/collection.vue | 26 +- app/src/modules/files/routes/item.vue | 33 +- .../settings/composables/use-project-info.ts | 9 + .../data-model/field-detail/field-detail.vue | 14 +- .../fields/components/field-select.vue | 14 +- .../routes/data-model/new-collection.vue | 34 +- .../routes/presets/collection/collection.vue | 9 +- .../presets-info-sidebar-detail.vue | 9 + .../modules/settings/routes/presets/item.vue | 35 +- .../modules/settings/routes/roles/add-new.vue | 19 +- .../settings/routes/roles/collection.vue | 8 + .../permissions-overview-toggle.vue | 21 + .../item/components/permissions-overview.vue | 33 +- .../roles/item/composables/use-permissions.ts | 26 +- .../permissions-detail/components/actions.vue | 13 +- .../permissions-detail/permissions-detail.vue | 15 +- app/src/modules/users/routes/item.vue | 15 +- .../components/continue-as/continue-as.vue | 9 +- .../components/login-form/login-form.vue | 2 +- app/src/routes/login/components/sso-links.vue | 6 +- app/src/routes/reset-password/request.vue | 1 + app/src/routes/reset-password/reset.vue | 1 + app/src/stores/collections.ts | 23 +- app/src/stores/notifications.ts | 54 +- app/src/stores/settings.ts | 15 +- app/src/stores/user.ts | 1 + app/src/types/notifications.ts | 2 + app/src/utils/notify/index.ts | 4 - app/src/utils/notify/notify.test.ts | 23 - app/src/utils/notify/notify.ts | 7 - app/src/utils/upload-file/upload-file.ts | 11 +- app/src/utils/upload-files/upload-files.ts | 9 +- .../comments-sidebar-detail/comment-input.vue | 12 +- .../comment-item-header.vue | 8 + .../comments-sidebar-detail/comment-item.vue | 9 + .../comments-sidebar-detail.vue | 8 + .../components/drawer-item/drawer-item.vue | 26 +- .../file-lightbox/file-lightbox.vue | 14 +- .../components/image-editor/image-editor.vue | 9 + .../components/notification-dialogs/index.ts | 4 + .../notification-dialogs.vue | 100 + .../revisions-drawer-detail.vue | 20 +- .../revisions-drawer.vue | 8 + app/src/views/private/private-view.vue | 3 + 64 files changed, 1606 insertions(+), 1213 deletions(-) delete mode 100644 app/src/utils/notify/index.ts delete mode 100644 app/src/utils/notify/notify.test.ts delete mode 100644 app/src/utils/notify/notify.ts create mode 100644 app/src/views/private/components/notification-dialogs/index.ts create mode 100644 app/src/views/private/components/notification-dialogs/notification-dialogs.vue diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index 0b0d3b69e8..f3c85f1576 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -91,6 +91,8 @@ import uploadFiles from '@/utils/upload-files'; import DrawerCollection from '@/views/private/components/drawer-collection'; import api from '@/api'; import useItem from '@/composables/use-item'; +import { useNotificationsStore } from '@/stores'; +import i18n from '@/lang'; export default defineComponent({ components: { DrawerCollection }, @@ -118,6 +120,7 @@ export default defineComponent({ const { url, isValidURL, loading: urlLoading, error: urlError, importFromURL } = useURLImport(); const { setSelection } = useSelection(); const activeDialog = ref<'choose' | 'url' | null>(null); + const notify = useNotificationsStore(); return { uploading, diff --git a/app/src/composables/use-item/use-item.ts b/app/src/composables/use-item/use-item.ts index ebe5e61b61..5c127128f6 100644 --- a/app/src/composables/use-item/use-item.ts +++ b/app/src/composables/use-item/use-item.ts @@ -1,10 +1,10 @@ import api from '@/api'; import { Ref, ref, watch, computed } from '@vue/composition-api'; -import notify from '@/utils/notify'; import i18n from '@/lang'; import useCollection from '@/composables/use-collection'; import { AxiosResponse } from 'axios'; import { APIError } from '@/types'; +import { useNotificationsStore } from '@/stores'; export function useItem(collection: Ref, primaryKey: Ref) { const { info: collectionInfo, primaryKeyField } = useCollection(collection); @@ -20,6 +20,7 @@ export function useItem(collection: Ref, primaryKey: Ref primaryKey.value === '+'); const isBatch = computed(() => typeof primaryKey.value === 'string' && primaryKey.value.includes(',')); const isSingle = computed(() => !!collectionInfo.value?.meta?.singleton); + const notify = useNotificationsStore(); const isArchived = computed(() => { if (!collectionInfo.value?.meta?.archive_field) return null; @@ -75,6 +76,12 @@ export function useItem(collection: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref, primaryKey: Ref; @@ -19,6 +21,7 @@ type Query = { export function useItems(collection: Ref, query: Query) { const { primaryKeyField, sortField } = useCollection(collection); + const notify = useNotificationsStore(); let loadingTimeout: any = null; @@ -184,6 +187,12 @@ export function useItems(collection: Ref, query: Query) { } } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { clearTimeout(loadingTimeout); loadingTimeout = null; diff --git a/app/src/interfaces/_system/tfa-setup/tfa-setup.vue b/app/src/interfaces/_system/tfa-setup/tfa-setup.vue index 4117470611..825b8bb360 100644 --- a/app/src/interfaces/_system/tfa-setup/tfa-setup.vue +++ b/app/src/interfaces/_system/tfa-setup/tfa-setup.vue @@ -65,6 +65,8 @@ import { defineComponent, ref, watch, onMounted } from '@vue/composition-api'; import api from '@/api'; import qrcode from 'qrcode'; import { nanoid } from 'nanoid'; +import { useNotificationsStore } from '@/stores'; +import i18n from '@/lang'; export default defineComponent({ props: { @@ -74,6 +76,7 @@ export default defineComponent({ }, }, setup(props) { + const notify = useNotificationsStore(); const tfaEnabled = ref(!!props.value); const enableActive = ref(false); const disableActive = ref(false); @@ -133,6 +136,12 @@ export default defineComponent({ error.value = null; } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } @@ -148,6 +157,12 @@ export default defineComponent({ disableActive.value = false; } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/interfaces/file/file.vue b/app/src/interfaces/file/file.vue index f1ef5bd0f6..74307f90bb 100644 --- a/app/src/interfaces/file/file.vue +++ b/app/src/interfaces/file/file.vue @@ -116,6 +116,8 @@ import DrawerCollection from '@/views/private/components/drawer-collection'; import api from '@/api'; import readableMimeType from '@/utils/readable-mime-type'; import getRootPath from '@/utils/get-root-path'; +import i18n from '@/lang'; +import { useNotificationsStore } from '@/stores'; type FileInfo = { id: number; @@ -136,6 +138,7 @@ export default defineComponent({ }, }, setup(props, { emit }) { + const notify = useNotificationsStore(); const activeDialog = ref<'upload' | 'choose' | 'url' | null>(null); const { loading, error, file, fetchFile } = useFile(); @@ -201,6 +204,12 @@ export default defineComponent({ file.value = response.data.data; } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/interfaces/files/files.vue b/app/src/interfaces/files/files.vue index 78daf1b616..5845d5258b 100644 --- a/app/src/interfaces/files/files.vue +++ b/app/src/interfaces/files/files.vue @@ -154,6 +154,7 @@ export default defineComponent({ const { loading, error, items } = usePreview( value, fields, + ref(null), relationInfo, getNewSelectedItems, getUpdatedItems, diff --git a/app/src/interfaces/image/image.vue b/app/src/interfaces/image/image.vue index bf0091918e..b12f5c89e1 100644 --- a/app/src/interfaces/image/image.vue +++ b/app/src/interfaces/image/image.vue @@ -56,7 +56,7 @@ import formatFilesize from '@/utils/format-filesize'; import i18n from '@/lang'; import FileLightbox from '@/views/private/components/file-lightbox'; import ImageEditor from '@/views/private/components/image-editor'; - +import { useNotificationsStore } from '@/stores'; import { nanoid } from 'nanoid'; import getRootPath from '@/utils/get-root-path'; @@ -82,6 +82,7 @@ export default defineComponent({ }, }, setup(props, { emit }) { + const notify = useNotificationsStore(); const loading = ref(false); const image = ref(null); const error = ref(null); @@ -160,6 +161,12 @@ export default defineComponent({ image.value = response.data.data; } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/interfaces/many-to-many/use-preview.ts b/app/src/interfaces/many-to-many/use-preview.ts index 6789424d83..cffd43bfc4 100644 --- a/app/src/interfaces/many-to-many/use-preview.ts +++ b/app/src/interfaces/many-to-many/use-preview.ts @@ -1,10 +1,11 @@ import { Ref, ref, watch, computed } from '@vue/composition-api'; import { Header } from '@/components/v-table/types'; import { RelationInfo } from './use-relation'; -import { useFieldsStore } from '@/stores/'; +import { useFieldsStore, useNotificationsStore } from '@/stores/'; import { Field, Collection } from '@/types'; import api from '@/api'; import { cloneDeep, get } from 'lodash'; +import i18n from '@/lang'; export default function usePreview( value: Ref<(string | number | Record)[] | null>, @@ -19,6 +20,7 @@ export default function usePreview( // Using a ref for the table headers here means that the table itself can update the // values if it needs to. This allows the user to manually resize the columns for example + const notify = useNotificationsStore(); const fieldsStore = useFieldsStore(); const tableHeaders = ref([]); const loading = ref(false); @@ -105,6 +107,12 @@ export default function usePreview( items.value = responseData; } catch (err) { error.value = err; + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/interfaces/many-to-one/many-to-one.vue b/app/src/interfaces/many-to-one/many-to-one.vue index b1ebb701be..5379a9350f 100644 --- a/app/src/interfaces/many-to-one/many-to-one.vue +++ b/app/src/interfaces/many-to-one/many-to-one.vue @@ -103,12 +103,13 @@ + + diff --git a/app/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue b/app/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue index eecace9969..f4c3e70526 100644 --- a/app/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue +++ b/app/src/views/private/components/revisions-drawer-detail/revisions-drawer-detail.vue @@ -48,6 +48,7 @@ import i18n from '@/lang'; import formatLocalized from '@/utils/localized-format'; import RevisionItem from './revision-item.vue'; import RevisionsDrawer from './revisions-drawer.vue'; +import { useNotificationsStore } from '@/stores'; export default defineComponent({ components: { RevisionItem, RevisionsDrawer }, @@ -62,10 +63,8 @@ export default defineComponent({ }, }, setup(props, { emit }) { - const { revisions, revisionsByDate, loading, error, refresh } = useRevisions( - props.collection, - props.primaryKey - ); + const notify = useNotificationsStore(); + const { revisions, revisionsByDate, loading, refresh } = useRevisions(props.collection, props.primaryKey); const hasCreate = computed(() => { // We expect the very first revision record to be a creation @@ -83,7 +82,6 @@ export default defineComponent({ revisions, revisionsByDate, loading, - error, refresh, hasCreate, modalActive, @@ -100,15 +98,13 @@ export default defineComponent({ function useRevisions(collection: string, primaryKey: number | string) { const revisions = ref(null); const revisionsByDate = ref(null); - const error = ref(null); const loading = ref(false); getRevisions(); - return { revisions, revisionsByDate, error, loading, refresh }; + return { revisions, revisionsByDate, loading, refresh }; async function getRevisions() { - error.value = null; loading.value = true; try { @@ -172,7 +168,13 @@ export default defineComponent({ revisionsByDate.value = orderBy(revisionsGrouped, ['date'], ['desc']); revisions.value = orderBy(response.data.data, ['activity.timestamp'], ['desc']); } catch (err) { - error.value = err; + console.error(err); + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/views/private/components/revisions-drawer-detail/revisions-drawer.vue b/app/src/views/private/components/revisions-drawer-detail/revisions-drawer.vue index 1da32cf910..4d1d68bb04 100644 --- a/app/src/views/private/components/revisions-drawer-detail/revisions-drawer.vue +++ b/app/src/views/private/components/revisions-drawer-detail/revisions-drawer.vue @@ -58,6 +58,7 @@ import RevisionsDrawerPicker from './revisions-drawer-picker.vue'; import RevisionsDrawerPreview from './revisions-drawer-preview.vue'; import RevisionsDrawerUpdates from './revisions-drawer-updates.vue'; import api from '@/api'; +import { useNotificationsStore } from '@/stores'; export default defineComponent({ components: { RevisionsDrawerPicker, RevisionsDrawerPreview, RevisionsDrawerUpdates }, @@ -76,6 +77,7 @@ export default defineComponent({ }, }, setup(props, { emit }) { + const notify = useNotificationsStore(); const _active = useSync(props, 'active', emit); const _current = useSync(props, 'current', emit); @@ -127,6 +129,12 @@ export default defineComponent({ emit('revert'); } catch (err) { console.error(err); + notify.add({ + title: i18n.t('unexpected_error'), + type: 'error', + dialog: true, + error: err, + }); } finally { reverting.value = false; } diff --git a/app/src/views/private/private-view.vue b/app/src/views/private/private-view.vue index 878e196ce7..80ff941a5d 100644 --- a/app/src/views/private/private-view.vue +++ b/app/src/views/private/private-view.vue @@ -51,6 +51,7 @@ + @@ -63,6 +64,7 @@ import ProjectInfo from './components/project-info'; import SidebarButton from './components/sidebar-button/'; import NotificationsGroup from './components/notifications-group/'; import NotificationsPreview from './components/notifications-preview/'; +import NotificationDialogs from './components/notification-dialogs/'; import { useUserStore, useAppStore } from '@/stores'; import i18n from '@/lang'; import emitter, { Events } from '@/events'; @@ -76,6 +78,7 @@ export default defineComponent({ SidebarButton, NotificationsGroup, NotificationsPreview, + NotificationDialogs, }, props: { title: { From c8b894aef1ca7852ef30e0c19b689c7c9b790ad8 Mon Sep 17 00:00:00 2001 From: Nitwel Date: Thu, 22 Oct 2020 17:56:31 +0200 Subject: [PATCH 019/639] code tweaks --- app/src/components/v-upload/v-upload.vue | 8 ++---- .../_system/tfa-setup/tfa-setup.vue | 12 --------- app/src/interfaces/file/file.vue | 25 ++++++++++--------- app/src/interfaces/image/image.vue | 5 +--- .../interfaces/many-to-one/many-to-one.vue | 5 ++-- .../interfaces/one-to-many/one-to-many.vue | 6 ++--- .../notification-dialogs.vue | 6 ----- 7 files changed, 20 insertions(+), 47 deletions(-) diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index f3c85f1576..82607a41e8 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -115,7 +115,7 @@ export default defineComponent({ }, }, setup(props, { emit }) { - const { uploading, progress, error, upload, onBrowseSelect, done, numberOfFiles } = useUpload(); + const { uploading, progress, upload, onBrowseSelect, done, numberOfFiles } = useUpload(); const { onDragEnter, onDragLeave, onDrop, dragging } = useDragging(); const { url, isValidURL, loading: urlLoading, error: urlError, importFromURL } = useURLImport(); const { setSelection } = useSelection(); @@ -125,7 +125,6 @@ export default defineComponent({ return { uploading, progress, - error, onDragEnter, onDragLeave, onDrop, @@ -146,14 +145,12 @@ export default defineComponent({ const progress = ref(0); const numberOfFiles = ref(0); const done = ref(0); - const error = ref(null); - return { uploading, progress, error, upload, onBrowseSelect, numberOfFiles, done }; + return { uploading, progress, upload, onBrowseSelect, numberOfFiles, done }; async function upload(files: FileList) { uploading.value = true; progress.value = 0; - error.value = null; try { numberOfFiles.value = files.length; @@ -171,7 +168,6 @@ export default defineComponent({ } } catch (err) { console.error(err); - error.value = err; } finally { uploading.value = false; done.value = 0; diff --git a/app/src/interfaces/_system/tfa-setup/tfa-setup.vue b/app/src/interfaces/_system/tfa-setup/tfa-setup.vue index 825b8bb360..1b3370778b 100644 --- a/app/src/interfaces/_system/tfa-setup/tfa-setup.vue +++ b/app/src/interfaces/_system/tfa-setup/tfa-setup.vue @@ -136,12 +136,6 @@ export default defineComponent({ error.value = null; } catch (err) { error.value = err; - notify.add({ - title: i18n.t('unexpected_error'), - type: 'error', - dialog: true, - error: err, - }); } finally { loading.value = false; } @@ -157,12 +151,6 @@ export default defineComponent({ disableActive.value = false; } catch (err) { error.value = err; - notify.add({ - title: i18n.t('unexpected_error'), - type: 'error', - dialog: true, - error: err, - }); } finally { loading.value = false; } diff --git a/app/src/interfaces/file/file.vue b/app/src/interfaces/file/file.vue index 74307f90bb..b395031bf9 100644 --- a/app/src/interfaces/file/file.vue +++ b/app/src/interfaces/file/file.vue @@ -140,7 +140,7 @@ export default defineComponent({ setup(props, { emit }) { const notify = useNotificationsStore(); const activeDialog = ref<'upload' | 'choose' | 'url' | null>(null); - const { loading, error, file, fetchFile } = useFile(); + const { loading, file, fetchFile } = useFile(); watch(() => props.value, fetchFile, { immediate: true }); @@ -158,20 +158,18 @@ export default defineComponent({ return assetURL.value + `?key=system-small-cover`; }); - const { url, isValidURL, loading: urlLoading, error: urlError, importFromURL } = useURLImport(); + const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport(); return { activeDialog, setSelection, loading, - error, file, fileExtension, imageThumbnail, onUpload, url, urlLoading, - urlError, importFromURL, isValidURL, assetURL, @@ -179,16 +177,14 @@ export default defineComponent({ function useFile() { const loading = ref(false); - const error = ref(null); const file = ref(null); - return { loading, error, file, fetchFile }; + return { loading, file, fetchFile }; async function fetchFile() { if (props.value === null) { file.value = null; loading.value = false; - error.value = null; return; } @@ -203,9 +199,9 @@ export default defineComponent({ file.value = response.data.data; } catch (err) { - error.value = err; + console.error(err); notify.add({ - title: i18n.t('unexpected_error'), + title: i18n.t('could_not_load_file'), type: 'error', dialog: true, error: err, @@ -233,7 +229,6 @@ export default defineComponent({ function useURLImport() { const url = ref(''); const loading = ref(false); - const error = ref(null); const isValidURL = computed(() => { try { @@ -244,7 +239,7 @@ export default defineComponent({ } }); - return { url, loading, error, isValidURL, importFromURL }; + return { url, loading, isValidURL, importFromURL }; async function importFromURL() { loading.value = true; @@ -260,7 +255,13 @@ export default defineComponent({ url.value = ''; emit('input', file.value?.id); } catch (err) { - error.value = err; + console.error(err); + notify.add({ + title: i18n.t('no_file_from_url'), + type: 'error', + dialog: true, + error: err, + }); } finally { loading.value = false; } diff --git a/app/src/interfaces/image/image.vue b/app/src/interfaces/image/image.vue index b12f5c89e1..ee42480061 100644 --- a/app/src/interfaces/image/image.vue +++ b/app/src/interfaces/image/image.vue @@ -85,7 +85,6 @@ export default defineComponent({ const notify = useNotificationsStore(); const loading = ref(false); const image = ref(null); - const error = ref(null); const lightboxActive = ref(false); const editorActive = ref(false); @@ -137,7 +136,6 @@ export default defineComponent({ return { loading, image, - error, src, meta, lightboxActive, @@ -160,7 +158,7 @@ export default defineComponent({ image.value = response.data.data; } catch (err) { - error.value = err; + console.error(err); notify.add({ title: i18n.t('unexpected_error'), type: 'error', @@ -186,7 +184,6 @@ export default defineComponent({ loading.value = false; image.value = null; - error.value = null; lightboxActive.value = false; editorActive.value = false; } diff --git a/app/src/interfaces/many-to-one/many-to-one.vue b/app/src/interfaces/many-to-one/many-to-one.vue index 5379a9350f..dcd238b241 100644 --- a/app/src/interfaces/many-to-one/many-to-one.vue +++ b/app/src/interfaces/many-to-one/many-to-one.vue @@ -194,7 +194,6 @@ export default defineComponent({ function useCurrent() { const currentItem = ref | null>(null); const loading = ref(false); - const error = ref(null); watch( () => props.value, @@ -264,7 +263,7 @@ export default defineComponent({ currentItem.value = response.data.data; } catch (err) { - error.value = err; + console.error(err); notify.add({ title: i18n.t('unexpected_error'), type: 'error', @@ -316,7 +315,7 @@ export default defineComponent({ items.value = response.data.data; } catch (err) { - error.value = err; + console.error(err); notify.add({ title: i18n.t('unexpected_error'), type: 'error', diff --git a/app/src/interfaces/one-to-many/one-to-many.vue b/app/src/interfaces/one-to-many/one-to-many.vue index 9c22626010..de0a55d2ed 100644 --- a/app/src/interfaces/one-to-many/one-to-many.vue +++ b/app/src/interfaces/one-to-many/one-to-many.vue @@ -115,7 +115,7 @@ export default defineComponent({ const notify = useNotificationsStore(); const { relation, relatedCollection, relatedPrimaryKeyField } = useRelation(); - const { tableHeaders, items, loading, error } = useTable(); + const { tableHeaders, items, loading } = useTable(); const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdits(); const { stageSelection, selectModalActive, selectionFilters } = useSelection(); const { sort, sortItems, sortedItems } = useSort(); @@ -263,7 +263,6 @@ export default defineComponent({ const tableHeaders = ref([]); const loading = ref(false); const items = ref[]>([]); - const error = ref(null); watch( () => props.value, @@ -311,7 +310,6 @@ export default defineComponent({ }) .concat(...newItems); } catch (err) { - error.value = err; notify.add({ title: i18n.t('unexpected_error'), type: 'error', @@ -359,7 +357,7 @@ export default defineComponent({ { immediate: true } ); - return { tableHeaders, items, loading, error }; + return { tableHeaders, items, loading }; } function useEdits() { diff --git a/app/src/views/private/components/notification-dialogs/notification-dialogs.vue b/app/src/views/private/components/notification-dialogs/notification-dialogs.vue index 8bc157d376..032c7a7836 100644 --- a/app/src/views/private/components/notification-dialogs/notification-dialogs.vue +++ b/app/src/views/private/components/notification-dialogs/notification-dialogs.vue @@ -50,12 +50,6 @@ Node: ${parsedInfo.value?.node.version} Name: ${notify.error?.name || notify.error || 'none'} Message: ${notify.error?.message || 'none'} -Action: ${notify.text || 'none'} -
- Error Stack - - ${notify.error?.stack?.replace(/^/gm, ' ').replace(' ', '')} -
`; return `https://github.com/directus/next/issues/new?body=${encodeURIComponent(debugInfo)}`; From 53d3ff860e6f377c613bb9d5a15b897057c0090b Mon Sep 17 00:00:00 2001 From: Nicola Krumschmidt Date: Thu, 22 Oct 2020 18:03:07 +0200 Subject: [PATCH 020/639] Enable save options for new items --- app/src/modules/collections/routes/item.vue | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/modules/collections/routes/item.vue b/app/src/modules/collections/routes/item.vue index 4280965d78..4b14711567 100644 --- a/app/src/modules/collections/routes/item.vue +++ b/app/src/modules/collections/routes/item.vue @@ -134,7 +134,7 @@ rounded icon :loading="saving" - :disabled="saveAllowed === false || hasEdits === false" + :disabled="isSavable === false" v-tooltip.bottom="saveAllowed ? $t('save') : $t('not_allowed')" @click="saveAndQuit" > @@ -143,7 +143,7 @@ From ee9baf02c0ac50969376bffd134824a324e87402 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 13:06:37 -0400 Subject: [PATCH 073/639] Move system fields out of DB --- api/src/controllers/auth.ts | 16 +++--- api/src/database/seeds/run.ts | 58 -------------------- api/src/database/system-data/fields/index.ts | 20 +++++++ api/src/services/authorization.ts | 18 ++++-- api/src/services/fields.ts | 13 +++++ api/src/services/payload.ts | 20 ++++--- api/src/utils/get-ast-from-query.ts | 10 +++- api/src/utils/has-fields.ts | 4 ++ 8 files changed, 76 insertions(+), 83 deletions(-) create mode 100644 api/src/database/system-data/fields/index.ts diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index b8a21e79b2..8986ac546b 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -46,15 +46,13 @@ router.post( const ip = req.ip; const userAgent = req.get('user-agent'); - const { accessToken, refreshToken, expires, id } = await authenticationService.authenticate( - { - ip, - userAgent, - email, - password, - otp, - } - ); + const { accessToken, refreshToken, expires } = await authenticationService.authenticate({ + ip, + userAgent, + email, + password, + otp, + }); const payload = { data: { access_token: accessToken, expires }, diff --git a/api/src/database/seeds/run.ts b/api/src/database/seeds/run.ts index b3e36d7ff2..c9c0e93455 100644 --- a/api/src/database/seeds/run.ts +++ b/api/src/database/seeds/run.ts @@ -95,61 +95,3 @@ export default async function runSeed(database: Knex) { }); } } - -// async function insertRows(database: Knex) { -// const rowSeeds = await fse.readdir(path.resolve(__dirname, './02-rows/')); - -// for (const rowSeedFile of rowSeeds) { -// const yamlRaw = await fse.readFile( -// path.resolve(__dirname, './02-rows', rowSeedFile), -// 'utf8' -// ); -// const seedData = yaml.safeLoad(yamlRaw) as RowSeed; - -// const dataWithDefaults = seedData.data.map((row) => { -// for (const [key, value] of Object.entries(row)) { -// if (value !== null && (typeof value === 'object' || Array.isArray(value))) { -// row[key] = JSON.stringify(value); -// } -// } - -// return merge({}, seedData.defaults, row); -// }); - -// await database.batchInsert(seedData.table, dataWithDefaults); -// } -// } - -// async function insertFields(database: Knex) { -// const fieldSeeds = await fse.readdir(path.resolve(__dirname, './03-fields/')); - -// const defaultsYaml = await fse.readFile( -// path.resolve(__dirname, './03-fields/_defaults.yaml'), -// 'utf8' -// ); -// const defaults = yaml.safeLoad(defaultsYaml) as FieldSeed; - -// for (const fieldSeedFile of fieldSeeds) { -// const yamlRaw = await fse.readFile( -// path.resolve(__dirname, './03-fields', fieldSeedFile), -// 'utf8' -// ); -// const seedData = yaml.safeLoad(yamlRaw) as FieldSeed; - -// if (fieldSeedFile === '_defaults.yaml') { -// continue; -// } - -// const dataWithDefaults = seedData.fields.map((row) => { -// for (const [key, value] of Object.entries(row)) { -// if (value !== null && (typeof value === 'object' || Array.isArray(value))) { -// (row as any)[key] = JSON.stringify(value); -// } -// } - -// return merge({}, defaults, row); -// }); - -// await database.batchInsert('directus_fields', dataWithDefaults); -// } -// } diff --git a/api/src/database/system-data/fields/index.ts b/api/src/database/system-data/fields/index.ts new file mode 100644 index 0000000000..18b29b5971 --- /dev/null +++ b/api/src/database/system-data/fields/index.ts @@ -0,0 +1,20 @@ +import { requireYAML } from '../../../utils/require-yaml'; +import { merge } from 'lodash'; +import { FieldMeta } from '../../../types'; +import fse from 'fs-extra'; +import path from 'path'; + +const defaults = requireYAML(require.resolve('./_defaults.yaml')); +const fieldData = fse.readdirSync(path.resolve(__dirname)); + +export let systemFieldRows: FieldMeta[] = []; + +for (const filepath of fieldData) { + if (['_defaults.yaml', 'index.ts'].includes(filepath)) continue; + + const systemFields = requireYAML(path.resolve(__dirname, filepath)); + + for (const field of systemFields.fields) { + systemFieldRows.push(merge({}, defaults, field)); + } +} diff --git a/api/src/services/authorization.ts b/api/src/services/authorization.ts index fe19914912..ae3cdaf9b0 100644 --- a/api/src/services/authorization.ts +++ b/api/src/services/authorization.ts @@ -20,6 +20,7 @@ import { ItemsService } from './items'; import { PayloadService } from './payload'; import { parseFilter } from '../utils/parse-filter'; import { toArray } from '../utils/to-array'; +import { systemFieldRows } from '../database/system-data/fields'; export class AuthorizationService { knex: Knex; @@ -268,13 +269,18 @@ export class AuthorizationService { let requiredColumns: string[] = []; for (const column of columns) { - const field = await this.knex - .select<{ special: string }>('special') - .from('directus_fields') - .where({ collection, field: column.name }) - .first(); + const field = + (await this.knex + .select<{ special: string }>('special') + .from('directus_fields') + .where({ collection, field: column.name }) + .first()) || + systemFieldRows.find( + (fieldMeta) => + fieldMeta.field === column.name && fieldMeta.collection === collection + ); - const specials = (field?.special || '').split(','); + const specials = field?.special ? toArray(field.special) : []; const hasGenerateSpecial = [ 'uuid', diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 69583310ea..8e8110dd54 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -12,6 +12,8 @@ import getDefaultValue from '../utils/get-default-value'; import cache from '../cache'; import SchemaInspector from 'knex-schema-inspector'; +import { systemFieldRows } from '../database/system-data/fields/'; + type RawField = Partial & { field: string; type: typeof types[number] }; export class FieldsService { @@ -38,8 +40,13 @@ export class FieldsService { filter: { collection: { _eq: collection } }, limit: -1, })) as FieldMeta[]; + + fields.push( + ...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection) + ); } else { fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 })) as FieldMeta[]; + fields.push(...systemFieldRows); } let columns = await schemaInspector.columnInfo(collection); @@ -163,6 +170,12 @@ export class FieldsService { fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[]; } + fieldInfo = + fieldInfo || + systemFieldRows.find( + (fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field + ); + try { column = await schemaInspector.columnInfo(collection, field); column.default_value = getDefaultValue(column); diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index a97993673b..31f8143794 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -17,6 +17,8 @@ import getLocalType from '../utils/get-local-type'; import { format, formatISO } from 'date-fns'; import { ForbiddenException } from '../exceptions'; import { toArray } from '../utils/to-array'; +import { FieldMeta } from '../types'; +import { systemFieldRows } from '../database/system-data/fields'; type Action = 'create' | 'read' | 'update'; @@ -148,17 +150,21 @@ export class PayloadService { const fieldsInPayload = Object.keys(processedPayload[0]); - const specialFieldsQuery = this.knex + let specialFieldsInCollection: FieldMeta[] = await this.knex .select('field', 'special') .from('directus_fields') .where({ collection: this.collection }) .whereNotNull('special'); - if (action === 'read') { - specialFieldsQuery.whereIn('field', fieldsInPayload); - } + specialFieldsInCollection.push( + ...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === this.collection) + ); - const specialFieldsInCollection = await specialFieldsQuery; + if (action === 'read') { + specialFieldsInCollection = specialFieldsInCollection.filter((fieldMeta) => { + return fieldsInPayload.includes(fieldMeta.field); + }); + } await Promise.all( processedPayload.map(async (record: any) => { @@ -203,13 +209,13 @@ export class PayloadService { } async processField( - field: { field: string; special: string }, + field: FieldMeta, payload: Partial, action: Action, accountability: Accountability | null ) { if (!field.special) return payload[field.field]; - const fieldSpecials = field.special.split(',').map((s) => s.trim()); + const fieldSpecials = field.special ? toArray(field.special) : []; let value = clone(payload[field.field]); diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index b13dd21c81..5f837b5c6f 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -16,6 +16,7 @@ import { cloneDeep } from 'lodash'; import Knex from 'knex'; import SchemaInspector from 'knex-schema-inspector'; import { getRelationType } from '../utils/get-relation-type'; +import { systemFieldRows } from '../database/system-data/fields'; type GetASTOptions = { accountability?: Accountability | null; @@ -261,9 +262,12 @@ export default async function getASTFromQuery( async function getFieldsInCollection(collection: string) { const columns = (await schemaInspector.columns(collection)).map((column) => column.column); - const fields = ( - await knex.select('field').from('directus_fields').where({ collection }) - ).map((field) => field.field); + const fields = [ + ...(await knex.select('field').from('directus_fields').where({ collection })).map( + (field) => field.field + ), + ...systemFieldRows.map((fieldMeta) => fieldMeta.field), + ]; const fieldsInCollection = [ ...columns, diff --git a/api/src/utils/has-fields.ts b/api/src/utils/has-fields.ts index cbcaf2c387..0eda617800 100644 --- a/api/src/utils/has-fields.ts +++ b/api/src/utils/has-fields.ts @@ -1,5 +1,6 @@ import database, { schemaInspector } from '../database'; import { uniq } from 'lodash'; +import { systemFieldRows } from '../database/system-data/fields'; export default async function hasFields(fields: { collection: string; field: string }[]) { const fieldsObject: { [collection: string]: string[] } = {}; @@ -33,6 +34,9 @@ export async function collectionHasFields(collection: string, fieldKeys: string[ const existingFields = uniq([ ...columns.map(({ column }) => column), ...fields.map(({ field }) => field), + ...systemFieldRows + .filter((fieldMeta) => fieldMeta.collection === collection) + .map((fieldMeta) => fieldMeta.field), ]); for (const key of fieldKeys) { From d3ca132fad6dca90418f88bab966dfbc7047bdfc Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 13:29:07 -0400 Subject: [PATCH 074/639] Move system relations out of db --- .../database/system-data/relations/index.ts | 9 +++++++ .../{ => relations}/relations.yaml | 0 api/src/services/payload.ts | 27 +++++++++++++------ api/src/services/relations.ts | 9 +++++++ api/src/utils/apply-query.ts | 8 ++++-- api/src/utils/get-ast-from-query.ts | 6 ++++- 6 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 api/src/database/system-data/relations/index.ts rename api/src/database/system-data/{ => relations}/relations.yaml (100%) diff --git a/api/src/database/system-data/relations/index.ts b/api/src/database/system-data/relations/index.ts new file mode 100644 index 0000000000..2fb8c5d571 --- /dev/null +++ b/api/src/database/system-data/relations/index.ts @@ -0,0 +1,9 @@ +import { requireYAML } from '../../../utils/require-yaml'; +import { merge } from 'lodash'; +import { Relation } from '../../../types'; + +const systemData = requireYAML(require.resolve('./relations.yaml')); + +export const systemRelationRows: Relation[] = systemData.data.map((row: Record) => { + return merge({}, systemData.defaults, row); +}); diff --git a/api/src/database/system-data/relations.yaml b/api/src/database/system-data/relations/relations.yaml similarity index 100% rename from api/src/database/system-data/relations.yaml rename to api/src/database/system-data/relations/relations.yaml diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index 31f8143794..71e8bac47a 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -19,6 +19,7 @@ import { ForbiddenException } from '../exceptions'; import { toArray } from '../utils/to-array'; import { FieldMeta } from '../types'; import { systemFieldRows } from '../database/system-data/fields'; +import { systemRelationRows } from '../database/system-data/relations'; type Action = 'create' | 'read' | 'update'; @@ -290,10 +291,15 @@ export class PayloadService { async processM2O( payload: Partial | Partial[] ): Promise | Partial[]> { - const relations = await this.knex - .select('*') - .from('directus_relations') - .where({ many_collection: this.collection }); + const relations = [ + ...(await this.knex + .select('*') + .from('directus_relations') + .where({ many_collection: this.collection })), + ...systemRelationRows.filter( + (systemRelation) => systemRelation.many_collection === this.collection + ), + ]; const payloads = clone(Array.isArray(payload) ? payload : [payload]); @@ -340,10 +346,15 @@ export class PayloadService { * Recursively save/update all nested related o2m items */ async processO2M(payload: Partial | Partial[], parent?: PrimaryKey) { - const relations = await this.knex - .select('*') - .from('directus_relations') - .where({ one_collection: this.collection }); + const relations = [ + ...(await this.knex + .select('*') + .from('directus_relations') + .where({ one_collection: this.collection })), + ...systemRelationRows.filter( + (systemRelation) => systemRelation.one_collection === this.collection + ), + ]; const payloads = clone(toArray(payload)); diff --git a/api/src/services/relations.ts b/api/src/services/relations.ts index 9b08c1b964..86bd719507 100644 --- a/api/src/services/relations.ts +++ b/api/src/services/relations.ts @@ -3,6 +3,8 @@ import { AbstractServiceOptions, Query, PrimaryKey, PermissionsAction, Relation import { PermissionsService } from './permissions'; import { toArray } from '../utils/to-array'; +import { systemRelationRows } from '../database/system-data/relations'; + /** * @TODO update foreign key constraints when relations are updated */ @@ -26,6 +28,10 @@ export class RelationsService extends ItemsService { | ParsedRelation[] | null; + if (results && Array.isArray(results)) { + results.push(...(systemRelationRows as ParsedRelation[])); + } + const filteredResults = await this.filterForbidden(results); return filteredResults; @@ -48,6 +54,9 @@ export class RelationsService extends ItemsService { | ParsedRelation[] | null; + // No need to merge system relations here. They don't have PKs so can never be directly + // targetted + const filteredResults = await this.filterForbidden(results); return filteredResults; } diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 754302b246..81aa0b8ae1 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -1,8 +1,9 @@ import { QueryBuilder } from 'knex'; -import { Query, Filter } from '../types'; +import { Query, Filter, Relation } from '../types'; import { schemaInspector } from '../database'; import Knex from 'knex'; import { clone, isPlainObject } from 'lodash'; +import { systemRelationRows } from '../database/system-data/relations'; export default async function applyQuery( knex: Knex, @@ -58,7 +59,10 @@ export async function applyFilter( rootFilter: Filter, collection: string ) { - const relations = await knex.select('*').from('directus_relations'); + const relations: Relation[] = [ + ...(await knex.select('*').from('directus_relations')), + ...systemRelationRows, + ]; addWhereClauses(rootQuery, rootFilter, collection); addJoins(rootQuery, rootFilter, collection); diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index 5f837b5c6f..149c5c5851 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -17,6 +17,7 @@ import Knex from 'knex'; import SchemaInspector from 'knex-schema-inspector'; import { getRelationType } from '../utils/get-relation-type'; import { systemFieldRows } from '../database/system-data/fields'; +import { systemRelationRows } from '../database/system-data/relations'; type GetASTOptions = { accountability?: Accountability | null; @@ -40,7 +41,10 @@ export default async function getASTFromQuery( * we might not need al this info at all times, but it's easier to fetch it all once, than trying to fetch it for every * requested field. @todo look into utilizing graphql/dataloader for this purpose */ - const relations = await knex.select('*').from('directus_relations'); + const relations = [ + ...(await knex.select('*').from('directus_relations')), + ...systemRelationRows, + ]; const permissions = accountability && accountability.admin !== true From 0f729a5d6b0959c26a4b987fed58e0c28326ff0a Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 13:39:39 -0400 Subject: [PATCH 075/639] Move preview defaults to app --- api/src/database/system-data/presets.yaml | 84 ---------------------- app/src/stores/presets.ts | 87 ++++++++++++++++++++++- 2 files changed, 86 insertions(+), 85 deletions(-) delete mode 100644 api/src/database/system-data/presets.yaml diff --git a/api/src/database/system-data/presets.yaml b/api/src/database/system-data/presets.yaml deleted file mode 100644 index df9e909390..0000000000 --- a/api/src/database/system-data/presets.yaml +++ /dev/null @@ -1,84 +0,0 @@ -table: directus_presets - -defaults: - bookmark: null - user: null - role: null - collection: null - search: null - filters: '[]' - layout: tabular - layout_query: null - layout_options: null - -data: - - collection: directus_files - layout: cards - layout_query: - cards: - sort: -uploaded_on - layout_options: - cards: - icon: insert_drive_file - title: '{{ title }}' - subtitle: '{{ type }} • {{ filesize }}' - size: 4 - imageFit: crop - - - collection: directus_users - layout: cards - layout_options: - cards: - icon: account_circle - title: '{{ first_name }} {{ last_name }}' - subtitle: '{{ email }}' - size: 4 - - - collection: directus_activity - layout: tabular - layout_query: - tabular: - sort: -timestamp - fields: - - action - - collection - - timestamp - - user - layout_options: - tabular: - widths: - action: 100 - collection: 210 - timestamp: 240 - user: 240 - - - collection: directus_webhooks - layout: tabular - layout_query: - tabular: - fields: - - status - - name - - method - - url - layout_options: - tabular: - widths: - status: 36 - name: 300 - - - collection: directus_roles - layout: tabular - layout_query: - tabular: - fields: - - icon - - name - - description - layout_options: - tabular: - widths: - icon: 36 - name: 248 - description: 500 - diff --git a/app/src/stores/presets.ts b/app/src/stores/presets.ts index ccd17fe5e3..d7cd5d34b5 100644 --- a/app/src/stores/presets.ts +++ b/app/src/stores/presets.ts @@ -3,6 +3,7 @@ import { Preset } from '@/types'; import { useUserStore } from '@/stores/'; import api from '@/api'; import { nanoid } from 'nanoid'; +import { merge } from 'lodash'; const defaultPreset: Omit = { bookmark: null, @@ -15,6 +16,77 @@ const defaultPreset: Omit = { layout_options: null, }; +const systemDefaults: Record> = { + directus_files: { + collection: 'directus_files', + layout: 'cards', + layout_query: { + cards: { + sort: '-uploaded_on', + }, + }, + layout_options: { + cards: { + icon: 'insert_drive_file', + title: '{{ title }}', + subtitle: '{{ type }} • {{ filesize }}', + size: 4, + imageFit: 'crop', + }, + }, + }, + directus_users: { + collection: 'directus_users', + layout: 'cards', + layout_options: { + cards: { + icon: 'account_circle', + title: '{{ first_name }} {{ last_name }}', + subtitle: '{{ email }}', + size: 4, + }, + }, + }, + directus_activity: { + collection: 'directus_activity', + layout: 'tabular', + layout_query: { + tabular: { + sort: '-timestamp', + fields: ['action', 'collection', 'timestamp', 'user'], + }, + }, + layout_options: { + tabular: { + widths: { + action: 100, + collection: 210, + timestamp: 240, + user: 240, + }, + }, + }, + }, + directus_roles: { + collection: 'directus_roles', + layout: 'tabular', + layout_query: { + tabular: { + fields: ['icon', 'name', 'description'], + }, + }, + layout_options: { + tabular: { + widths: { + icon: 36, + name: 248, + description: 500, + }, + }, + }, + }, +}; + let currentUpdate: Record = {}; export const usePresetsStore = createStore({ @@ -50,7 +122,20 @@ export const usePresetsStore = createStore({ }), ]); - this.state.collectionPresets = values.map((response) => response.data.data).flat(); + const presets = values.map((response) => response.data.data).flat(); + + // Inject system defaults if they don't exist + for (const systemCollection of Object.keys(systemDefaults)) { + const existingGlobalDefault = presets.find((preset) => { + return preset.collection === systemCollection && !preset.user && !preset.role && !preset.bookmark; + }); + + if (!existingGlobalDefault) { + presets.push(merge({}, defaultPreset, systemDefaults[systemCollection])); + } + } + + this.state.collectionPresets = presets; }, async dehydrate() { this.reset(); From bcb4041ff9f4647f813d46475465fe89c4c22b74 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 14:05:36 -0400 Subject: [PATCH 076/639] Add migrations --- .../20201029A-remove-system-relations.ts | 109 ++ ...0201029B-remove-system-collections copy.ts | 80 + .../20201029C-remove-system-fields.ts | 1627 +++++++++++++++++ api/src/database/seeds/04-fields.yaml | 3 - api/src/database/seeds/05-activity.yaml | 3 - api/src/database/seeds/08-permissions.yaml | 3 - api/src/database/seeds/09-presets.yaml | 3 - api/src/database/seeds/10-relations.yaml | 6 - api/src/services/fields.ts | 2 +- 9 files changed, 1817 insertions(+), 19 deletions(-) create mode 100644 api/src/database/migrations/20201029A-remove-system-relations.ts create mode 100644 api/src/database/migrations/20201029B-remove-system-collections copy.ts create mode 100644 api/src/database/migrations/20201029C-remove-system-fields.ts diff --git a/api/src/database/migrations/20201029A-remove-system-relations.ts b/api/src/database/migrations/20201029A-remove-system-relations.ts new file mode 100644 index 0000000000..90b4288cb5 --- /dev/null +++ b/api/src/database/migrations/20201029A-remove-system-relations.ts @@ -0,0 +1,109 @@ +import Knex from 'knex'; + +export async function up(knex: Knex) { + await knex('directus_relations') + .delete() + .where('many_collection', 'like', 'directus_%') + .andWhere('one_collection', 'like', 'directus_%'); +} + +export async function down(knex: Knex) { + const systemRelations = [ + { + many_collection: 'directus_users', + many_field: 'role', + many_primary: 'id', + one_collection: 'directus_roles', + one_field: 'users', + one_primary: 'id', + }, + { + many_collection: 'directus_users', + many_field: 'avatar', + many_primary: 'id', + one_collection: 'directus_files', + one_primary: 'id', + }, + { + many_collection: 'directus_revisions', + many_field: 'activity', + many_primary: 'id', + one_collection: 'directus_activity', + one_field: 'revisions', + one_primary: 'id', + }, + { + many_collection: 'directus_presets', + many_field: 'user', + many_primary: 'id', + one_collection: 'directus_users', + one_primary: 'id', + }, + { + many_collection: 'directus_presets', + many_field: 'role', + many_primary: 'id', + one_collection: 'directus_roles', + one_primary: 'id', + }, + { + many_collection: 'directus_folders', + many_field: 'parent', + many_primary: 'id', + one_collection: 'directus_folders', + one_primary: 'id', + }, + { + many_collection: 'directus_files', + many_field: 'folder', + many_primary: 'id', + one_collection: 'directus_folders', + one_primary: 'id', + }, + { + many_collection: 'directus_files', + many_field: 'uploaded_by', + many_primary: 'id', + one_collection: 'directus_users', + one_primary: 'id', + }, + { + many_collection: 'directus_fields', + many_field: 'collection', + many_primary: 'id', + one_collection: 'directus_collections', + one_field: 'fields', + one_primary: 'collection', + }, + { + many_collection: 'directus_activity', + many_field: 'user', + many_primary: 'id', + one_collection: 'directus_users', + one_primary: 'id', + }, + { + many_collection: 'directus_settings', + many_field: 'project_logo', + many_primary: 'id', + one_collection: 'directus_files', + one_primary: 'id', + }, + { + many_collection: 'directus_settings', + many_field: 'public_foreground', + many_primary: 'id', + one_collection: 'directus_files', + one_primary: 'id', + }, + { + many_collection: 'directus_settings', + many_field: 'public_background', + many_primary: 'id', + one_collection: 'directus_files', + one_primary: 'id', + }, + ]; + + await knex.insert(systemRelations).into('directus_relations'); +} diff --git a/api/src/database/migrations/20201029B-remove-system-collections copy.ts b/api/src/database/migrations/20201029B-remove-system-collections copy.ts new file mode 100644 index 0000000000..fb9569a7cb --- /dev/null +++ b/api/src/database/migrations/20201029B-remove-system-collections copy.ts @@ -0,0 +1,80 @@ +import Knex from 'knex'; + +export async function up(knex: Knex) { + await knex('directus_collections').delete().where('collection', 'like', 'directus_%'); +} + +export async function down(knex: Knex) { + const systemCollections = [ + { + collection: 'directus_activity', + note: 'Accountability logs for all events', + }, + { + collection: 'directus_collections', + icon: 'list_alt', + note: 'Additional collection configuration and metadata', + }, + { + collection: 'directus_fields', + icon: 'input', + note: 'Additional field configuration and metadata', + }, + { + collection: 'directus_files', + icon: 'folder', + note: 'Metadata for all managed file assets', + }, + { + collection: 'directus_folders', + note: 'Provides virtual directories for files', + }, + { + collection: 'directus_permissions', + icon: 'admin_panel_settings', + note: 'Access permissions for each role', + }, + { + collection: 'directus_presets', + icon: 'bookmark_border', + note: 'Presets for collection defaults and bookmarks', + }, + { + collection: 'directus_relations', + icon: 'merge_type', + note: 'Relationship configuration and metadata', + }, + { + collection: 'directus_revisions', + note: 'Data snapshots for all activity', + }, + { + collection: 'directus_roles', + icon: 'supervised_user_circle', + note: 'Permission groups for system users', + }, + { + collection: 'directus_sessions', + note: 'User session information', + }, + { + collection: 'directus_settings', + singleton: true, + note: 'Project configuration options', + }, + { + collection: 'directus_users', + archive_field: 'status', + archive_value: 'archived', + unarchive_value: 'draft', + icon: 'people_alt', + note: 'System users for the platform', + }, + { + collection: 'directus_webhooks', + note: 'Configuration for event-based HTTP requests', + }, + ]; + + await knex.insert(systemCollections).into('directus_collections'); +} diff --git a/api/src/database/migrations/20201029C-remove-system-fields.ts b/api/src/database/migrations/20201029C-remove-system-fields.ts new file mode 100644 index 0000000000..1b4195f177 --- /dev/null +++ b/api/src/database/migrations/20201029C-remove-system-fields.ts @@ -0,0 +1,1627 @@ +import Knex from 'knex'; +import { uniq } from 'lodash'; + +const systemFields = [ + { + collection: 'directus_collections', + field: 'collection_divider', + special: 'alias', + interface: 'divider', + options: { + icon: 'box', + title: 'Collection Setup', + color: '#2F80ED', + }, + locked: true, + sort: 1, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'collection', + interface: 'text-input', + options: { + font: 'monospace', + }, + locked: true, + readonly: true, + sort: 2, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'icon', + interface: 'icon', + options: null, + locked: true, + sort: 3, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'note', + interface: 'text-input', + options: { + placeholder: 'A description of this collection...', + }, + locked: true, + sort: 4, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'display_template', + interface: 'display-template', + options: { + collectionField: 'collection', + }, + locked: true, + sort: 5, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'hidden', + special: 'boolean', + interface: 'toggle', + options: { + label: 'Hide within the App', + }, + locked: true, + sort: 6, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'singleton', + special: 'boolean', + interface: 'toggle', + options: { + label: 'Treat as single object', + }, + locked: true, + sort: 7, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'translations', + special: 'json', + interface: 'repeater', + options: { + template: '{{ translation }} ({{ language }})', + fields: [ + { + field: 'language', + name: 'Language', + type: 'string', + schema: { + default_value: 'en-US', + }, + meta: { + interface: 'system-language', + width: 'half', + }, + }, + { + field: 'translation', + name: 'translation', + type: 'string', + meta: { + interface: 'text-input', + width: 'half', + options: { + placeholder: 'Enter a translation...', + }, + }, + }, + ], + }, + locked: true, + sort: 8, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'archive_divider', + special: 'alias', + interface: 'divider', + options: { + icon: 'archive', + title: 'Archive', + color: '#2F80ED', + }, + locked: true, + sort: 9, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'archive_field', + interface: 'field', + options: { + collectionField: 'collection', + allowNone: true, + placeholder: 'Choose a field...', + }, + locked: true, + sort: 10, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'archive_app_filter', + interface: 'toggle', + special: 'boolean', + options: { + label: 'Enable App Archive Filter', + }, + locked: true, + sort: 11, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'archive_value', + interface: 'text-input', + options: { + font: 'monospace', + iconRight: 'archive', + placeholder: 'Value set when archiving...', + }, + locked: true, + sort: 12, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'unarchive_value', + interface: 'text-input', + options: { + font: 'monospace', + iconRight: 'unarchive', + placeholder: 'Value set when unarchiving...', + }, + locked: true, + sort: 13, + width: 'half', + }, + { + collection: 'directus_collections', + field: 'sort_divider', + special: 'alias', + interface: 'divider', + options: { + icon: 'sort', + title: 'Sort', + color: '#2F80ED', + }, + locked: true, + sort: 14, + width: 'full', + }, + { + collection: 'directus_collections', + field: 'sort_field', + interface: 'field', + options: { + collectionField: 'collection', + placeholder: 'Choose a field...', + typeAllowList: ['float', 'decimal', 'integer'], + allowNone: true, + }, + locked: true, + sort: 15, + width: 'half', + }, + { + collection: 'directus_roles', + field: 'id', + hidden: true, + interface: 'text-input', + locked: true, + special: 'uuid', + }, + { + collection: 'directus_roles', + field: 'name', + interface: 'text-input', + options: { + placeholder: 'The unique name for this role...', + }, + locked: true, + sort: 1, + width: 'half', + }, + { + collection: 'directus_roles', + field: 'icon', + interface: 'icon', + display: 'icon', + locked: true, + sort: 2, + width: 'half', + }, + { + collection: 'directus_roles', + field: 'description', + interface: 'text-input', + options: { + placeholder: 'A description of this role...', + }, + locked: true, + sort: 3, + width: 'full', + }, + { + collection: 'directus_roles', + field: 'app_access', + interface: 'toggle', + locked: true, + special: 'boolean', + sort: 4, + width: 'half', + }, + { + collection: 'directus_roles', + field: 'admin_access', + interface: 'toggle', + locked: true, + special: 'boolean', + sort: 5, + width: 'half', + }, + { + collection: 'directus_roles', + field: 'ip_access', + interface: 'tags', + options: { + placeholder: 'Add allowed IP addresses, leave empty to allow all...', + }, + locked: true, + special: 'csv', + sort: 6, + width: 'full', + }, + { + collection: 'directus_roles', + field: 'enforce_tfa', + interface: 'toggle', + locked: true, + sort: 7, + special: 'boolean', + width: 'half', + }, + { + collection: 'directus_roles', + field: 'users', + interface: 'one-to-many', + locked: true, + special: 'o2m', + sort: 8, + options: { + fields: ['first_name', 'last_name'], + }, + width: 'full', + }, + { + collection: 'directus_roles', + field: 'module_list', + interface: 'repeater', + locked: true, + options: { + template: '{{ name }}', + addLabel: 'Add New Module...', + fields: [ + { + name: 'Icon', + field: 'icon', + type: 'string', + meta: { + interface: 'icon', + width: 'half', + }, + }, + { + name: 'Name', + field: 'name', + type: 'string', + meta: { + interface: 'text-input', + width: 'half', + options: { + iconRight: 'title', + placeholder: 'Enter a title...', + }, + }, + }, + { + name: 'Link', + field: 'link', + type: 'string', + meta: { + interface: 'text-input', + width: 'full', + options: { + iconRight: 'link', + placeholder: 'Relative or absolute URL...', + }, + }, + }, + ], + }, + special: 'json', + sort: 9, + width: 'full', + }, + { + collection: 'directus_roles', + field: 'collection_list', + interface: 'repeater', + locked: true, + options: { + template: '{{ group_name }}', + addLabel: 'Add New Group...', + fields: [ + { + name: 'Group Name', + field: 'group_name', + type: 'string', + meta: { + width: 'half', + interface: 'text-input', + options: { + iconRight: 'title', + placeholder: 'Label this group...', + }, + }, + schema: { + is_nullable: false, + }, + }, + { + name: 'Type', + field: 'accordion', + type: 'string', + schema: { + default_value: 'always_open', + }, + meta: { + width: 'half', + interface: 'dropdown', + options: { + choices: [ + { + value: 'always_open', + text: 'Always Open', + }, + { + value: 'start_open', + text: 'Start Open', + }, + { + value: 'start_collapsed', + text: 'Start Collapsed', + }, + ], + }, + }, + }, + { + name: 'Collections', + field: 'collections', + type: 'JSON', + meta: { + interface: 'repeater', + options: { + addLabel: 'Add New Collection...', + template: '{{ collection }}', + fields: [ + { + name: 'Collection', + field: 'collection', + type: 'string', + meta: { + interface: 'collection', + width: 'full', + }, + schema: { + is_nullable: false, + }, + }, + ], + }, + }, + }, + ], + }, + special: 'json', + sort: 10, + width: 'full', + }, + { + collection: 'directus_fields', + field: 'options', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_fields', + field: 'display_options', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_fields', + field: 'locked', + hidden: true, + locked: true, + special: 'boolean', + }, + { + collection: 'directus_fields', + field: 'readonly', + hidden: true, + locked: true, + special: 'boolean', + }, + { + collection: 'directus_fields', + field: 'hidden', + hidden: true, + locked: true, + special: 'boolean', + }, + { + collection: 'directus_fields', + field: 'special', + hidden: true, + locked: true, + special: 'csv', + }, + { + collection: 'directus_fields', + field: 'translations', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_users', + field: 'first_name', + interface: 'text-input', + locked: true, + options: { + iconRight: 'account_circle', + }, + sort: 1, + width: 'half', + }, + { + collection: 'directus_users', + field: 'last_name', + interface: 'text-input', + locked: true, + options: { + iconRight: 'account_circle', + }, + sort: 2, + width: 'half', + }, + { + collection: 'directus_users', + field: 'email', + interface: 'text-input', + locked: true, + options: { + iconRight: 'email', + }, + sort: 3, + width: 'half', + }, + { + collection: 'directus_users', + field: 'password', + special: 'hash, conceal', + interface: 'hash', + locked: true, + options: { + iconRight: 'lock', + masked: true, + }, + sort: 4, + width: 'half', + }, + { + collection: 'directus_users', + field: 'avatar', + interface: 'file', + locked: true, + sort: 5, + width: 'full', + }, + { + collection: 'directus_users', + field: 'location', + interface: 'text-input', + options: { + iconRight: 'place', + }, + sort: 6, + width: 'half', + }, + { + collection: 'directus_users', + field: 'title', + interface: 'text-input', + options: { + iconRight: 'work', + }, + sort: 7, + width: 'half', + }, + { + collection: 'directus_users', + field: 'description', + interface: 'textarea', + sort: 8, + width: 'full', + }, + { + collection: 'directus_users', + field: 'tags', + interface: 'tags', + special: 'json', + sort: 9, + width: 'full', + options: { + iconRight: 'local_offer', + }, + }, + { + collection: 'directus_users', + field: 'preferences_divider', + interface: 'divider', + options: { + icon: 'face', + title: 'User Preferences', + color: '#2F80ED', + }, + special: 'alias', + sort: 10, + width: 'full', + }, + { + collection: 'directus_users', + field: 'language', + interface: 'dropdown', + locked: true, + options: { + choices: [ + { + text: 'Afrikaans (South Africa)', + value: 'af-ZA', + }, + { + text: 'Arabic (Saudi Arabia)', + value: 'ar-SA', + }, + { + text: 'Catalan (Spain)', + value: 'ca-ES', + }, + { + text: 'Chinese (Simplified)', + value: 'zh-CN', + }, + { + text: 'Czech (Czech Republic)', + value: 'cs-CZ', + }, + { + text: 'Danish (Denmark)', + value: 'da-DK', + }, + { + text: 'Dutch (Netherlands)', + value: 'nl-NL', + }, + { + text: 'English (United States)', + value: 'en-US', + }, + { + text: 'Finnish (Finland)', + value: 'fi-FI', + }, + { + text: 'French (France)', + value: 'fr-FR', + }, + { + text: 'German (Germany)', + value: 'de-DE', + }, + { + text: 'Greek (Greece)', + value: 'el-GR', + }, + { + text: 'Hebrew (Israel)', + value: 'he-IL', + }, + { + text: 'Hungarian (Hungary)', + value: 'hu-HU', + }, + { + text: 'Icelandic (Iceland)', + value: 'is-IS', + }, + { + text: 'Indonesian (Indonesia)', + value: 'id-ID', + }, + { + text: 'Italian (Italy)', + value: 'it-IT', + }, + { + text: 'Japanese (Japan)', + value: 'ja-JP', + }, + { + text: 'Korean (Korea)', + value: 'ko-KR', + }, + { + text: 'Malay (Malaysia)', + value: 'ms-MY', + }, + { + text: 'Norwegian (Norway)', + value: 'no-NO', + }, + { + text: 'Polish (Poland)', + value: 'pl-PL', + }, + { + text: 'Portuguese (Brazil)', + value: 'pt-BR', + }, + { + text: 'Portuguese (Portugal)', + value: 'pt-PT', + }, + { + text: 'Russian (Russian Federation)', + value: 'ru-RU', + }, + { + text: 'Spanish (Spain)', + value: 'es-ES', + }, + { + text: 'Spanish (Latin America)', + value: 'es-419', + }, + { + text: 'Taiwanese Mandarin (Taiwan)', + value: 'zh-TW', + }, + { + text: 'Turkish (Turkey)', + value: 'tr-TR', + }, + { + text: 'Ukrainian (Ukraine)', + value: 'uk-UA', + }, + { + text: 'Vietnamese (Vietnam)', + value: 'vi-VN', + }, + ], + }, + sort: 11, + width: 'half', + }, + { + collection: 'directus_users', + field: 'theme', + interface: 'dropdown', + locked: true, + options: { + choices: [ + { + value: 'auto', + text: 'Automatic (Based on System)', + }, + { + value: 'light', + text: 'Light Mode', + }, + { + value: 'dark', + text: 'Dark Mode', + }, + ], + }, + sort: 12, + width: 'half', + }, + { + collection: 'directus_users', + field: 'tfa_secret', + interface: 'tfa-setup', + locked: true, + special: 'conceal', + sort: 13, + width: 'half', + }, + { + collection: 'directus_users', + field: 'admin_divider', + interface: 'divider', + locked: true, + options: { + icon: 'verified_user', + title: 'Admin Options', + color: '#F2994A', + }, + special: 'alias', + sort: 14, + width: 'full', + }, + { + collection: 'directus_users', + field: 'status', + interface: 'dropdown', + locked: true, + options: { + choices: [ + { + text: 'Draft', + value: 'draft', + }, + { + text: 'Invited', + value: 'invited', + }, + { + text: 'Active', + value: 'active', + }, + { + text: 'Suspended', + value: 'suspended', + }, + { + text: 'Archived', + value: 'archived', + }, + ], + }, + sort: 15, + width: 'half', + }, + { + collection: 'directus_users', + field: 'role', + interface: 'many-to-one', + locked: true, + options: { + template: '{{ name }}', + }, + special: 'm2o', + sort: 16, + width: 'half', + }, + { + collection: 'directus_users', + field: 'token', + interface: 'token', + locked: true, + options: { + iconRight: 'vpn_key', + placeholder: 'Enter a secure access token...', + }, + sort: 17, + width: 'full', + }, + { + collection: 'directus_users', + field: 'id', + special: 'uuid', + interface: 'text-input', + locked: true, + options: { + iconRight: 'vpn_key', + }, + sort: 18, + width: 'full', + }, + { + collection: 'directus_folders', + field: 'id', + interface: 'text-input', + locked: true, + special: 'uuid', + }, + { + collection: 'directus_files', + field: 'id', + hidden: true, + interface: 'text-input', + locked: true, + special: 'uuid', + }, + { + collection: 'directus_files', + field: 'title', + interface: 'text-input', + locked: true, + options: { + iconRight: 'title', + placeholder: 'A unique title...', + }, + sort: 1, + width: 'full', + }, + { + collection: 'directus_files', + field: 'description', + interface: 'textarea', + locked: true, + sort: 2, + width: 'full', + options: { + placeholder: 'An optional description...', + }, + }, + { + collection: 'directus_files', + field: 'tags', + interface: 'tags', + locked: true, + options: { + iconRight: 'local_offer', + }, + special: 'json', + sort: 3, + width: 'full', + display: 'tags', + }, + { + collection: 'directus_files', + field: 'location', + interface: 'text-input', + locked: true, + options: { + iconRight: 'place', + placeholder: 'An optional location...', + }, + sort: 4, + width: 'half', + }, + { + collection: 'directus_files', + field: 'storage', + interface: 'text-input', + locked: true, + options: { + iconRight: 'storage', + }, + sort: 5, + width: 'half', + readonly: true, + }, + { + collection: 'directus_files', + field: 'storage_divider', + interface: 'divider', + locked: true, + options: { + icon: 'insert_drive_file', + title: 'File Naming', + color: '#2F80ED', + }, + special: 'alias', + sort: 6, + width: 'full', + }, + { + collection: 'directus_files', + field: 'filename_disk', + interface: 'text-input', + locked: true, + options: { + iconRight: 'publish', + placeholder: 'Name on disk storage...', + }, + sort: 7, + width: 'half', + }, + { + collection: 'directus_files', + field: 'filename_download', + interface: 'text-input', + locked: true, + options: { + iconRight: 'get_app', + placeholder: 'Name when downloading...', + }, + sort: 8, + width: 'half', + }, + { + collection: 'directus_files', + field: 'metadata', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_files', + field: 'type', + display: 'mime-type', + }, + { + collection: 'directus_files', + field: 'filesize', + display: 'filesize', + }, + { + collection: 'directus_files', + field: 'modified_by', + interface: 'user', + locked: true, + special: 'user-updated', + width: 'half', + display: 'user', + }, + { + collection: 'directus_files', + field: 'modified_on', + interface: 'datetime', + locked: true, + special: 'date-updated', + width: 'half', + display: 'datetime', + }, + { + collection: 'directus_files', + field: 'created_on', + display: 'datetime', + }, + { + collection: 'directus_files', + field: 'created_by', + display: 'user', + }, + { + collection: 'directus_permissions', + field: 'permissions', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_permissions', + field: 'presets', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_presets', + field: 'filters', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_presets', + field: 'layout_query', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_presets', + field: 'layout_options', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_revisions', + field: 'data', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_revisions', + field: 'delta', + hidden: true, + locked: true, + special: 'json', + }, + { + collection: 'directus_settings', + field: 'project_name', + interface: 'text-input', + locked: true, + options: { + iconRight: 'title', + placeholder: 'My project...', + }, + sort: 1, + translations: { + language: 'en-US', + translations: 'Name', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'project_url', + interface: 'text-input', + locked: true, + options: { + iconRight: 'link', + placeholder: 'https://example.com', + }, + sort: 2, + translations: { + language: 'en-US', + translations: 'Website', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'project_color', + interface: 'color', + locked: true, + note: 'Login & Logo Background', + sort: 3, + translations: { + language: 'en-US', + translations: 'Brand Color', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'project_logo', + interface: 'file', + locked: true, + note: 'White 40x40 SVG/PNG', + sort: 4, + translations: { + language: 'en-US', + translations: 'Brand Logo', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'public_divider', + interface: 'divider', + locked: true, + options: { + icon: 'public', + title: 'Public Pages', + color: '#2F80ED', + }, + special: 'alias', + sort: 5, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'public_foreground', + interface: 'file', + locked: true, + sort: 6, + translations: { + language: 'en-US', + translations: 'Login Foreground', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'public_background', + interface: 'file', + locked: true, + sort: 7, + translations: { + language: 'en-US', + translations: 'Login Background', + }, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'public_note', + interface: 'textarea', + locked: true, + options: { + placeholder: 'A short, public message that supports markdown formatting...', + }, + sort: 8, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'security_divider', + interface: 'divider', + locked: true, + options: { + icon: 'security', + title: 'Security', + color: '#2F80ED', + }, + special: 'alias', + sort: 9, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'auth_password_policy', + interface: 'dropdown', + locked: true, + options: { + choices: [ + { + value: null, + text: 'None – Not Recommended', + }, + { + value: '/^.{8,}$/', + text: 'Weak – Minimum 8 Characters', + }, + { + value: + "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/", + text: 'Strong – Upper / Lowercase / Numbers / Special', + }, + ], + }, + sort: 10, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'auth_login_attempts', + interface: 'numeric', + locked: true, + options: { + iconRight: 'lock', + }, + sort: 11, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'files_divider', + interface: 'divider', + locked: true, + options: { + icon: 'storage', + title: 'Files & Thumbnails', + color: '#2F80ED', + }, + special: 'alias', + sort: 12, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'storage_asset_presets', + interface: 'repeater', + locked: true, + options: { + fields: [ + { + field: 'key', + name: 'Key', + type: 'string', + schema: { + is_nullable: false, + }, + meta: { + interface: 'slug', + options: { + onlyOnCreate: false, + }, + width: 'half', + }, + }, + { + field: 'fit', + name: 'Fit', + type: 'string', + schema: { + is_nullable: false, + }, + meta: { + interface: 'dropdown', + options: { + choices: [ + { + value: 'contain', + text: 'Contain (preserve aspect ratio)', + }, + { + value: 'cover', + text: 'Cover (forces exact size)', + }, + ], + }, + width: 'half', + }, + }, + { + field: 'width', + name: 'Width', + type: 'integer', + schema: { + is_nullable: false, + }, + meta: { + interface: 'numeric', + width: 'half', + }, + }, + { + field: 'height', + name: 'Height', + type: 'integer', + schema: { + is_nullable: false, + }, + meta: { + interface: 'numeric', + width: 'half', + }, + }, + { + field: 'quality', + type: 'integer', + name: 'Quality', + schema: { + default_value: 80, + is_nullable: false, + }, + meta: { + interface: 'slider', + options: { + max: 100, + min: 0, + step: 1, + }, + width: 'full', + }, + }, + ], + template: '{{key}}', + }, + special: 'json', + sort: 13, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'storage_asset_transform', + interface: 'dropdown', + locked: true, + options: { + choices: [ + { + value: 'all', + text: 'All', + }, + { + value: 'none', + text: 'None', + }, + { + value: 'presets', + text: 'Presets Only', + }, + ], + }, + sort: 14, + width: 'half', + }, + { + collection: 'directus_settings', + field: 'id', + hidden: true, + locked: true, + }, + { + collection: 'directus_settings', + field: 'overrides_divider', + interface: 'divider', + locked: true, + options: { + icon: 'brush', + title: 'App Overrides', + color: '#2F80ED', + }, + special: 'alias', + sort: 15, + width: 'full', + }, + { + collection: 'directus_settings', + field: 'custom_css', + interface: 'code', + locked: true, + options: { + language: 'css', + lineNumber: true, + }, + sort: 16, + width: 'full', + }, + { + collection: 'directus_webhooks', + field: 'id', + hidden: true, + locked: true, + }, + { + collection: 'directus_webhooks', + field: 'name', + interface: 'text-input', + locked: true, + options: { + iconRight: 'title', + }, + sort: 1, + width: 'full', + }, + { + collection: 'directus_webhooks', + field: 'method', + interface: 'dropdown', + display: 'labels', + display_options: { + defaultBackground: '#ECEFF1', + choices: null, + format: false, + }, + locked: true, + options: { + choices: ['GET', 'POST'], + }, + sort: 2, + width: 'half', + }, + { + collection: 'directus_webhooks', + field: 'url', + interface: 'text-input', + locked: true, + options: { + iconRight: 'link', + }, + sort: 3, + width: 'half', + }, + { + collection: 'directus_webhooks', + field: 'status', + interface: 'dropdown', + display: 'labels', + display_options: { + defaultColor: '#B0BEC5', + defaultBackground: '#ECEFF1', + showAsDot: true, + choices: [ + { + text: 'Active', + value: 'active', + foreground: '#607D8B', + background: '#2F80ED', + }, + { + text: 'Inactive', + value: 'inactive', + foreground: '#607D8B', + background: '#ECEFF1', + }, + ], + }, + locked: true, + options: { + choices: [ + { + text: 'Active', + value: 'active', + }, + { + text: 'Inactive', + value: 'inactive', + }, + ], + }, + sort: 4, + width: 'half', + }, + { + collection: 'directus_webhooks', + field: 'data', + interface: 'toggle', + locked: true, + options: { + label: 'Send Event Data', + }, + special: 'boolean', + sort: 5, + width: 'half', + }, + { + collection: 'directus_webhooks', + field: 'triggers_divider', + interface: 'divider', + options: { + icon: 'api', + title: 'Triggers', + color: '#2F80ED', + }, + special: 'alias', + sort: 6, + width: 'full', + }, + { + collection: 'directus_webhooks', + field: 'actions', + interface: 'checkboxes', + options: { + choices: [ + { + text: 'Create', + value: 'create', + }, + { + text: 'Update', + value: 'update', + }, + { + text: 'Delete', + value: 'delete', + }, + ], + }, + special: 'csv', + sort: 7, + width: 'full', + }, + { + collection: 'directus_webhooks', + field: 'collections', + interface: 'collections', + special: 'csv', + sort: 8, + width: 'full', + }, + { + collection: 'directus_activity', + field: 'action', + display: 'labels', + display_options: { + defaultForeground: '#263238', + defaultBackground: '#eceff1', + choices: [ + { + text: 'Create', + value: 'create', + foreground: '#27ae60', + background: '#c9ebd7', + }, + { + text: 'Update', + value: 'update', + foreground: '#2f80ed', + background: '#cbdffb', + }, + { + text: 'Delete', + value: 'delete', + foreground: '#eb5757', + background: '#fad5d5', + }, + { + text: 'Login', + value: 'authenticate', + foreground: '#9b51e0', + background: '#e6d3f7', + }, + ], + }, + }, + { + collection: 'directus_activity', + field: 'collection', + display: 'collection', + display_options: { + icon: true, + }, + }, + { + collection: 'directus_activity', + field: 'timestamp', + display: 'datetime', + options: { + relative: true, + }, + }, + { + collection: 'directus_activity', + field: 'user', + display: 'user', + }, + { + collection: 'directus_activity', + field: 'comment', + display: 'formatted-text', + display_options: { + subdued: true, + }, + }, + { + collection: 'directus_activity', + field: 'user_agent', + display: 'formatted-text', + display_options: { + font: 'monospace', + }, + }, + { + collection: 'directus_activity', + field: 'ip', + display: 'formatted-text', + display_options: { + font: 'monospace', + }, + }, + { + collection: 'directus_activity', + field: 'revisions', + interface: 'one-to-many', + locked: true, + special: 'o2m', + options: { + fields: ['collection', 'item'], + }, + width: 'full', + }, + { + collection: 'directus_relations', + field: 'one_allowed_collections', + locked: true, + special: 'csv', + }, +]; + +export async function up(knex: Knex) { + const fieldKeys = uniq(systemFields.map((field) => field.field)); + await knex('directus_fields') + .delete() + .where('collection', 'like', 'directus_%') + .whereIn('field', fieldKeys); +} + +export async function down(knex: Knex) { + await knex.insert(systemFields).into('directus_fields'); +} diff --git a/api/src/database/seeds/04-fields.yaml b/api/src/database/seeds/04-fields.yaml index a4050afed2..1b48fc5cb6 100644 --- a/api/src/database/seeds/04-fields.yaml +++ b/api/src/database/seeds/04-fields.yaml @@ -7,9 +7,6 @@ columns: type: string length: 64 nullable: false - references: - table: directus_collections - column: collection field: type: string length: 64 diff --git a/api/src/database/seeds/05-activity.yaml b/api/src/database/seeds/05-activity.yaml index 9abddfaeeb..d73a96d8ae 100644 --- a/api/src/database/seeds/05-activity.yaml +++ b/api/src/database/seeds/05-activity.yaml @@ -25,9 +25,6 @@ columns: type: string length: 64 nullable: false - references: - table: directus_collections - column: collection item: type: string length: 255 diff --git a/api/src/database/seeds/08-permissions.yaml b/api/src/database/seeds/08-permissions.yaml index ac2333e413..64af84088c 100644 --- a/api/src/database/seeds/08-permissions.yaml +++ b/api/src/database/seeds/08-permissions.yaml @@ -12,9 +12,6 @@ columns: type: string length: 64 nullable: false - references: - table: directus_collections - column: collection action: type: string length: 10 diff --git a/api/src/database/seeds/09-presets.yaml b/api/src/database/seeds/09-presets.yaml index 2d348a9fd5..da1e658126 100644 --- a/api/src/database/seeds/09-presets.yaml +++ b/api/src/database/seeds/09-presets.yaml @@ -19,9 +19,6 @@ columns: collection: type: string length: 64 - references: - table: directus_collections - column: collection search: type: string length: 100 diff --git a/api/src/database/seeds/10-relations.yaml b/api/src/database/seeds/10-relations.yaml index 752a192b8a..4f16294b9d 100644 --- a/api/src/database/seeds/10-relations.yaml +++ b/api/src/database/seeds/10-relations.yaml @@ -7,9 +7,6 @@ columns: type: string length: 64 nullable: false - references: - table: directus_collections - column: collection many_field: type: string length: 64 @@ -21,9 +18,6 @@ columns: one_collection: type: string length: 64 - references: - table: directus_collections - column: collection one_field: type: string length: 64 diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 8e8110dd54..82e8b3ba78 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -80,7 +80,7 @@ export class FieldsService { aliasQuery.andWhere('collection', collection); } - let aliasFields = await aliasQuery; + let aliasFields = [...(await aliasQuery), ...systemFieldRows]; const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations']; From e47cac02bcd051a579a3d9a4fd3a4cc6c9c6c11b Mon Sep 17 00:00:00 2001 From: Ben Haynes Date: Thu, 29 Oct 2020 14:10:01 -0400 Subject: [PATCH 077/639] Add items to docs --- docs/guides/items.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/guides/items.md diff --git a/docs/guides/items.md b/docs/guides/items.md new file mode 100644 index 0000000000..eab438e8bd --- /dev/null +++ b/docs/guides/items.md @@ -0,0 +1,4 @@ +# Items + +> TK + From b54f9a9ab183118305aa0e5c25c51920ea4c1a00 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 14:22:22 -0400 Subject: [PATCH 078/639] Add defaults to migrations --- .../20201029A-remove-system-relations.ts | 13 +++++++++- ...=> 20201029B-remove-system-collections.ts} | 13 +++++++++- .../20201029C-remove-system-fields.ts | 25 ++++++++++++++++--- 3 files changed, 46 insertions(+), 5 deletions(-) rename api/src/database/migrations/{20201029B-remove-system-collections copy.ts => 20201029B-remove-system-collections.ts} (89%) diff --git a/api/src/database/migrations/20201029A-remove-system-relations.ts b/api/src/database/migrations/20201029A-remove-system-relations.ts index 90b4288cb5..fcf3e76c71 100644 --- a/api/src/database/migrations/20201029A-remove-system-relations.ts +++ b/api/src/database/migrations/20201029A-remove-system-relations.ts @@ -1,4 +1,5 @@ import Knex from 'knex'; +import { merge } from 'lodash'; export async function up(knex: Knex) { await knex('directus_relations') @@ -8,6 +9,16 @@ export async function up(knex: Knex) { } export async function down(knex: Knex) { + const defaults = { + many_collection: 'directus_users', + many_field: null, + many_primary: null, + one_collection: null, + one_field: null, + one_primary: null, + junction_field: null, + }; + const systemRelations = [ { many_collection: 'directus_users', @@ -103,7 +114,7 @@ export async function down(knex: Knex) { one_collection: 'directus_files', one_primary: 'id', }, - ]; + ].map((row) => merge({}, defaults, row)); await knex.insert(systemRelations).into('directus_relations'); } diff --git a/api/src/database/migrations/20201029B-remove-system-collections copy.ts b/api/src/database/migrations/20201029B-remove-system-collections.ts similarity index 89% rename from api/src/database/migrations/20201029B-remove-system-collections copy.ts rename to api/src/database/migrations/20201029B-remove-system-collections.ts index fb9569a7cb..f1ce5158a3 100644 --- a/api/src/database/migrations/20201029B-remove-system-collections copy.ts +++ b/api/src/database/migrations/20201029B-remove-system-collections.ts @@ -1,10 +1,21 @@ import Knex from 'knex'; +import { merge } from 'lodash'; export async function up(knex: Knex) { await knex('directus_collections').delete().where('collection', 'like', 'directus_%'); } export async function down(knex: Knex) { + const defaults = { + collection: null, + hidden: false, + singleton: false, + icon: null, + note: null, + translations: null, + display_template: null, + }; + const systemCollections = [ { collection: 'directus_activity', @@ -74,7 +85,7 @@ export async function down(knex: Knex) { collection: 'directus_webhooks', note: 'Configuration for event-based HTTP requests', }, - ]; + ].map((row) => merge({}, defaults, row)); await knex.insert(systemCollections).into('directus_collections'); } diff --git a/api/src/database/migrations/20201029C-remove-system-fields.ts b/api/src/database/migrations/20201029C-remove-system-fields.ts index 1b4195f177..0c1d6afb75 100644 --- a/api/src/database/migrations/20201029C-remove-system-fields.ts +++ b/api/src/database/migrations/20201029C-remove-system-fields.ts @@ -1,5 +1,23 @@ import Knex from 'knex'; -import { uniq } from 'lodash'; +import { uniq, merge } from 'lodash'; + +const defaults = { + collection: null, + field: null, + special: null, + interface: null, + options: null, + display: null, + display_options: null, + locked: false, + readonly: false, + hidden: false, + sort: null, + width: 'full', + group: null, + translations: null, + note: null, +}; const systemFields = [ { @@ -1612,10 +1630,11 @@ const systemFields = [ locked: true, special: 'csv', }, -]; +].map((row) => merge({}, defaults, row)); export async function up(knex: Knex) { - const fieldKeys = uniq(systemFields.map((field) => field.field)); + const fieldKeys = uniq(systemFields.map((field: any) => field.field)); + await knex('directus_fields') .delete() .where('collection', 'like', 'directus_%') From f82b80c292625759ddf58f4b89f09df89c0f2a9d Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 14:26:59 -0400 Subject: [PATCH 079/639] Stringify nested json --- .../migrations/20201029A-remove-system-relations.ts | 10 +++++++++- .../migrations/20201029B-remove-system-collections.ts | 10 +++++++++- .../migrations/20201029C-remove-system-fields.ts | 10 +++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/api/src/database/migrations/20201029A-remove-system-relations.ts b/api/src/database/migrations/20201029A-remove-system-relations.ts index fcf3e76c71..df117adcf3 100644 --- a/api/src/database/migrations/20201029A-remove-system-relations.ts +++ b/api/src/database/migrations/20201029A-remove-system-relations.ts @@ -114,7 +114,15 @@ export async function down(knex: Knex) { one_collection: 'directus_files', one_primary: 'id', }, - ].map((row) => merge({}, defaults, row)); + ].map((row) => { + for (const [key, value] of Object.entries(row)) { + if (value !== null && (typeof value === 'object' || Array.isArray(value))) { + (row as any)[key] = JSON.stringify(value); + } + } + + return merge({}, defaults, row); + }); await knex.insert(systemRelations).into('directus_relations'); } diff --git a/api/src/database/migrations/20201029B-remove-system-collections.ts b/api/src/database/migrations/20201029B-remove-system-collections.ts index f1ce5158a3..239df98301 100644 --- a/api/src/database/migrations/20201029B-remove-system-collections.ts +++ b/api/src/database/migrations/20201029B-remove-system-collections.ts @@ -85,7 +85,15 @@ export async function down(knex: Knex) { collection: 'directus_webhooks', note: 'Configuration for event-based HTTP requests', }, - ].map((row) => merge({}, defaults, row)); + ].map((row) => { + for (const [key, value] of Object.entries(row)) { + if (value !== null && (typeof value === 'object' || Array.isArray(value))) { + (row as any)[key] = JSON.stringify(value); + } + } + + return merge({}, defaults, row); + }); await knex.insert(systemCollections).into('directus_collections'); } diff --git a/api/src/database/migrations/20201029C-remove-system-fields.ts b/api/src/database/migrations/20201029C-remove-system-fields.ts index 0c1d6afb75..e7368c65c5 100644 --- a/api/src/database/migrations/20201029C-remove-system-fields.ts +++ b/api/src/database/migrations/20201029C-remove-system-fields.ts @@ -1630,7 +1630,15 @@ const systemFields = [ locked: true, special: 'csv', }, -].map((row) => merge({}, defaults, row)); +].map((row) => { + for (const [key, value] of Object.entries(row)) { + if (value !== null && (typeof value === 'object' || Array.isArray(value))) { + (row as any)[key] = JSON.stringify(value); + } + } + + return merge({}, defaults, row); +}); export async function up(knex: Knex) { const fieldKeys = uniq(systemFields.map((field: any) => field.field)); From 1743c2523d3eae56761b7aba1deca22a7da5a9db Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 14:48:57 -0400 Subject: [PATCH 080/639] Fix double casting of field meta values --- api/src/services/fields.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 82e8b3ba78..f4a8eff651 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -11,6 +11,7 @@ import { PayloadService } from '../services/payload'; import getDefaultValue from '../utils/get-default-value'; import cache from '../cache'; import SchemaInspector from 'knex-schema-inspector'; +import { toArray } from '../utils/to-array'; import { systemFieldRows } from '../database/system-data/fields/'; @@ -80,12 +81,15 @@ export class FieldsService { aliasQuery.andWhere('collection', collection); } - let aliasFields = [...(await aliasQuery), ...systemFieldRows]; + let aliasFields = [ + ...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[]), + ...systemFieldRows, + ]; const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations']; aliasFields = aliasFields.filter((field) => { - const specials = (field.special || '').split(','); + const specials = toArray(field.special); for (const type of aliasTypes) { if (specials.includes(type)) return true; @@ -94,19 +98,17 @@ export class FieldsService { return false; }); - aliasFields = (await this.payloadService.processValues('read', aliasFields)) as FieldMeta[]; - const aliasFieldsAsField = aliasFields.map((field) => { const data = { collection: field.collection, field: field.field, - type: field.special[0], + type: field.special?.[0], schema: null, meta: field, }; return data; - }); + }) as Field[]; const result = [...columnsWithSystem, ...aliasFieldsAsField]; From 9c04c92aa49adeb4a09eeaf8daf8d803c0e540a4 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 14:51:05 -0400 Subject: [PATCH 081/639] Add system flag to system rows --- api/src/database/system-data/collections/index.ts | 2 +- api/src/database/system-data/fields/index.ts | 2 +- api/src/database/system-data/relations/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/database/system-data/collections/index.ts b/api/src/database/system-data/collections/index.ts index 4ad4a1cf0f..3c1d31223e 100644 --- a/api/src/database/system-data/collections/index.ts +++ b/api/src/database/system-data/collections/index.ts @@ -6,6 +6,6 @@ const systemData = requireYAML(require.resolve('./collections.yaml')); export const systemCollectionRows: CollectionMeta[] = systemData.data.map( (row: Record) => { - return merge({}, systemData.defaults, row); + return merge({ system: true }, systemData.defaults, row); } ); diff --git a/api/src/database/system-data/fields/index.ts b/api/src/database/system-data/fields/index.ts index 18b29b5971..e30249a01c 100644 --- a/api/src/database/system-data/fields/index.ts +++ b/api/src/database/system-data/fields/index.ts @@ -15,6 +15,6 @@ for (const filepath of fieldData) { const systemFields = requireYAML(path.resolve(__dirname, filepath)); for (const field of systemFields.fields) { - systemFieldRows.push(merge({}, defaults, field)); + systemFieldRows.push(merge({ system: true }, defaults, field)); } } diff --git a/api/src/database/system-data/relations/index.ts b/api/src/database/system-data/relations/index.ts index 2fb8c5d571..7ffc843e59 100644 --- a/api/src/database/system-data/relations/index.ts +++ b/api/src/database/system-data/relations/index.ts @@ -5,5 +5,5 @@ import { Relation } from '../../../types'; const systemData = requireYAML(require.resolve('./relations.yaml')); export const systemRelationRows: Relation[] = systemData.data.map((row: Record) => { - return merge({}, systemData.defaults, row); + return merge({ system: true }, systemData.defaults, row); }); From 3b97a408b4a88a1b4a1ac63aa9b94cbc9badbc33 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 29 Oct 2020 15:14:05 -0400 Subject: [PATCH 082/639] Disable locked fields on field setup --- app/src/lang/en-US/index.json | 3 ++ .../data-model/collections/collections.vue | 4 +-- .../fields/components/field-select.vue | 18 +++++++++++- .../fields/components/fields-management.vue | 29 +++++++++++++++---- .../routes/data-model/fields/fields.vue | 2 ++ app/src/types/fields.ts | 1 + 6 files changed, 49 insertions(+), 8 deletions(-) diff --git a/app/src/lang/en-US/index.json b/app/src/lang/en-US/index.json index 182be512bf..e7e7503769 100644 --- a/app/src/lang/en-US/index.json +++ b/app/src/lang/en-US/index.json @@ -1002,6 +1002,9 @@ "singleton": "Singleton", "singleton_label": "Treat as single object", + + "system_fields_locked": "System fields are locked and can't be edited", + "fields": { "directus_activity": { "action": "Action", diff --git a/app/src/modules/settings/routes/data-model/collections/collections.vue b/app/src/modules/settings/routes/data-model/collections/collections.vue index eb49a6ab36..32b8f698e4 100644 --- a/app/src/modules/settings/routes/data-model/collections/collections.vue +++ b/app/src/modules/settings/routes/data-model/collections/collections.vue @@ -76,10 +76,10 @@ small class="no-meta" name="report_problem" - v-if="!item.meta" + v-if="!item.meta && item.collection.startsWith('directus_') === false" v-tooltip="$t('db_only_click_to_configure')" /> - + 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 4b57708fed..43649c9ae6 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 @@ -1,6 +1,18 @@ + + diff --git a/app/src/interfaces/many-to-many/many-to-many.vue b/app/src/interfaces/many-to-many/many-to-many.vue index 111ae8d351..af6e2991db 100644 --- a/app/src/interfaces/many-to-many/many-to-many.vue +++ b/app/src/interfaces/many-to-many/many-to-many.vue @@ -2,7 +2,7 @@ {{ $t('relationship_not_setup') }} -
+
{ if (props.template !== null) return props.template; - return collectionInfo.value?.meta?.display_template; + return collectionInfo.value?.meta?.display_template || `{{ ${relatedPrimaryKeyField} }}`; }); const requiredFields = computed(() => { From ec98d072d428fa5ec801c3a1cdf4ae965df9d50d Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 2 Nov 2020 15:33:26 -0500 Subject: [PATCH 121/639] Fix o2m not rendering nested m2o data Fixes #841 --- .../interfaces/many-to-many/use-preview.ts | 114 +++++++++--------- .../interfaces/one-to-many/one-to-many.vue | 4 +- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/app/src/interfaces/many-to-many/use-preview.ts b/app/src/interfaces/many-to-many/use-preview.ts index f58f3fdce6..468af348a9 100644 --- a/app/src/interfaces/many-to-many/use-preview.ts +++ b/app/src/interfaces/many-to-many/use-preview.ts @@ -1,8 +1,8 @@ -import { Ref, ref, watch, computed } from '@vue/composition-api'; +import { Ref, ref, watch } from '@vue/composition-api'; import { Header } from '@/components/v-table/types'; import { RelationInfo } from './use-relation'; import { useFieldsStore } from '@/stores/'; -import { Field, Collection } from '@/types'; +import { Field } from '@/types'; import api from '@/api'; import { cloneDeep, get } from 'lodash'; @@ -25,20 +25,6 @@ export default function usePreview( const items = ref[]>([]); const error = ref(null); - function getRelatedFields(fields: string[]) { - const { junctionField } = relation.value; - - return fields.reduce((acc: string[], field) => { - const sections = field.split('.'); - if (junctionField === sections[0] && sections.length >= 2) acc.push(sections.slice(1).join('.')); - return acc; - }, []); - } - - function getJunctionFields() { - return (fields.value || []).filter((field) => field.includes('.') === false); - } - watch( () => value.value, async (newVal) => { @@ -112,6 +98,61 @@ export default function usePreview( { immediate: true } ); + // Seeing we don't care about saving those tableHeaders, we can reset it whenever the + // fields prop changes (most likely when we're navigating to a different o2m context) + watch( + () => fields.value, + () => { + const { junctionField, junctionCollection } = relation.value; + + tableHeaders.value = (fields.value.length > 0 + ? fields.value + : getDefaultFields().map((field) => `${junctionField}.${field}`) + ) + .map((fieldKey) => { + let field = fieldsStore.getField(junctionCollection, fieldKey); + + if (!field) return null; + + const header: Header = { + text: field.name, + value: fieldKey, + align: 'left', + sortable: true, + width: null, + field: { + display: field.meta?.display, + displayOptions: field.meta?.display_options, + interface: field.meta?.interface, + interfaceOptions: field.meta?.options, + type: field.type, + field: field.field, + }, + }; + + return header; + }) + .filter((h) => h) as Header[]; + }, + { immediate: true } + ); + + return { tableHeaders, items, loading, error }; + + function getRelatedFields(fields: string[]) { + const { junctionField } = relation.value; + + return fields.reduce((acc: string[], field) => { + const sections = field.split('.'); + if (junctionField === sections[0] && sections.length >= 2) acc.push(sections.slice(1).join('.')); + return acc; + }, []); + } + + function getJunctionFields() { + return (fields.value || []).filter((field) => field.includes('.') === false); + } + async function loadRelatedIds() { const { junctionPkField, junctionField, relationPkField, junctionCollection } = relation.value; @@ -162,49 +203,8 @@ export default function usePreview( return response?.data.data as Record[]; } - // Seeing we don't care about saving those tableHeaders, we can reset it whenever the - // fields prop changes (most likely when we're navigating to a different o2m context) - watch( - () => fields.value, - () => { - const { junctionField, junctionCollection } = relation.value; - - tableHeaders.value = (fields.value.length > 0 - ? fields.value - : getDefaultFields().map((field) => `${junctionField}.${field}`) - ) - .map((fieldKey) => { - let field = fieldsStore.getField(junctionCollection, fieldKey); - - if (!field) return null; - - const header: Header = { - text: field.name, - value: fieldKey, - align: 'left', - sortable: true, - width: null, - field: { - display: field.meta?.display, - displayOptions: field.meta?.display_options, - interface: field.meta?.interface, - interfaceOptions: field.meta?.options, - type: field.type, - field: field.field, - }, - }; - - return header; - }) - .filter((h) => h) as Header[]; - }, - { immediate: true } - ); - function getDefaultFields(): string[] { const fields = fieldsStore.getFieldsForCollection(relation.value.relationCollection); return fields.slice(0, 3).map((field: Field) => field.field); } - - return { tableHeaders, items, loading, error }; } diff --git a/app/src/interfaces/one-to-many/one-to-many.vue b/app/src/interfaces/one-to-many/one-to-many.vue index 6bb1128d19..a3a8c858fa 100644 --- a/app/src/interfaces/one-to-many/one-to-many.vue +++ b/app/src/interfaces/one-to-many/one-to-many.vue @@ -19,7 +19,7 @@