diff --git a/.eslintignore b/.eslintignore index f06235c460..b98800ed6f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules dist +app/vite.config.js diff --git a/.eslintrc.js b/.eslintrc.js index a98c7f4f43..f0428946c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,7 +25,7 @@ module.exports = { overrides: [ // Parse rollup configration as module { - files: ['rollup.config.js'], + files: ['rollup.config.js', 'vite.config.js'], parserOptions: { sourceType: 'module', }, @@ -38,7 +38,7 @@ module.exports = { parser: '@typescript-eslint/parser', }, extends: [ - 'plugin:vue/essential', + 'plugin:vue/vue3-essential', 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier-vue/recommended', @@ -58,8 +58,6 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 0, // Allow unused variables when they begin with an underscore '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - // Disable validity checks on v-slot directive (consider to enable this rule later on) - 'vue/valid-v-slot': 0, }, }, ], diff --git a/.stylelintrc.json b/.stylelintrc.json index 2d65959985..541eb314fd 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,29 +1,22 @@ { - "extends": [ - "stylelint-config-standard", - "stylelint-config-rational-order", - "stylelint-config-prettier" - ], + "extends": ["stylelint-config-standard", "stylelint-config-rational-order", "stylelint-config-prettier"], "plugins": ["stylelint-order", "stylelint-scss"], "rules": { "indentation": "tab", - "order/order": [ - "dollar-variables", - "custom-properties", - "declarations", - "at-variables", - "rules" - ], + "order/order": ["dollar-variables", "custom-properties", "declarations", "at-variables", "rules"], "at-rule-no-unknown": null, "scss/at-rule-no-unknown": true, - "selector-pseudo-element-no-unknown": [ + "selector-pseudo-class-no-unknown": [ true, { - "ignorePseudoElements": ["v-deep"] + "ignorePseudoClasses": ["deep", "slotted", "global"] } ], "string-quotes": "single", "length-zero-no-unit": null, - "no-descending-specificity": true + "no-descending-specificity": true, + "rule-empty-line-before": ["always", { "except": "first-nested" }], + "block-closing-brace-empty-line-before": "never", + "block-opening-brace-newline-after": "always-multi-line" } } diff --git a/api/package.json b/api/package.json index 6de84a92f4..a0abd860fc 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "directus", - "version": "9.0.0-rc.73", + "version": "9.0.0-rc.74", "license": "GPL-3.0-only", "homepage": "https://github.com/directus/directus#readme", "description": "Directus is a real-time API and App dashboard for managing SQL database content.", @@ -66,14 +66,14 @@ "example.env" ], "dependencies": { - "@directus/app": "9.0.0-rc.73", - "@directus/drive": "9.0.0-rc.73", - "@directus/drive-azure": "9.0.0-rc.73", - "@directus/drive-gcs": "9.0.0-rc.73", - "@directus/drive-s3": "9.0.0-rc.73", - "@directus/format-title": "9.0.0-rc.73", - "@directus/schema": "9.0.0-rc.73", - "@directus/specs": "9.0.0-rc.73", + "@directus/app": "9.0.0-rc.74", + "@directus/drive": "9.0.0-rc.74", + "@directus/drive-azure": "9.0.0-rc.74", + "@directus/drive-gcs": "9.0.0-rc.74", + "@directus/drive-s3": "9.0.0-rc.74", + "@directus/format-title": "9.0.0-rc.74", + "@directus/schema": "9.0.0-rc.74", + "@directus/specs": "9.0.0-rc.74", "@godaddy/terminus": "^4.9.0", "argon2": "^0.28.1", "async": "^3.2.0", @@ -90,9 +90,9 @@ "date-fns": "^2.21.1", "deep-map": "^2.0.0", "destroy": "^1.0.4", - "dotenv": "^9.0.2", + "dotenv": "^10.0.0", "eventemitter2": "^6.4.3", - "execa": "^5.0.1", + "execa": "^5.1.1", "exif-reader": "^1.0.3", "express": "^4.17.1", "express-pino-logger": "^6.0.0", @@ -110,13 +110,14 @@ "jsonwebtoken": "^8.5.1", "keyv": "^4.0.3", "knex": "^0.95.6", - "knex-schema-inspector": "^1.5.6", + "knex-schema-inspector": "^1.5.7", "liquidjs": "^9.25.0", "lodash": "^4.17.21", "macos-release": "^2.4.1", "mime-types": "^2.1.31", "ms": "^2.1.3", "nanoid": "^3.1.23", + "node-cron": "^3.0.0", "node-machine-id": "^1.1.12", "nodemailer": "^6.6.1", "openapi3-ts": "^2.0.0", @@ -135,7 +136,7 @@ "optionalDependencies": { "@keyv/redis": "^2.1.2", "connect-memcached": "^1.0.0", - "connect-redis": "^5.2.0", + "connect-redis": "^6.0.0", "connect-session-knex": "^2.1.0", "ioredis": "^4.27.2", "keyv-memcache": "^1.2.5", @@ -169,9 +170,10 @@ "@types/mime-types": "^2.1.0", "@types/ms": "^0.7.31", "@types/node": "^15.12.0", + "@types/node-cron": "^2.0.3", "@types/nodemailer": "^6.4.1", "@types/qs": "^6.9.6", - "@types/sharp": "^0.28.1", + "@types/sharp": "^0.28.3", "@types/stream-json": "^1.7.0", "@types/uuid": "^8.3.0", "@types/uuid-validate": "^0.0.1", diff --git a/api/src/database/migrations/20210608A-add-deep-clone-config.ts b/api/src/database/migrations/20210608A-add-deep-clone-config.ts new file mode 100644 index 0000000000..fa9affe79f --- /dev/null +++ b/api/src/database/migrations/20210608A-add-deep-clone-config.ts @@ -0,0 +1,13 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('directus_collections', (table) => { + table.json('item_duplication_fields').nullable(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('directus_collections', (table) => { + table.dropColumn('item_duplication_fields'); + }); +} diff --git a/api/src/database/system-data/fields/collections.yaml b/api/src/database/system-data/fields/collections.yaml index aa2c2193e8..c6ef4294f5 100644 --- a/api/src/database/system-data/fields/collections.yaml +++ b/api/src/database/system-data/fields/collections.yaml @@ -179,3 +179,19 @@ fields: - text: '$t:field_options.directus_collections.do_not_track_anything' value: null width: half + + - field: duplication_divider + special: + - alias + - no-data + interface: presentation-divider + options: + icon: content_copy + title: Duplication + + - field: item_duplication_fields + special: + - json + interface: code + options: + language: JSON diff --git a/api/src/env.ts b/api/src/env.ts index 25b9873a37..f028959f4d 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -7,11 +7,10 @@ import dotenv from 'dotenv'; import fs from 'fs'; import { clone, toNumber, toString } from 'lodash'; import path from 'path'; -import logger from './logger'; import { requireYAML } from './utils/require-yaml'; import { toArray } from './utils/to-array'; -const acceptableEnvTypes = ['string', 'number', 'regex', 'array']; +const acceptedEnvTypes = ['string', 'number', 'regex', 'array']; const defaults: Record = { CONFIG_PATH: path.resolve(process.cwd(), '.env'), @@ -125,7 +124,7 @@ function getEnv() { return exported; } - logger.warn( + throw new Error( `Invalid JS configuration file export type. Requires one of "function", "object", received: "${typeof exported}"` ); } @@ -141,11 +140,11 @@ function getEnv() { return data as Record; } - logger.warn('Invalid YAML configuration. Root has to ben an object.'); + throw new Error('Invalid YAML configuration. Root has to be an object.'); } // Default to env vars plain text files - return dotenv.parse(fs.readFileSync(configPath).toString()); + return dotenv.parse(fs.readFileSync(configPath, { encoding: 'utf8' })); } function getVariableType(variable: string) { @@ -175,12 +174,33 @@ function getEnvironmentValueByType(envVariableString: string) { function processValues(env: Record) { env = clone(env); - for (const [key, value] of Object.entries(env)) { - if (typeof value === 'string' && acceptableEnvTypes.some((envType) => value.includes(`${envType}:`))) { + for (let [key, value] of Object.entries(env)) { + // If key ends with '_FILE', try to get the value from the file defined in this variable + // and store it in the variable with the same name but without '_FILE' at the end + let newKey; + if (key.length > 5 && key.endsWith('_FILE')) { + try { + value = fs.readFileSync(value, { encoding: 'utf8' }); + newKey = key.slice(0, -5); + if (newKey in env) { + throw new Error( + `Duplicate environment variable encountered: you can't use "${key}" and "${newKey}" simultaneously.` + ); + } + key = newKey; + } catch { + throw new Error(`Failed to read value from file "${value}", defined in environment variable "${key}".`); + } + } + + // Convert values with a type prefix + // (see https://docs.directus.io/reference/environment-variables/#environment-syntax-prefix) + if (typeof value === 'string' && acceptedEnvTypes.some((envType) => value.includes(`${envType}:`))) { env[key] = getEnvironmentValueByType(value); continue; } + // Convert values where the key is defined in typeMap if (typeMap[key]) { switch (typeMap[key]) { case 'number': @@ -193,14 +213,35 @@ function processValues(env: Record) { env[key] = toArray(value); break; } - continue; } - if (value === 'true') env[key] = true; - if (value === 'false') env[key] = false; - if (value === 'null') env[key] = null; - if (String(value).startsWith('0') === false && isNaN(value) === false && value.length > 0) env[key] = Number(value); + // Try to convert remaining values: + // - boolean values to boolean + // - 'null' to null + // - number values (> 0 <= Number.MAX_SAFE_INTEGER) to number + if (value === 'true' || value === 'false') { + env[key] = !!value; + continue; + } + if (value === 'null') { + env[key] = null; + continue; + } + if ( + String(value).startsWith('0') === false && + isNaN(value) === false && + value.length > 0 && + value <= Number.MAX_SAFE_INTEGER + ) { + env[key] = Number(value); + continue; + } + + // If '_FILE' variable hasn't been processed yet, store it as it is (string) + if (newKey) { + env[key] = value; + } } return env; diff --git a/api/src/extensions.ts b/api/src/extensions.ts index 279d9f733b..4e5b971306 100644 --- a/api/src/extensions.ts +++ b/api/src/extensions.ts @@ -11,6 +11,7 @@ import * as services from './services'; import { EndpointRegisterFunction, HookRegisterFunction } from './types'; import { getSchema } from './utils/get-schema'; import listFolders from './utils/list-folders'; +import { schedule, validate } from 'node-cron'; export async function ensureFoldersExist(): Promise { const folders = ['endpoints', 'hooks', 'interfaces', 'modules', 'layouts', 'displays']; @@ -94,8 +95,19 @@ function registerHooks(hooks: string[]) { } const events = register({ services, exceptions, env, database: getDatabase(), getSchema }); + for (const [event, handler] of Object.entries(events)) { - emitter.on(event, handler); + if (event.startsWith('cron(')) { + const cron = event.match(/\(([^)]+)\)/)?.[1]; + + if (!cron || validate(cron) === false) { + logger.warn(`Couldn't register cron hook. Provided cron is invalid: ${cron}`); + } else { + schedule(cron, handler); + } + } else { + emitter.on(event, handler); + } } } } diff --git a/api/src/grant.ts b/api/src/grant.ts index bb1814f678..e8b702a961 100644 --- a/api/src/grant.ts +++ b/api/src/grant.ts @@ -4,6 +4,7 @@ import env from './env'; import { toArray } from './utils/to-array'; +import { getConfigFromEnv } from './utils/get-config-from-env'; const enabledProviders = toArray(env.OAUTH_PROVIDERS).map((provider) => provider.toLowerCase()); @@ -16,23 +17,8 @@ const config: any = { }, }; -for (const [key, value] of Object.entries(env)) { - if (key.startsWith('OAUTH') === false) continue; - - const parts = key.split('_'); - const provider = parts[1].toLowerCase(); - - if (enabledProviders.includes(provider) === false) continue; - - // OAUTH SETTING = VALUE - parts.splice(0, 2); - - const configKey = parts.join('_').toLowerCase(); - - config[provider] = { - ...(config[provider] || {}), - [configKey]: value, - }; +for (const provider of enabledProviders) { + config[provider] = getConfigFromEnv(`OAUTH_${provider.toUpperCase()}_`, undefined, 'underscore'); } export default config; diff --git a/api/src/types/collection.ts b/api/src/types/collection.ts index 6d16a42d1d..e589efb85e 100644 --- a/api/src/types/collection.ts +++ b/api/src/types/collection.ts @@ -8,6 +8,7 @@ export type CollectionMeta = { singleton: boolean; icon: string | null; translations: Record; + item_duplication_fields: string[] | null; accountability: 'all' | 'accountability' | null; }; diff --git a/api/src/utils/get-config-from-env.ts b/api/src/utils/get-config-from-env.ts index 89c26ed26b..e7580b7ea9 100644 --- a/api/src/utils/get-config-from-env.ts +++ b/api/src/utils/get-config-from-env.ts @@ -2,7 +2,11 @@ import camelcase from 'camelcase'; import { set } from 'lodash'; import env from '../env'; -export function getConfigFromEnv(prefix: string, omitPrefix?: string | string[]): any { +export function getConfigFromEnv( + prefix: string, + omitPrefix?: string | string[], + type: 'camelcase' | 'underscore' = 'camelcase' +): Record { const config: any = {}; for (const [key, value] of Object.entries(env)) { @@ -23,12 +27,22 @@ export function getConfigFromEnv(prefix: string, omitPrefix?: string | string[]) if (key.includes('__')) { const path = key .split('__') - .map((key, index) => (index === 0 ? camelcase(camelcase(key.slice(prefix.length))) : camelcase(key))); + .map((key, index) => (index === 0 ? transform(transform(key.slice(prefix.length))) : transform(key))); set(config, path.join('.'), value); } else { - config[camelcase(key.slice(prefix.length))] = value; + config[transform(key.slice(prefix.length))] = value; } } return config; + + function transform(key: string): string { + if (type === 'camelcase') { + return camelcase(key); + } else if (type === 'underscore') { + return key.toLowerCase(); + } + + return key; + } } diff --git a/app/.browserslistrc b/app/.browserslistrc deleted file mode 100644 index d6471a38cc..0000000000 --- a/app/.browserslistrc +++ /dev/null @@ -1,2 +0,0 @@ -> 1% -last 2 versions diff --git a/app/.gitignore b/app/.gitignore index e282d9bb44..53f7466aca 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,22 +1,5 @@ node_modules -/dist -coverage -public/img/docs - -# local env files -.env.local -.env.*.local - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea -.vscode -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? +.DS_Store +dist +dist-ssr +*.local \ No newline at end of file diff --git a/app/babel.config.js b/app/babel.config.js deleted file mode 100644 index 7da92c5fa0..0000000000 --- a/app/babel.config.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - presets: [ - [ - '@vue/app', - { - targets: { esmodules: true }, - polyfills: [], - }, - ], - ], - plugins: ['@babel/plugin-proposal-optional-chaining'], -}; diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000000..8e0471e0e8 --- /dev/null +++ b/app/index.html @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Loading… + + + + + +
+ +
+ + + + + diff --git a/app/package.json b/app/package.json index 69033598fe..3792dfa7a8 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@directus/app", - "version": "9.0.0-rc.73", + "version": "9.0.0-rc.74", "private": false, "description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases", "author": "Rijk van Zanten ", @@ -18,28 +18,30 @@ "access": "public" }, "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", "copy-docs-images": "rimraf public/img/docs && copyfiles -u 3 \"../docs/assets/**/*\" \"public/img/docs\" --verbose", "predev": "npm run copy-docs-images", "prebuild": "npm run copy-docs-images", - "dev": "vue-cli-service serve", - "build": "vue-cli-service build", "prepublishOnly": "npm run build" }, "gitHead": "24621f3934dc77eb23441331040ed13c676ceffd", "devDependencies": { - "@directus/docs": "9.0.0-rc.73", - "@directus/format-title": "9.0.0-rc.73", + "@directus/docs": "9.0.0-rc.74", + "@directus/format-title": "9.0.0-rc.74", "@fullcalendar/core": "^5.7.2", "@fullcalendar/daygrid": "^5.7.2", "@fullcalendar/interaction": "^5.7.2", "@fullcalendar/list": "^5.7.2", "@fullcalendar/timegrid": "^5.7.2", - "@popperjs/core": "^2.9.1", + "@popperjs/core": "^2.9.2", + "@rollup/plugin-yaml": "^3.0.0", "@sindresorhus/slugify": "^2.1.0", - "@tinymce/tinymce-vue": "^3.2.8", + "@tinymce/tinymce-vue": "^4.0.0", "@types/base-64": "^1.0.0", "@types/bytes": "^3.1.0", - "@types/codemirror": "^0.0.109", + "@types/codemirror": "^5.60.0", "@types/color": "^3.0.1", "@types/diff": "^5.0.0", "@types/dompurify": "^2.2.2", @@ -49,14 +51,13 @@ "@types/mime-types": "^2.1.0", "@types/ms": "^0.7.31", "@types/qrcode": "^1.4.0", - "@types/tiny-async-pool": "^1.0.0", + "@vitejs/plugin-vue": "^1.2.1", "@vue/cli-plugin-babel": "^4.5.13", "@vue/cli-plugin-router": "^4.5.8", "@vue/cli-plugin-typescript": "^4.5.13", "@vue/cli-plugin-vuex": "^4.5.8", "@vue/cli-service": "^4.5.13", - "@vue/composition-api": "^0.6.7", - "@vue/test-utils": "^1.2.0", + "@vue/compiler-sfc": "^3.0.5", "axios": "^0.21.1", "base-64": "^1.0.0", "codemirror": "^5.61.1", @@ -67,33 +68,24 @@ "escape-string-regexp": "^5.0.0", "front-matter": "^4.0.2", "html-entities": "^2.3.2", - "joi": "^17.4.0", "jsonlint-mod": "^1.7.6", "marked": "^2.0.7", "micromustache": "^8.0.3", + "mime": "^2.5.2", "mitt": "^2.1.0", "nanoid": "^3.1.23", - "pinia": "^0.0.7", - "portal-vue": "^2.1.7", - "prettier": "^2.3.0", + "pinia": "^2.0.0-alpha.13", + "prettier": "^2.3.1", "pretty-ms": "^7.0.1", "qrcode": "^1.4.4", - "raw-loader": "^4.0.2", - "resize-observer": "^1.0.2", "rimraf": "^3.0.2", "sass": "^1.34.1", - "sass-loader": "^9.0.2", - "stylelint": "^13.13.1", - "tiny-async-pool": "^1.2.0", - "tinymce": "^5.8.1", - "vue": "^2.6.12", - "vue-cli-plugin-yaml": "^1.0.2", - "vue-i18n": "^8.24.4", - "vue-loader": "^15.9.7", - "vue-router": "^3.4.8", - "vue-template-compiler": "^2.6.10", - "vuedraggable": "^2.24.3", - "vuepress": "^1.5.2", - "webpack-assets-manifest": "^3.1.1" + "tinymce": "^5.7.1", + "typescript": "^4.2.4", + "vite": "^2.1.5", + "vue": "^3.0.5", + "vue-i18n": "^9.1.6", + "vue-router": "^4.0.6", + "vuedraggable": "^4.0.1" } } diff --git a/app/public/.htaccess b/app/public/.htaccess deleted file mode 100644 index ae945dc0c2..0000000000 --- a/app/public/.htaccess +++ /dev/null @@ -1,12 +0,0 @@ - - - RewriteEngine on - - # If file or directory exists behave normally - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - - # Otherwise use index.html (you might need to update path) - RewriteRule . /index.html [L,QSA] - - diff --git a/app/public/index.html b/app/public/index.html deleted file mode 100644 index fc9b64f9be..0000000000 --- a/app/public/index.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Loading… - - - - -
- - - diff --git a/app/src/app.vue b/app/src/app.vue index 213997f66e..41daa83313 100644 --- a/app/src/app.vue +++ b/app/src/app.vue @@ -1,13 +1,13 @@ - - - - diff --git a/app/src/components/v-button/v-button.vue b/app/src/components/v-button/v-button.vue index 22312d8309..344d313deb 100644 --- a/app/src/components/v-button/v-button.vue +++ b/app/src/components/v-button/v-button.vue @@ -4,8 +4,8 @@ -import { defineComponent, computed, PropType } from '@vue/composition-api'; -import { Location } from 'vue-router'; +import { defineComponent, computed, PropType } from 'vue'; +import { RouteLocation } from 'vue-router'; import useSizeClass, { sizeProps } from '@/composables/size-class'; import { useGroupable } from '@/composables/groupable'; import { notEmpty } from '@/utils/is-empty'; export default defineComponent({ + emits: ['click'], props: { autofocus: { type: Boolean, @@ -85,7 +86,7 @@ export default defineComponent({ default: false, }, to: { - type: [String, Object] as PropType, + type: [String, Object] as PropType, default: null, }, href: { @@ -135,7 +136,7 @@ export default defineComponent({ const { active, toggle } = useGroupable({ value: props.value, - group: 'button-group', + group: 'item-group', }); return { sizeClass, onClick, component, active, toggle }; @@ -150,8 +151,8 @@ export default defineComponent({ }); - - diff --git a/app/src/components/v-card/v-card-actions.vue b/app/src/components/v-card/v-card-actions.vue index c17a11bc55..3e833e2e1e 100644 --- a/app/src/components/v-card/v-card-actions.vue +++ b/app/src/components/v-card/v-card-actions.vue @@ -1,15 +1,15 @@ - @@ -177,13 +181,13 @@ body { } &.dense { - ::v-deep .v-text-overflow { + :deep(.v-text-overflow) { color: var(--foreground-normal); } &:hover, &.active { - ::v-deep .v-text-overflow { + :deep(.v-text-overflow) { color: var(--primary); } } diff --git a/app/src/components/v-list/v-list.vue b/app/src/components/v-list/v-list.vue index 89913928c8..fb8bb2dc9f 100644 --- a/app/src/components/v-list/v-list.vue +++ b/app/src/components/v-list/v-list.vue @@ -5,16 +5,13 @@ - - diff --git a/app/src/components/v-menu/use-popper.ts b/app/src/components/v-menu/use-popper.ts index 64caa37981..8fede4e67f 100644 --- a/app/src/components/v-menu/use-popper.ts +++ b/app/src/components/v-menu/use-popper.ts @@ -1,3 +1,4 @@ +import { createPopper } from '@popperjs/core/lib/popper-lite'; import { Instance, Modifier, Placement } from '@popperjs/core'; import arrow from '@popperjs/core/lib/modifiers/arrow'; import computeStyles from '@popperjs/core/lib/modifiers/computeStyles'; @@ -6,8 +7,7 @@ import flip from '@popperjs/core/lib/modifiers/flip'; import offset from '@popperjs/core/lib/modifiers/offset'; import popperOffsets from '@popperjs/core/lib/modifiers/popperOffsets'; import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow'; -import { createPopper } from '@popperjs/core/lib/popper-base'; -import { onUnmounted, ref, Ref, watch } from '@vue/composition-api'; +import { onUnmounted, ref, Ref, watch } from 'vue'; export function usePopper( reference: Ref, @@ -53,7 +53,7 @@ export function usePopper( popperInstance.value.forceUpdate(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion observer.observe(popper.value!, { - attributes: true, + attributes: false, childList: true, characterData: true, subtree: true, diff --git a/app/src/components/v-menu/v-menu.vue b/app/src/components/v-menu/v-menu.vue index 6c1baebf16..022381411c 100644 --- a/app/src/components/v-menu/v-menu.vue +++ b/app/src/components/v-menu/v-menu.vue @@ -18,53 +18,54 @@ /> - -
-
-
- + + +
+
+
+ +
-
- +
+
- - - diff --git a/app/src/components/v-progress/circular/v-progress-circular.vue b/app/src/components/v-progress/circular/v-progress-circular.vue index 1f17dab46a..8d238d3cfa 100644 --- a/app/src/components/v-progress/circular/v-progress-circular.vue +++ b/app/src/components/v-progress/circular/v-progress-circular.vue @@ -23,10 +23,11 @@ - - diff --git a/app/src/components/v-sheet/v-sheet.vue b/app/src/components/v-sheet/v-sheet.vue index 8a9a2c0273..5be539f32b 100644 --- a/app/src/components/v-sheet/v-sheet.vue +++ b/app/src/components/v-sheet/v-sheet.vue @@ -5,7 +5,7 @@ - - - diff --git a/app/src/components/v-text-overflow.vue b/app/src/components/v-text-overflow.vue index b98af618e4..9de4457540 100644 --- a/app/src/components/v-text-overflow.vue +++ b/app/src/components/v-text-overflow.vue @@ -5,7 +5,7 @@ diff --git a/app/src/displays/related-values/related-values.vue b/app/src/displays/related-values/related-values.vue index 3961a4b024..86e6960dea 100644 --- a/app/src/displays/related-values/related-values.vue +++ b/app/src/displays/related-values/related-values.vue @@ -18,7 +18,7 @@ - + @@ -26,15 +26,15 @@ - + diff --git a/app/src/interfaces/_system/system-display-template/system-display-template.vue b/app/src/interfaces/_system/system-display-template/system-display-template.vue index 1f78ae9e24..eb7f2ca1e4 100644 --- a/app/src/interfaces/_system/system-display-template/system-display-template.vue +++ b/app/src/interfaces/_system/system-display-template/system-display-template.vue @@ -1,18 +1,26 @@ diff --git a/app/src/interfaces/_system/system-field/system-field.vue b/app/src/interfaces/_system/system-field/system-field.vue index 0b18b6a4bd..862e8da941 100644 --- a/app/src/interfaces/_system/system-field/system-field.vue +++ b/app/src/interfaces/_system/system-field/system-field.vue @@ -1,15 +1,15 @@ - diff --git a/app/src/modules/settings/composables/use-project-info.ts b/app/src/modules/settings/composables/use-project-info.ts index 70f0539961..f0720df833 100644 --- a/app/src/modules/settings/composables/use-project-info.ts +++ b/app/src/modules/settings/composables/use-project-info.ts @@ -1,7 +1,7 @@ import api from '@/api'; -import { computed, ref, Ref } from '@vue/composition-api'; import bytes from 'bytes'; import prettyMS from 'pretty-ms'; +import { computed, ComputedRef, ref, Ref } from 'vue'; type ServerInfo = { directus: { @@ -19,7 +19,28 @@ type ServerInfo = { }; }; -export function useProjectInfo(): Record { +type UsableProjectInfo = { + info: Ref; + parsedInfo: ComputedRef<{ + directus: { + version: string; + }; + node: { + version: string; + uptime: string; + }; + os: { + type: string; + version: string; + uptime: string; + totalmem: string; + }; + } | null>; + loading: Ref; + error: Ref; +}; + +export function useProjectInfo(): UsableProjectInfo { const info = ref(); const loading = ref(false); const error = ref(); diff --git a/app/src/modules/settings/index.ts b/app/src/modules/settings/index.ts index 16b1da831d..79cfbf0f1a 100644 --- a/app/src/modules/settings/index.ts +++ b/app/src/modules/settings/index.ts @@ -2,7 +2,8 @@ import api from '@/api'; import { useCollection } from '@/composables/use-collection'; import { defineModule } from '@/modules/define'; import { useCollectionsStore, useFieldsStore } from '@/stores'; -import { ref } from '@vue/composition-api'; +import RouterPass from '@/utils/router-passthrough'; +import { ref } from 'vue'; import Collections from './routes/data-model/collections/collections.vue'; import FieldDetail from './routes/data-model/field-detail/field-detail.vue'; import Fields from './routes/data-model/fields/fields.vue'; @@ -26,130 +27,151 @@ export default defineModule({ color: 'var(--warning)', routes: [ { - path: '/', - redirect: '/data-model', + path: '', + redirect: '/settings/data-model', }, { name: 'settings-project', - path: '/project', + path: 'project', component: Project, }, { - name: 'settings-collections', - path: '/data-model', - component: Collections, - beforeEnter(to, from, next) { - const collectionsStore = useCollectionsStore(); - collectionsStore.hydrate(); - next(); - }, + path: 'data-model', + component: RouterPass, children: [ { - path: '+', - name: 'settings-add-new', - components: { - add: NewCollection, + name: 'settings-collections', + path: '', + component: Collections, + beforeEnter() { + const collectionsStore = useCollectionsStore(); + collectionsStore.hydrate(); }, + children: [ + { + path: '+', + name: 'settings-add-new', + components: { + add: NewCollection, + }, + }, + ], }, - ], - }, - { - name: 'settings-fields', - path: '/data-model/:collection', - component: Fields, - async beforeEnter(to, from, next) { - const { info } = useCollection(ref(to.params.collection)); - const fieldsStore = useFieldsStore(); + { + name: 'settings-fields', + path: ':collection', + component: Fields, + async beforeEnter(to) { + const { info } = useCollection(ref(to.params.collection as string)); + const fieldsStore = useFieldsStore(); - if (!info.value?.meta) { - await api.patch(`/collections/${to.params.collection}`, { meta: {} }); - } + if (!info.value?.meta) { + await api.patch(`/collections/${to.params.collection}`, { meta: {} }); + } - fieldsStore.hydrate(); - - next(); - }, - props: (route) => ({ - collection: route.params.collection, - field: route.params.field, - type: route.query.type, - }), - children: [ - { - path: ':field', - name: 'settings-fields-field', - components: { - field: FieldDetail, + fieldsStore.hydrate(); }, + props: (route) => ({ + collection: route.params.collection, + field: route.params.field, + type: route.query.type, + }), + children: [ + { + path: ':field', + name: 'settings-fields-field', + components: { + field: FieldDetail, + }, + }, + ], }, ], }, { - name: 'settings-roles-collection', - path: '/roles', - component: RolesCollection, + path: 'roles', + component: RouterPass, children: [ { - path: '+', - name: 'settings-add-new-role', - components: { - add: NewRole, - }, + name: 'settings-roles-collection', + path: '', + component: RolesCollection, + children: [ + { + path: '+', + name: 'settings-add-new-role', + components: { + add: NewRole, + }, + }, + ], + }, + { + path: 'public', + component: RolesPublicItem, + props: true, + children: [ + { + path: ':permissionKey', + components: { + permissionsDetail: RolesPermissionsDetail, + }, + }, + ], + }, + { + name: 'settings-roles-item', + path: ':primaryKey', + component: RolesItem, + props: true, + children: [ + { + path: ':permissionKey', + components: { + permissionsDetail: RolesPermissionsDetail, + }, + }, + ], }, ], }, { - path: '/roles/public', - component: RolesPublicItem, - props: true, + path: 'presets', + component: RouterPass, children: [ { - path: ':permissionKey', - components: { - permissionsDetail: RolesPermissionsDetail, - }, + name: 'settings-presets-collection', + path: '', + component: PresetsCollection, + }, + { + name: 'settings-presets-item', + path: ':id', + component: PresetsItem, + props: true, }, ], }, { - name: 'settings-roles-item', - path: '/roles/:primaryKey', - component: RolesItem, - props: true, + path: 'webhooks', + component: RouterPass, children: [ { - path: ':permissionKey', - components: { - permissionsDetail: RolesPermissionsDetail, - }, + name: 'settings-webhooks-collection', + path: '', + component: WebhooksCollection, + }, + { + name: 'settings-webhooks-item', + path: ':primaryKey', + component: WebhooksItem, + props: true, }, ], }, - { - name: 'settings-presets-collection', - path: '/presets', - component: PresetsCollection, - }, - { - name: 'settings-presets-item', - path: '/presets/:id', - component: PresetsItem, - props: true, - }, - { - name: 'settings-webhooks-collection', - path: '/webhooks', - component: WebhooksCollection, - }, - { - name: 'settings-webhooks-item', - path: '/webhooks/:primaryKey', - component: WebhooksItem, - props: true, - }, { name: 'settings-not-found', - path: '*', + path: ':_(.+)+', component: NotFound, }, ], 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 2ec4e33c52..f33d53dc16 100644 --- a/app/src/modules/settings/routes/data-model/collections/collections.vue +++ b/app/src/modules/settings/routes/data-model/collections/collections.vue @@ -1,6 +1,6 @@
- - {{ $t('no_collections_copy_admin') }} + + {{ t('no_collections_copy_admin') }} - @@ -22,20 +22,20 @@
- + - {{ $t('edit') }} + {{ t('edit') }} - + - {{ $t('delete') }} + {{ t('delete') }} @@ -43,15 +43,15 @@ - {{ $t('delete_comment') }} - {{ $t('delete_are_you_sure') }} + {{ t('delete_comment') }} + {{ t('delete_are_you_sure') }} - {{ $t('cancel') }} + {{ t('cancel') }} - {{ $t('delete') }} + {{ t('delete') }} @@ -60,10 +60,10 @@ - diff --git a/app/src/views/private/components/header-bar/header-bar.vue b/app/src/views/private/components/header-bar/header-bar.vue index 4deb4a6aa6..5594e984db 100644 --- a/app/src/views/private/components/header-bar/header-bar.vue +++ b/app/src/views/private/components/header-bar/header-bar.vue @@ -1,14 +1,14 @@ diff --git a/app/src/views/private/components/module-bar-avatar/module-bar-avatar.vue b/app/src/views/private/components/module-bar-avatar/module-bar-avatar.vue index 4e00162fce..cf8b60314b 100644 --- a/app/src/views/private/components/module-bar-avatar/module-bar-avatar.vue +++ b/app/src/views/private/components/module-bar-avatar/module-bar-avatar.vue @@ -9,19 +9,19 @@ x-large :class="{ show: hover }" class="sign-out" - v-tooltip.right="$t('sign_out')" + v-tooltip.right="t('sign_out')" > - {{ $t('sign_out_confirm') }} + {{ t('sign_out_confirm') }} - {{ $t('cancel') }} + {{ t('cancel') }} - {{ $t('sign_out') }} + {{ t('sign_out') }} @@ -36,26 +36,29 @@