Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-09 14:16:36 -04:00
529 changed files with 36532 additions and 32331 deletions

View File

@@ -1,2 +1,3 @@
node_modules
dist
app/vite.config.js

View File

@@ -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,
},
},
],

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.json('item_duplication_fields').nullable();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.dropColumn('item_duplication_fields');
});
}

View File

@@ -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

View File

@@ -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<string, any> = {
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<string, string>;
}
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<string, any>) {
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<string, any>) {
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;

View File

@@ -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<void> {
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);
}
}
}
}

View File

@@ -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 <PROVIDER> 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;

View File

@@ -8,6 +8,7 @@ export type CollectionMeta = {
singleton: boolean;
icon: string | null;
translations: Record<string, string>;
item_duplication_fields: string[] | null;
accountability: 'all' | 'accountability' | null;
};

View File

@@ -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<string, any> {
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;
}
}

View File

@@ -1,2 +0,0 @@
> 1%
last 2 versions

25
app/.gitignore vendored
View File

@@ -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

View File

@@ -1,12 +0,0 @@
module.exports = {
presets: [
[
'@vue/app',
{
targets: { esmodules: true },
polyfills: [],
},
],
],
plugins: ['@babel/plugin-proposal-optional-chaining'],
};

149
app/index.html Normal file
View File

@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1, viewport-fit=cover"
/>
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no" />
<meta name="HandheldFriendly" content="true" />
<meta name="MobileOptimized" content="width" />
<meta name="msapplication-TileColor" content="#263238" />
<meta name="theme-color" content="#263238" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="mask-icon" href="/img/icons/safari-pinned-tab.svg" color="#263238" />
<link rel="icon" type="image/png" sizes="512x512" href="/img/icons/android-chrome-512x512.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/img/icons/android-chrome-192x192.png" />
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/img/icons/apple-touch-icon-152x152.png" />
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2048-2732.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2732-2048.jpg"
media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1668-2388.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2388-1668.jpg"
media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1536-2048.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2048-1536.jpg"
media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1668-2224.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2224-1668.jpg"
media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1620-2160.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2160-1620.jpg"
media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1242-2688.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2688-1242.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1125-2436.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2436-1125.jpg"
media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-828-1792.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1792-828.jpg"
media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1242-2208.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-2208-1242.jpg"
media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-750-1334.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1334-750.jpg"
media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-640-1136.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)"
/>
<link
rel="apple-touch-startup-image"
href="/img/splashscreens/apple-splash-1136-640.jpg"
media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)"
/>
<title>Loading&hellip;</title>
<style id="custom-css"></style>
</head>
<body class="light">
<noscript>
<strong>We're sorry but Directus doesn't work without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<div id="dialog-outlet"></div>
<div id="menu-outlet"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -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 <rijk@rngr.org>",
@@ -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"
}
}

View File

@@ -1,12 +0,0 @@
<ifModule mod_rewrite.c>
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]
</ifModule>

View File

@@ -1,53 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=1, viewport-fit=cover">
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
<meta name="HandheldFriendly" content="true">
<meta name="MobileOptimized" content="width">
<meta name="msapplication-TileColor" content="#263238">
<meta name="theme-color" content="#263238">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
<link rel="mask-icon" href="<%= BASE_URL %>img/icons/safari-pinned-tab.svg" color="#263238">
<link rel="icon" type="image/png" sizes="512x512" href="<%= BASE_URL %>img/icons/android-chrome-512x512.png">
<link rel="icon" type="image/png" sizes="192x192" href="<%= BASE_URL %>img/icons/android-chrome-192x192.png">
<link rel="apple-touch-icon" href="<%= BASE_URL %>img/icons/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="<%= BASE_URL %>img/icons/apple-touch-icon-152x152.png">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2048-2732.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2732-2048.jpg" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1668-2388.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2388-1668.jpg" media="(device-width: 834px) and (device-height: 1194px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1536-2048.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2048-1536.jpg" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1668-2224.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2224-1668.jpg" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1620-2160.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2160-1620.jpg" media="(device-width: 810px) and (device-height: 1080px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1242-2688.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2688-1242.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1125-2436.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2436-1125.jpg" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-828-1792.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1792-828.jpg" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1242-2208.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-2208-1242.jpg" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-750-1334.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1334-750.jpg" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-640-1136.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)">
<link rel="apple-touch-startup-image" href="<%= BASE_URL %>img/splashscreens/apple-splash-1136-640.jpg" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: landscape)">
<title>Loading&hellip;</title>
<style id="custom-css"></style>
</head>
<body class="light">
<noscript>
<strong>We're sorry but Directus doesn't work without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -1,13 +1,13 @@
<template>
<div id="app" :style="brandStyle">
<div id="directus" :style="brandStyle">
<transition name="fade">
<div class="hydrating" v-if="hydrating">
<v-progress-circular indeterminate />
</div>
</transition>
<v-info v-if="error" type="danger" :title="$t('unexpected_error')" icon="error" center>
{{ $t('unexpected_error_copy') }}
<v-info v-if="error" type="danger" :title="t('unexpected_error')" icon="error" center>
{{ t('unexpected_error_copy') }}
<template #append>
<v-error :error="error" />
@@ -16,15 +16,13 @@
<router-view v-else-if="!hydrating" />
<portal-target name="dialog-outlet" transition="transition-dialog" multiple />
<portal-target name="menu-outlet" transition="transition-bounce" multiple />
<mounting-portal mount-to="#custom-css" target-tag="style">{{ customCSS }}</mounting-portal>
<teleport to="#custom-css">{{ customCSS }}</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs, watch, computed, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, toRefs, watch, computed, provide } from 'vue';
import * as stores from '@/stores';
import api, { addTokenToURL } from '@/api';
import axios from 'axios';
@@ -34,27 +32,26 @@ import setFavicon from '@/utils/set-favicon';
export default defineComponent({
setup() {
const { t } = useI18n();
const { useAppStore, useUserStore, useServerStore } = stores;
const appStore = useAppStore();
const userStore = useUserStore();
const serverStore = useServerStore();
const { hydrating, sidebarOpen } = toRefs(appStore.state);
const { hydrating, sidebarOpen } = toRefs(appStore);
const brandStyle = computed(() => {
return {
'--brand': serverStore.state.info?.project?.project_color || 'var(--primary)',
'--brand': serverStore.info?.project?.project_color || 'var(--primary)',
};
});
watch(
[() => serverStore.state.info?.project?.project_color, () => serverStore.state.info?.project?.project_logo],
() => {
const hasCustomLogo = !!serverStore.state.info?.project?.project_logo;
setFavicon(serverStore.state.info?.project?.project_color || '#00C897', hasCustomLogo);
}
);
watch([() => serverStore.info?.project?.project_color, () => serverStore.info?.project?.project_logo], () => {
const hasCustomLogo = !!serverStore.info?.project?.project_logo;
setFavicon(serverStore.info?.project?.project_color || '#00C897', hasCustomLogo);
});
const { width } = useWindowSize();
@@ -74,7 +71,7 @@ export default defineComponent({
);
watch(
() => userStore.state.currentUser,
() => userStore.currentUser,
(newUser) => {
document.body.classList.remove('dark');
document.body.classList.remove('light');
@@ -93,17 +90,17 @@ export default defineComponent({
);
watch(
() => serverStore.state.info?.project?.project_name,
() => serverStore.info?.project?.project_name,
(projectName) => {
document.title = projectName || 'Directus';
}
);
const customCSS = computed(() => {
return serverStore.state?.info?.project?.custom_css || '';
return serverStore.info?.project?.custom_css || '';
});
const error = computed(() => appStore.state.error);
const error = computed(() => appStore.error);
/**
* This allows custom extensions to use the apps internals
@@ -115,13 +112,17 @@ export default defineComponent({
addTokenToURL,
});
return { hydrating, brandStyle, error, customCSS };
return { t, hydrating, brandStyle, error, customCSS };
},
});
</script>
<style lang="scss" scoped>
#app {
:global(#app) {
height: 100%;
}
#directus {
height: 100%;
}
@@ -142,7 +143,7 @@ export default defineComponent({
transition: opacity var(--medium) var(--transition);
}
.fade-enter,
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@@ -1,8 +1,8 @@
import api from '@/api';
import { dehydrate, hydrate } from '@/hydrate';
import router from '@/router';
import { router } from '@/router';
import { useAppStore } from '@/stores';
import { RawLocation } from 'vue-router';
import { RouteLocationRaw } from 'vue-router';
export type LoginCredentials = {
email: string;
@@ -31,7 +31,7 @@ export async function login(credentials: LoginCredentials): Promise<void> {
setTimeout(() => refresh(), response.data.data.expires - 10000);
}
appStore.state.authenticated = true;
appStore.authenticated = true;
await hydrate();
}
@@ -58,7 +58,7 @@ export async function refresh({ navigate }: LogoutOptions = { navigate: true }):
if (response.data.data.expires <= 2100000000) {
refreshTimeout = setTimeout(() => refresh(), response.data.data.expires - 10000);
}
appStore.state.authenticated = true;
appStore.authenticated = true;
return accessToken;
} catch (error) {
@@ -96,12 +96,12 @@ export async function logout(optionsRaw: LogoutOptions = {}): Promise<void> {
await api.post(`/auth/logout`);
}
appStore.state.authenticated = false;
appStore.authenticated = false;
await dehydrate();
if (options.navigate === true) {
const location: RawLocation = {
const location: RouteLocationRaw = {
path: `/login`,
query: { reason: options.reason },
};

View File

@@ -1,17 +1,17 @@
import ExportSidebarDetail from '@/views/private/components/export-sidebar-detail';
import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail';
import RenderDisplay from '@/views/private/components/render-display';
import RenderTemplate from '@/views/private/components/render-template';
import SidebarDetail from '@/views/private/components/sidebar-detail/';
import UserPopover from '@/views/private/components/user-popover';
import ValueNull from '@/views/private/components/value-null';
import Vue from 'vue';
import { App } from 'vue';
import TransitionBounce from './transition/bounce';
import TransitionDialog from './transition/dialog';
import TransitionExpand from './transition/expand';
import VAvatar from './v-avatar/';
import VBadge from './v-badge/';
import VBreadcrumb from './v-breadcrumb';
import VButtonGroup from './v-button-group/';
import VButton from './v-button/';
import VCard, { VCardActions, VCardSubtitle, VCardText, VCardTitle } from './v-card';
import VCheckbox from './v-checkbox/';
@@ -49,67 +49,69 @@ import VTextOverflow from './v-text-overflow.vue';
import VTextarea from './v-textarea';
import VUpload from './v-upload';
Vue.component('v-avatar', VAvatar);
Vue.component('v-badge', VBadge);
Vue.component('v-breadcrumb', VBreadcrumb);
Vue.component('v-button', VButton);
Vue.component('v-button-group', VButtonGroup);
Vue.component('v-card-actions', VCardActions);
Vue.component('v-card-subtitle', VCardSubtitle);
Vue.component('v-card-text', VCardText);
Vue.component('v-card-title', VCardTitle);
Vue.component('v-card', VCard);
Vue.component('v-checkbox', VCheckbox);
Vue.component('v-chip', VChip);
Vue.component('v-detail', VDetail);
Vue.component('v-dialog', VDialog);
Vue.component('v-divider', VDivider);
Vue.component('v-error', VError);
Vue.component('v-fancy-select', VFancySelect);
Vue.component('v-field-template', VFieldTemplate);
Vue.component('v-field-select', VFieldSelect);
Vue.component('v-form', VForm);
Vue.component('v-hover', VHover);
Vue.component('v-icon', VIcon);
Vue.component('v-info', VInfo);
Vue.component('v-input', VInput);
Vue.component('v-item-group', VItemGroup);
Vue.component('v-item', VItem);
Vue.component('v-list-group', VListGroup);
Vue.component('v-list-item-content', VListItemContent);
Vue.component('v-list-item-hint', VListItemHint);
Vue.component('v-list-item-icon', VListItemIcon);
Vue.component('v-list-item', VListItem);
Vue.component('v-list', VList);
Vue.component('v-menu', VMenu);
Vue.component('v-drawer', VDrawer);
Vue.component('v-notice', VNotice);
Vue.component('v-overlay', VOverlay);
Vue.component('v-pagination', VPagination);
Vue.component('v-progress-circular', VProgressCircular);
Vue.component('v-progress-linear', VProgressLinear);
Vue.component('v-radio', VRadio);
Vue.component('v-select', VSelect);
Vue.component('v-sheet', VSheet);
Vue.component('v-skeleton-loader', VSkeletonLoader);
Vue.component('v-slider', VSlider);
Vue.component('v-switch', VSwitch);
Vue.component('v-tab-item', VTabItem);
Vue.component('v-tab', VTab);
Vue.component('v-table', VTable);
Vue.component('v-tabs-items', VTabsItems);
Vue.component('v-tabs', VTabs);
Vue.component('v-textarea', VTextarea);
Vue.component('v-text-overflow', VTextOverflow);
Vue.component('v-upload', VUpload);
export function registerComponents(app: App): void {
app.component('v-avatar', VAvatar);
app.component('v-badge', VBadge);
app.component('v-breadcrumb', VBreadcrumb);
app.component('v-button', VButton);
app.component('v-card-actions', VCardActions);
app.component('v-card-subtitle', VCardSubtitle);
app.component('v-card-text', VCardText);
app.component('v-card-title', VCardTitle);
app.component('v-card', VCard);
app.component('v-checkbox', VCheckbox);
app.component('v-chip', VChip);
app.component('v-detail', VDetail);
app.component('v-dialog', VDialog);
app.component('v-divider', VDivider);
app.component('v-error', VError);
app.component('v-fancy-select', VFancySelect);
app.component('v-field-template', VFieldTemplate);
app.component('v-field-select', VFieldSelect);
app.component('v-form', VForm);
app.component('v-hover', VHover);
app.component('v-icon', VIcon);
app.component('v-info', VInfo);
app.component('v-input', VInput);
app.component('v-item-group', VItemGroup);
app.component('v-item', VItem);
app.component('v-list-group', VListGroup);
app.component('v-list-item-content', VListItemContent);
app.component('v-list-item-hint', VListItemHint);
app.component('v-list-item-icon', VListItemIcon);
app.component('v-list-item', VListItem);
app.component('v-list', VList);
app.component('v-menu', VMenu);
app.component('v-drawer', VDrawer);
app.component('v-notice', VNotice);
app.component('v-overlay', VOverlay);
app.component('v-pagination', VPagination);
app.component('v-progress-circular', VProgressCircular);
app.component('v-progress-linear', VProgressLinear);
app.component('v-radio', VRadio);
app.component('v-select', VSelect);
app.component('v-sheet', VSheet);
app.component('v-skeleton-loader', VSkeletonLoader);
app.component('v-slider', VSlider);
app.component('v-switch', VSwitch);
app.component('v-tab-item', VTabItem);
app.component('v-tab', VTab);
app.component('v-table', VTable);
app.component('v-tabs-items', VTabsItems);
app.component('v-tabs', VTabs);
app.component('v-textarea', VTextarea);
app.component('v-text-overflow', VTextOverflow);
app.component('v-upload', VUpload);
Vue.component('transition-bounce', TransitionBounce);
Vue.component('transition-dialog', TransitionDialog);
Vue.component('transition-expand', TransitionExpand);
app.component('transition-bounce', TransitionBounce);
app.component('transition-dialog', TransitionDialog);
app.component('transition-expand', TransitionExpand);
Vue.component('render-display', RenderDisplay);
Vue.component('render-template', RenderTemplate);
Vue.component('filter-sidebar-detail', FilterSidebarDetail);
Vue.component('sidebar-detail', SidebarDetail);
Vue.component('user-popover', UserPopover);
Vue.component('value-null', ValueNull);
app.component('render-display', RenderDisplay);
app.component('render-template', RenderTemplate);
app.component('filter-sidebar-detail', FilterSidebarDetail);
app.component('export-sidebar-detail', ExportSidebarDetail);
app.component('sidebar-detail', SidebarDetail);
app.component('user-popover', UserPopover);
app.component('value-null', ValueNull);
}

View File

@@ -1,11 +1,12 @@
<template>
<transition-group name="bounce" tag="div">
<transition-group name="bounce" tag="div" v-bind="$attrs">
<slot />
</transition-group>
</template>
<style lang="scss">
/** @NOTE this is not scoped on purpose. The children are outsisde of the tree (portal) */
/** @NOTE this is not scoped on purpose. The children are outsisde of the tree (teleport) */
.bounce-enter-active,
.bounce-leave-active {
transition: opacity var(--fast) var(--transition);
@@ -15,7 +16,7 @@
}
}
.bounce-enter,
.bounce-enter-from,
.bounce-leave-to {
opacity: 0;

View File

@@ -1,11 +1,12 @@
<template>
<transition-group name="dialog">
<transition-group name="dialog" tag="span" v-bind="$attrs">
<slot />
</transition-group>
</template>
<style lang="scss">
/** @NOTE this is not scoped on purpose. The children are outside of the tree (portal) */
/** @NOTE this is not scoped on purpose. The children are outside of the tree (teleport) */
.dialog-enter-active,
.dialog-leave-active {
transition: opacity var(--slow) var(--transition);
@@ -21,7 +22,7 @@
}
}
.dialog-enter,
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import ExpandMethods from './transition-expand-methods';
export default defineComponent({

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
@@ -34,7 +34,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-avatar {
position: relative;
display: flex;
@@ -48,35 +48,33 @@ body {
text-overflow: ellipsis;
background-color: var(--v-avatar-color);
border-radius: var(--border-radius);
}
&.tile {
border-radius: 0;
}
.tile {
border-radius: 0;
}
&.x-small {
--v-avatar-size: 24px;
.x-small {
--v-avatar-size: 24px;
border-radius: 2px;
}
border-radius: 2px;
}
&.small {
--v-avatar-size: 36px;
}
.small {
--v-avatar-size: 36px;
}
&.large {
--v-avatar-size: 64px;
}
.large {
--v-avatar-size: 64px;
}
&.x-large {
--v-avatar-size: 80px;
}
.x-large {
--v-avatar-size: 80px;
}
::v-deep {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
:slotted(img) {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {

View File

@@ -15,7 +15,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
interface Breadcrumb {
to: string;
@@ -46,8 +46,6 @@ body {
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-breadcrumb {
display: flex;
align-items: center;
@@ -93,7 +91,7 @@ body {
}
}
@include breakpoint(small) {
@media (min-width: 600px) {
font-size: inherit;
}
}

View File

@@ -1,4 +0,0 @@
import VButtonGroup from './v-button-group.vue';
export { VButtonGroup };
export default VButtonGroup;

View File

@@ -1,107 +0,0 @@
<template>
<div class="v-button-group" :class="{ rounded, tile }">
<v-item-group
:value="value"
:mandatory="mandatory"
:max="max"
:multiple="multiple"
scope="button-group"
@input="update"
>
<slot />
</v-item-group>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
mandatory: {
type: Boolean,
default: false,
},
max: {
type: Number,
default: -1,
},
multiple: {
type: Boolean,
default: false,
},
value: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
rounded: {
type: Boolean,
default: false,
},
tile: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
}
return { update };
},
});
</script>
<style>
body {
--v-button-group-background-color-active: var(--primary-alt);
}
</style>
<style lang="scss" scoped>
.v-button-group {
.v-item-group {
::v-deep .v-button {
--border-radius: 0px;
&:active {
transform: unset;
}
&.active {
--v-button-background-color: var(--v-button-group-background-color-active);
--v-button-background-color-hover: var(--v-button-group-background-color-active);
}
&:first-child {
--border-radius: var(--border-radius) 0px 0px var(--border-radius);
}
&:last-child {
--border-radius: 0px var(--border-radius) var(--border-radius) 0px;
}
}
}
&.tile .v-item-group ::v-deep .v-button {
&:first-child .button {
--border-radius: 0px;
}
&:last-child .button {
--border-radius: 0px;
}
}
&.rounded:not(.tile) .v-item-group ::v-deep .v-button {
&:first-child .button {
--border-radius: var(--v-button-height) 0px 0px var(--v-button-height);
}
&:last-child .button {
--border-radius: 0px var(--v-button-height) var(--v-button-height) 0px;
}
}
}
</style>

View File

@@ -4,8 +4,8 @@
<component
v-focus="autofocus"
:is="component"
:active-class="to ? 'activated' : null"
:exact="exact"
:active-class="!exact && to ? 'activated' : null"
:exact-active-class="exact && to ? 'activated' : null"
:download="download"
class="button"
:class="[
@@ -44,13 +44,14 @@
</template>
<script lang="ts">
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<string | Location>,
type: [String, Object] as PropType<string | RouteLocation>,
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({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-button-width: auto;
--v-button-height: 44px;
--v-button-color: var(--foreground-inverted);
@@ -167,187 +168,185 @@ body {
--v-button-line-height: 22px;
--v-button-min-width: 140px;
}
</style>
<style lang="scss" scoped>
.v-button {
display: inline-flex;
align-items: center;
}
&.secondary {
--v-button-color: var(--foreground-normal);
--v-button-color-hover: var(--foreground-normal);
--v-button-color-activated: var(--foreground-normal);
--v-button-background-color: var(--border-subdued); // I'm so sorry! 🥺
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-background-color-activated: var(--background-normal-alt);
}
.secondary {
--v-button-color: var(--foreground-normal);
--v-button-color-hover: var(--foreground-normal);
--v-button-color-activated: var(--foreground-normal);
--v-button-background-color: var(--border-subdued);
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-background-color-activated: var(--background-normal-alt);
}
&.full-width {
display: flex;
min-width: 100%;
}
.v-button.full-width {
display: flex;
min-width: 100%;
}
.button {
position: relative;
display: flex;
align-items: center;
width: var(--v-button-width);
min-width: var(--v-button-min-width);
height: var(--v-button-height);
padding: 0 19px;
color: var(--v-button-color);
font-weight: var(--v-button-font-weight);
font-size: var(--v-button-font-size);
line-height: var(--v-button-line-height);
text-decoration: none;
background-color: var(--v-button-background-color);
border: var(--border-width) solid var(--v-button-background-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--fast) var(--transition);
transition-property: background-color border;
.button {
position: relative;
display: flex;
align-items: center;
width: var(--v-button-width);
min-width: var(--v-button-min-width);
height: var(--v-button-height);
padding: 0 19px;
color: var(--v-button-color);
font-weight: var(--v-button-font-weight);
font-size: var(--v-button-font-size);
line-height: var(--v-button-line-height);
text-decoration: none;
background-color: var(--v-button-background-color);
border: var(--border-width) solid var(--v-button-background-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--fast) var(--transition);
transition-property: background-color border;
}
&:hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border-color: var(--v-button-background-color-hover);
}
.button:hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border-color: var(--v-button-background-color-hover);
}
&.align-left {
justify-content: flex-start;
}
.align-left {
justify-content: flex-start;
}
&.align-center {
justify-content: center;
}
.align-center {
justify-content: center;
}
&.align-right {
justify-content: flex-end;
}
.align-right {
justify-content: flex-end;
}
&:focus {
outline: 0;
}
.button:focus {
outline: 0;
}
&:disabled {
color: var(--v-button-color-disabled);
background-color: var(--v-button-background-color-disabled);
border: var(--border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
}
.button:disabled {
color: var(--v-button-color-disabled);
background-color: var(--v-button-background-color-disabled);
border: var(--border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
}
&.rounded {
border-radius: calc(var(--v-button-height) / 2);
}
.rounded {
border-radius: calc(var(--v-button-height) / 2);
}
&.outlined {
--v-button-color: var(--v-button-background-color);
.outlined {
--v-button-color: var(--v-button-background-color);
background-color: transparent;
background-color: transparent;
}
&:not(.activated):hover {
color: var(--v-button-background-color-hover);
background-color: transparent;
border-color: var(--v-button-background-color-hover);
}
.outlined:not(.activated):hover {
color: var(--v-button-background-color-hover);
background-color: transparent;
border-color: var(--v-button-background-color-hover);
}
&.secondary {
--v-button-color: var(--foreground-subdued);
}
}
.outlined.secondary {
--v-button-color: var(--foreground-subdued);
}
&.dashed {
border-style: dashed;
}
.dashed {
border-style: dashed;
}
&.x-small {
--v-button-height: 28px;
--v-button-font-size: 12px;
--v-button-font-weight: 600;
--v-button-min-width: 60px;
--border-radius: 4px;
.x-small {
--v-button-height: 28px;
--v-button-font-size: 12px;
--v-button-font-weight: 600;
--v-button-min-width: 60px;
--border-radius: 4px;
padding: 0 12px;
}
padding: 0 12px;
}
&.small {
--v-button-height: 36px;
--v-button-font-size: 14px;
--v-button-min-width: 120px;
.small {
--v-button-height: 36px;
--v-button-font-size: 14px;
--v-button-min-width: 120px;
padding: 0 12px;
}
padding: 0 12px;
}
&.large {
--v-button-height: 52px;
--v-button-min-width: 154px;
.large {
--v-button-height: 52px;
--v-button-min-width: 154px;
padding: 0 12px;
}
padding: 0 12px;
}
&.x-large {
--v-button-height: 64px;
--v-button-font-size: 18px;
--v-button-min-width: 180px;
.x-large {
--v-button-height: 64px;
--v-button-font-size: 18px;
--v-button-min-width: 180px;
padding: 0 12px;
}
padding: 0 12px;
}
&.icon {
width: var(--v-button-height);
min-width: 0;
padding: 0;
}
.icon {
width: var(--v-button-height);
min-width: 0;
padding: 0;
}
&.full-width {
min-width: 100%;
}
.button.full-width {
min-width: 100%;
}
.content,
.spinner {
max-width: 100%;
margin: 0 -1px; // Fixes slightly cropped icons
padding: 0 1px; // Fixes slightly cropped icons
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.content,
.spinner {
max-width: 100%;
margin: 0 -1px;
padding: 0 1px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.content {
position: relative;
display: flex;
align-items: center;
line-height: normal;
.content {
position: relative;
display: flex;
align-items: center;
line-height: normal;
}
&.invisible {
opacity: 0;
}
}
.content.invisible {
opacity: 0;
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.v-progress-circular {
--v-progress-circular-color: var(--v-button-color);
--v-progress-circular-background-color: transparent;
}
}
.spinner .v-progress-circular {
--v-progress-circular-color: var(--v-button-color);
--v-progress-circular-background-color: transparent;
}
&.activated,
&.active {
--v-button-color: var(--v-button-color-activated) !important;
--v-button-color-hover: var(--v-button-color-activated) !important;
--v-button-background-color: var(--v-button-background-color-activated) !important;
--v-button-background-color-hover: var(--v-button-background-color-activated) !important;
}
.activated,
.active {
--v-button-color: var(--v-button-color-activated) !important;
--v-button-color-hover: var(--v-button-color-activated) !important;
--v-button-background-color: var(--v-button-background-color-activated) !important;
--v-button-background-color-hover: var(--v-button-background-color-activated) !important;
}
&.tile {
border-radius: 0;
}
}
.tile {
border-radius: 0;
}
</style>

View File

@@ -1,15 +1,15 @@
<template functional>
<template>
<div class="v-card-actions"><slot /></div>
</template>
<style lang="scss" scoped>
<style scoped>
.v-card-actions {
display: flex;
justify-content: flex-end;
padding: var(--v-card-padding);
}
& ::v-deep > .v-button + .v-button {
margin-left: 12px;
}
.v-card-actions > :slotted(.v-button + .v-button) {
margin-left: 12px;
}
</style>

View File

@@ -1,4 +1,4 @@
<template function>
<template>
<div class="v-card-subtitle"><slot /></div>
</template>

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<div class="v-card-text"><slot /></div>
</template>

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<div class="v-card-title type-label"><slot /></div>
</template>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {

View File

@@ -9,31 +9,28 @@
:disabled="disabled"
:class="{ checked: isChecked, indeterminate, block }"
>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<div class="prepend" v-if="$slots.prepend"><slot name="prepend" /></div>
<v-icon class="checkbox" :name="icon" @click.stop="toggleInput" :disabled="disabled" />
<span class="label type-text">
<slot v-if="customValue === false">{{ label }}</slot>
<input @click.stop class="custom-input" v-else v-model="_value" />
<input @click.stop class="custom-input" v-else v-model="internalValue" />
</span>
<div class="append" v-if="$scopedSlots.append"><slot name="append" /></div>
<div class="append" v-if="$slots.append"><slot name="append" /></div>
</component>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSync from '@/composables/use-sync';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:indeterminate', 'update:modelValue', 'update:value'],
props: {
value: {
type: String,
default: null,
},
inputValue: {
modelValue: {
type: [Boolean, Array],
default: false,
},
@@ -71,14 +68,14 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const _value = useSync(props, 'value', emit);
const internalValue = useSync(props, 'value', emit);
const isChecked = computed<boolean>(() => {
if (props.inputValue instanceof Array) {
return props.inputValue.includes(props.value);
if (props.modelValue instanceof Array) {
return props.modelValue.includes(props.value);
}
return props.inputValue === true;
return props.modelValue === true;
});
const icon = computed<string>(() => {
@@ -86,15 +83,15 @@ export default defineComponent({
return isChecked.value ? props.iconOn : props.iconOff;
});
return { isChecked, toggleInput, icon, _value };
return { isChecked, toggleInput, icon, internalValue };
function toggleInput(): void {
if (props.indeterminate === true) {
emit('update:indeterminate', false);
}
if (props.inputValue instanceof Array) {
const newValue = [...props.inputValue];
if (props.modelValue instanceof Array) {
const newValue = [...props.modelValue];
if (isChecked.value === false) {
newValue.push(props.value);
@@ -102,9 +99,9 @@ export default defineComponent({
newValue.splice(newValue.indexOf(props.value), 1);
}
emit('change', newValue);
emit('update:modelValue', newValue);
} else {
emit('change', !isChecked.value);
emit('update:modelValue', !isChecked.value);
}
}
},
@@ -196,6 +193,7 @@ body {
.checkbox {
--v-icon-color: var(--primary);
}
&.block {
background-color: var(--background-subdued);
border-color: var(--border-normal-alt);

View File

@@ -1,5 +1,10 @@
<template>
<span v-if="_active" class="v-chip" :class="[sizeClass, { outlined, label, disabled, close }]" @click="onClick">
<span
v-if="internalActive"
class="v-chip"
:class="[sizeClass, { outlined, label, disabled, close }]"
@click="onClick"
>
<span class="chip-content">
<slot />
<span v-if="close" class="close-outline" :class="{ disabled }" @click.stop="onCloseClick">
@@ -10,10 +15,11 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { defineComponent, ref, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
emits: ['update:active', 'click', 'close'],
props: {
active: {
type: Boolean,
@@ -42,22 +48,22 @@ export default defineComponent({
...sizeProps,
},
setup(props, { emit }) {
const _localActive = ref(true);
const internalLocalActive = ref(true);
const _active = computed<boolean>({
const internalActive = computed<boolean>({
get: () => {
if (props.active !== null) return props.active;
return _localActive.value;
return internalLocalActive.value;
},
set: (active: boolean) => {
emit('update:active', active);
_localActive.value = active;
internalLocalActive.value = active;
},
});
const sizeClass = useSizeClass(props);
return { sizeClass, _active, onClick, onCloseClick };
return { sizeClass, internalActive, onClick, onCloseClick };
function onClick(event: MouseEvent) {
if (props.disabled) return;
@@ -66,7 +72,7 @@ export default defineComponent({
function onCloseClick(event: MouseEvent) {
if (props.disabled) return;
_active.value = !_active.value;
internalActive.value = !internalActive.value;
emit('close', event);
}
},
@@ -113,6 +119,7 @@ body {
color: var(--v-chip-color);
background-color: var(--v-chip-background-color);
border-color: var(--v-chip-background-color);
&:hover {
color: var(--v-chip-color);
background-color: var(--v-chip-background-color);

View File

@@ -1,11 +1,11 @@
<template>
<div class="v-detail" :class="{ disabled }">
<v-divider @click.native="_active = !_active">
<v-icon v-if="!disabled" :name="_active ? 'unfold_less' : 'unfold_more'" small />
<v-divider @click="internalActive = !internalActive">
<v-icon v-if="!disabled" :name="internalActive ? 'unfold_less' : 'unfold_more'" small />
<slot name="title">{{ label }}</slot>
</v-divider>
<transition-expand>
<div v-if="_active">
<div v-if="internalActive">
<slot />
</div>
</transition-expand>
@@ -13,23 +13,19 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import { defineComponent, computed, ref } from 'vue';
import { i18n } from '@/lang';
export default defineComponent({
model: {
prop: 'active',
event: 'toggle',
},
emits: ['update:modelValue'],
props: {
active: {
modelValue: {
type: Boolean,
default: undefined,
},
label: {
type: String,
default: i18n.t('toggle'),
default: i18n.global.t('toggle'),
},
startOpen: {
type: Boolean,
@@ -43,20 +39,20 @@ export default defineComponent({
setup(props, { emit }) {
const localActive = ref(props.startOpen);
const _active = computed({
const internalActive = computed({
get() {
if (props.active !== undefined) {
return props.active;
if (props.modelValue !== undefined) {
return props.modelValue;
}
return localActive.value;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
return { _active };
return { internalActive };
},
});
</script>
@@ -69,6 +65,7 @@ export default defineComponent({
.v-detail:not(.disabled) .v-divider {
--v-divider-label-color: var(--foreground-subdued);
&:hover {
--v-divider-label-color: var(--foreground-normal-alt);

View File

@@ -1,28 +1,28 @@
<template>
<div class="v-dialog">
<slot name="activator" v-bind="{ on: () => (_active = true) }" />
<slot name="activator" v-bind="{ on: () => (internalActive = true) }" />
<portal to="dialog-outlet">
<div v-if="_active" class="container" :class="[className, placement]" :key="id">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
</portal>
<teleport to="#dialog-outlet">
<transition-dialog @after-leave="leave">
<div v-if="internalActive" class="container" :class="[className, placement]">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
</transition-dialog>
</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { defineComponent, ref, computed } from 'vue';
import { nanoid } from 'nanoid';
import useShortcut from '@/composables/use-shortcut';
import { useDialogRouteLeave } from '@/composables/use-dialog-route';
export default defineComponent({
model: {
prop: 'active',
event: 'toggle',
},
emits: ['esc', 'update:modelValue'],
props: {
active: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -38,7 +38,7 @@ export default defineComponent({
},
setup(props, { emit }) {
useShortcut('escape', (event, cancelNext) => {
if (_active.value) {
if (internalActive.value) {
emit('esc');
cancelNext();
}
@@ -49,21 +49,23 @@ export default defineComponent({
const className = ref<string | null>(null);
const id = computed(() => nanoid());
const _active = computed({
const internalActive = computed({
get() {
return props.active !== undefined ? props.active : localActive.value;
return props.modelValue !== undefined ? props.modelValue : localActive.value;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
return { emitToggle, className, nudge, id, _active };
const leave = useDialogRouteLeave();
return { emitToggle, className, nudge, leave, id, internalActive };
function emitToggle() {
if (props.persistent === false) {
emit('toggle', !props.active);
emit('update:modelValue', !props.modelValue);
} else {
nudge();
}
@@ -81,8 +83,6 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-dialog {
--v-dialog-z-index: 100;
@@ -97,86 +97,90 @@ export default defineComponent({
display: flex;
width: 100%;
height: 100%;
}
::v-deep > * {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
.container > :slotted(*) {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
}
.container.center {
align-items: center;
justify-content: center;
}
.container.center.nudge > :slotted(*:not(:first-child)) {
animation: nudge 200ms;
}
.container.right {
align-items: center;
justify-content: flex-end;
}
.container.right.nudge > :slotted(*:not(:first-child)) {
transform-origin: right;
animation: shake 200ms;
}
.container :slotted(.v-card) {
--v-card-min-width: calc(100vw - 40px);
--v-card-padding: 28px;
--v-card-background-color: var(--background-page);
}
.container :slotted(.v-card) .v-card-title {
padding-bottom: 8px;
}
.container :slotted(.v-card) .v-card-actions {
flex-direction: column-reverse;
flex-wrap: wrap;
}
.container :slotted(.v-card) .v-card-actions .v-button {
width: 100%;
}
.container :slotted(.v-card) .v-card-actions .v-button .button {
width: 100%;
}
.container :slotted(.v-card) .v-card-actions > .v-button + .v-button {
margin-bottom: 20px;
margin-left: 0;
}
.container :slotted(.v-sheet) {
--v-sheet-padding: 24px;
--v-sheet-max-width: 560px;
}
.container .v-overlay {
--v-overlay-z-index: 1;
}
@media (min-width: 600px) {
.container :slotted(.v-card) {
--v-card-min-width: 540px;
}
&.center {
align-items: center;
justify-content: center;
&.nudge > ::v-deep *:not(:first-child) {
animation: nudge 200ms;
}
.container :slotted(.v-card) .v-card-actions {
flex-direction: inherit;
flex-wrap: nowrap;
}
&.right {
align-items: center;
justify-content: flex-end;
&.nudge > ::v-deep *:not(:first-child) {
transform-origin: right;
animation: shake 200ms;
}
.container :slotted(.v-card) .v-card-actions .v-button {
width: auto;
}
::v-deep .v-card {
--v-card-min-width: calc(100vw - 40px);
--v-card-padding: 28px;
--v-card-background-color: var(--background-page);
.v-card-title {
padding-bottom: 8px;
}
.v-card-actions {
flex-direction: column-reverse;
flex-wrap: wrap;
.v-button {
width: 100%;
.button {
width: 100%;
}
}
& > .v-button + .v-button {
margin-bottom: 20px;
margin-left: 0;
}
}
@include breakpoint(small) {
--v-card-min-width: 540px;
.v-card-actions {
flex-direction: inherit;
flex-wrap: nowrap;
.v-button {
width: auto;
.button {
width: auto;
}
}
& > .v-button + .v-button {
margin-bottom: 0;
margin-left: 12px;
}
}
}
.container :slotted(.v-card) .v-card-actions .v-button .button {
width: auto;
}
::v-deep .v-sheet {
--v-sheet-padding: 24px;
--v-sheet-max-width: 560px;
}
.v-overlay {
--v-overlay-z-index: 1;
.container :slotted(.v-card) .v-card-actions > .v-button + .v-button {
margin-bottom: 0;
margin-left: 12px;
}
}

View File

@@ -9,7 +9,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -60,7 +60,7 @@ body {
margin-right: 16px;
color: var(--v-divider-label-color);
.v-icon {
:slotted(.v-icon) {
margin-right: 4px;
transform: translateY(-1px);
}

View File

@@ -1,18 +1,18 @@
<template>
<v-dialog v-model="_active" @esc="$emit('cancel')" :persistent="persistent" placement="right">
<v-dialog v-model="internalActive" @esc="$emit('cancel')" :persistent="persistent" placement="right">
<template #activator="{ on }">
<slot name="activator" v-bind="{ on }" />
</template>
<article class="v-drawer">
<v-button
v-if="showCancel"
v-if="cancelable"
class="cancel"
@click="$emit('cancel')"
icon
rounded
secondary
v-tooltip.bottom="$t('cancel')"
v-tooltip.bottom="t('cancel')"
>
<v-icon name="close" />
</v-button>
@@ -57,18 +57,16 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, computed, provide } from 'vue';
import HeaderBar from '@/views/private/components/header-bar/header-bar.vue';
import i18n from '@/lang';
import { i18n } from '@/lang';
export default defineComponent({
emits: ['cancel', 'update:modelValue'],
components: {
HeaderBar,
},
model: {
prop: 'active',
event: 'toggle',
},
props: {
title: {
type: String,
@@ -78,7 +76,7 @@ export default defineComponent({
type: String,
default: null,
},
active: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -92,31 +90,33 @@ export default defineComponent({
},
sidebarLabel: {
type: String,
default: i18n.t('sidebar'),
default: i18n.global.t('sidebar'),
},
cancelable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
setup(props, { emit }) {
const { t } = useI18n();
const localActive = ref(false);
const mainEl = ref<Element>();
provide('main-element', mainEl);
const _active = computed({
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
return props.modelValue === undefined ? localActive.value : props.modelValue;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
const showCancel = computed(() => {
return 'cancel' in listeners;
});
return { _active, mainEl, showCancel };
return { t, internalActive, mainEl };
},
});
</script>
@@ -128,8 +128,6 @@ body {
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-drawer {
position: relative;
display: flex;
@@ -177,7 +175,7 @@ body {
display: none;
@include breakpoint(medium) {
@media (min-width: 960px) {
position: relative;
z-index: 2;
display: block;
@@ -193,7 +191,7 @@ body {
.v-overlay {
--v-overlay-z-index: 1;
@include breakpoint(medium) {
@media (min-width: 960px) {
--v-overlay-z-index: none;
display: none;
@@ -207,14 +205,14 @@ body {
flex-grow: 1;
overflow: auto;
@include breakpoint(small) {
@media (min-width: 600px) {
--content-padding: 32px;
--content-padding-bottom: 132px;
}
}
}
@include breakpoint(medium) {
@media (min-width: 960px) {
width: calc(100% - 64px);
}
}
@@ -227,7 +225,7 @@ body {
border-radius: var(--border-radius);
}
@include breakpoint(medium) {
@media (min-width: 960px) {
display: none;
}
}

View File

@@ -2,18 +2,20 @@
<div class="v-error selectable">
<output>[{{ code }}] {{ message }}</output>
<v-icon
v-tooltip="$t('copy_details')"
v-tooltip="t('copy_details')"
v-if="showCopy"
small
class="copy-error"
:name="copied ? 'check' : 'content_copy'"
clickable
@click="copyError"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType, ref } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, PropType, ref } from 'vue';
import { isPlainObject } from 'lodash';
export default defineComponent({
@@ -24,6 +26,8 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const code = computed(() => {
return props.error?.response?.data?.errors?.[0]?.extensions?.code || props.error?.extensions?.code || 'UNKNOWN';
});
@@ -42,7 +46,7 @@ export default defineComponent({
const showCopy = computed(() => !!navigator.clipboard?.writeText);
return { code, copyError, showCopy, copied, message };
return { t, code, copyError, showCopy, copied, message };
async function copyError() {
const error = props.error?.response?.data || props.error;

View File

@@ -1,10 +1,8 @@
import { TranslateResult } from 'vue-i18n';
export type FancySelectItem = {
icon: string;
value: string | number;
text: string | TranslateResult;
description?: string | TranslateResult;
text: string;
description?: string;
divider?: boolean;
iconRight?: string;
};

View File

@@ -1,13 +1,12 @@
<template>
<div class="v-fancy-select">
<transition-group tag="div" name="option">
<template v-for="(item, index) in visibleItems">
<v-divider :key="index" v-if="item.divider === true" />
<template v-for="(item, index) in visibleItems" :key="index">
<v-divider v-if="item.divider === true" />
<div
v-else
:key="item.value"
class="v-fancy-select-option"
:class="{ active: item.value === value, disabled }"
:class="{ active: item.value === modelValue, disabled }"
:style="{
'--index': index,
}"
@@ -22,7 +21,7 @@
<div class="description">{{ item.description }}</div>
</div>
<v-icon v-if="value === item.value && disabled === false" name="cancel" @click.stop="toggle(item)" />
<v-icon v-if="modelValue === item.value && disabled === false" name="cancel" @click.stop="toggle(item)" />
<v-icon class="icon-right" v-else-if="item.iconRight" :name="item.iconRight" />
</div>
</template>
@@ -31,16 +30,17 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { defineComponent, PropType, computed } from 'vue';
import { FancySelectItem } from './types';
export default defineComponent({
emits: ['update:modelValue'],
props: {
items: {
type: Array as PropType<FancySelectItem[]>,
required: true,
},
value: {
modelValue: {
type: [String, Number],
default: null,
},
@@ -51,10 +51,10 @@ export default defineComponent({
},
setup(props, { emit }) {
const visibleItems = computed(() => {
if (props.value === null) return props.items;
if (props.modelValue === null) return props.items;
return props.items.filter((item) => {
return item.value === props.value;
return item.value === props.modelValue;
});
});
@@ -62,8 +62,8 @@ export default defineComponent({
function toggle(item: FancySelectItem) {
if (props.disabled === true) return;
if (props.value === item.value) emit('input', null);
else emit('input', item.value);
if (props.modelValue === item.value) emit('update:modelValue', null);
else emit('update:modelValue', item.value);
}
},
});
@@ -147,7 +147,7 @@ export default defineComponent({
transition: all 500ms var(--transition);
}
.option-enter,
.option-enter-from,
.option-leave-to {
opacity: 0;
}

View File

@@ -1,31 +1,28 @@
<template>
<v-notice v-if="!availableFields || availableFields.length === 0">
{{ $t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
{{ t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
</v-notice>
<draggable
v-else
:force-fallback="true"
v-model="selectedFields"
item-key="field"
draggable=".draggable"
:set-data="hideDragImage"
class="v-field-select"
>
<v-chip
v-for="(field, index) in selectedFields"
:key="index"
class="field draggable"
v-tooltip="field.field"
@click="removeField(field.field)"
>
{{ field.name }}
</v-chip>
<template #item="{ element }">
<v-chip class="field draggable" v-tooltip="element.field" @click="removeField(element.field)">
{{ element.name }}
</v-chip>
</template>
<template #footer>
<v-menu show-arrow v-model="menuActive" class="add" placement="bottom">
<template #activator="{ toggle }">
<v-button @click="toggle" small>
{{ $t('add_field') }}
{{ t('add_field') }}
<v-icon small name="add" />
</v-button>
</template>
@@ -45,23 +42,25 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, toRefs, ref, PropType, computed } from 'vue';
import FieldListItem from '../v-field-template/field-list-item.vue';
import { Field, Collection, Relation } from '@/types';
import Draggable from 'vuedraggable';
import Draggable from 'vuedraggable/src/vuedraggable.js';
import useFieldTree from '@/composables/use-field-tree';
import useCollection from '@/composables/use-collection';
import { FieldTree } from '../v-field-template/types';
import hideDragImage from '@/utils/hide-drag-image';
export default defineComponent({
emits: ['update:modelValue'],
components: { FieldListItem, Draggable },
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Array as PropType<string[]>,
default: null,
},
@@ -79,30 +78,32 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const menuActive = ref(false);
const { collection, inject } = toRefs(props);
const { info } = useCollection(collection);
const { tree } = useFieldTree(collection, false, inject);
const _value = computed({
const internalValue = computed({
get() {
return props.value || [];
return props.modelValue || [];
},
set(newVal: string[]) {
emit('input', newVal);
emit('update:modelValue', newVal);
},
});
const selectedFields = computed({
get() {
return _value.value.map((field) => ({
return internalValue.value.map((field) => ({
field,
name: findTree(tree.value, field.split('.'))?.name as string,
}));
},
set(newVal: { field: string; name: string }[]) {
_value.value = newVal.map((field) => field.field);
internalValue.value = newVal.map((field) => field.field);
},
});
@@ -111,6 +112,7 @@ export default defineComponent({
});
return {
t,
menuActive,
addField,
removeField,
@@ -139,7 +141,7 @@ export default defineComponent({
name: field.name,
field: field.field,
key: field.key,
disabled: _value.value.includes(prefix + field.field),
disabled: internalValue.value.includes(prefix + field.field),
children: parseTree(field.children, prefix + field.field + '.'),
};
});
@@ -148,13 +150,13 @@ export default defineComponent({
}
function removeField(field: string) {
_value.value = _value.value.filter((f) => f !== field);
internalValue.value = internalValue.value.filter((f) => f !== field);
}
function addField(field: string) {
const newArray = _value.value;
const newArray = internalValue.value;
newArray.push(field);
_value.value = [...new Set(newArray)];
internalValue.value = [...new Set(newArray)];
}
},
});

View File

@@ -2,6 +2,7 @@
<v-list-item
v-if="field.children === undefined || depth === 0"
:disabled="field.disabled"
clickable
@click="$emit('add', `${parent ? parent + '.' : ''}${field.field}`)"
>
<v-list-item-content>{{ field.name || formatTitle(field.field) }}</v-list-item-content>
@@ -20,11 +21,12 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
import { FieldTree } from './types';
import formatTitle from '@directus/format-title';
export default defineComponent({
emits: ['add'],
name: 'field-list-item',
props: {
field: {

View File

@@ -1,8 +1,6 @@
import { TranslateResult } from 'vue-i18n';
export type FieldTree = {
field: string;
name: string | TranslateResult;
name: string;
key: string;
disabled?: boolean;
children?: FieldTree[];

View File

@@ -6,11 +6,11 @@
<span ref="contentEl" class="content" contenteditable @keydown="onKeyDown" @input="onInput" @click="onClick">
<span class="text" />
</span>
<span class="placeholder" v-if="placeholder && !value">{{ placeholder }}</span>
<span class="placeholder" v-if="placeholder && !modelValue">{{ placeholder }}</span>
</template>
<template #append>
<v-icon name="add_box" outline @click="toggle" :disabled="disabled" />
<v-icon name="add_box" outline clickable @click="toggle" :disabled="disabled" />
</template>
</v-input>
</template>
@@ -22,20 +22,21 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType } from '@vue/composition-api';
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType } from 'vue';
import FieldListItem from './field-list-item.vue';
import useFieldTree from '@/composables/use-field-tree';
import { FieldTree } from './types';
import { Field, Relation } from '@/types';
export default defineComponent({
emits: ['update:modelValue'],
components: { FieldListItem },
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
modelValue: {
type: String,
default: null,
},
@@ -68,7 +69,7 @@ export default defineComponent({
const { collection, inject } = toRefs(props);
const { tree } = useFieldTree(collection, true, inject);
watch(() => props.value, setContent, { immediate: true });
watch(() => props.modelValue, setContent, { immediate: true });
onMounted(() => {
if (contentEl.value) {
@@ -89,7 +90,7 @@ export default defineComponent({
if (!contentEl.value) return;
const valueString = getInputValue();
emit('input', valueString);
emit('update:modelValue', valueString);
}
function onClick(event: MouseEvent) {
@@ -98,7 +99,7 @@ export default defineComponent({
if (target.tagName.toLowerCase() !== 'button') return;
const field = target.dataset.field;
emit('input', props.value.replace(`{{${field}}}`, ''));
emit('update:modelValue', props.modelValue.replace(`{{${field}}}`, ''));
const before = target.previousElementSibling;
const after = target.nextElementSibling;
@@ -256,15 +257,15 @@ export default defineComponent({
function setContent() {
if (!contentEl.value) return;
if (props.value === null || props.value === '') {
if (props.modelValue === null || props.modelValue === '') {
contentEl.value.innerHTML = '<span class="text"></span>';
return;
}
if (props.value !== getInputValue()) {
if (props.modelValue !== getInputValue()) {
const regex = /({{.*?}})/g;
const newInnerHTML = props.value
const newInnerHTML = props.modelValue
.split(regex)
.map((part) => {
if (part.startsWith('{{') === false) {
@@ -285,7 +286,7 @@ export default defineComponent({
});
</script>
<style lang="scss" scoped>
<style scoped>
.content {
display: block;
flex-grow: 1;
@@ -295,47 +296,45 @@ export default defineComponent({
font-size: 14px;
font-family: var(--family-monospace);
white-space: nowrap;
}
::v-deep {
> * {
display: inline-block;
white-space: nowrap;
}
:deep(br) {
display: none;
}
br {
display: none;
}
:deep(span) {
min-width: 1px;
min-height: 1em;
}
span {
min-width: 1px;
min-height: 1em;
}
:deep(button) {
margin: -1px 4px 0;
padding: 2px 4px 0;
color: var(--primary);
background-color: var(--primary-alt);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: background-color, color;
user-select: none;
}
button {
margin: -1px 4px 0; // top offset for monospace
padding: 2px 4px 0; // top offset for monospace
color: var(--primary);
background-color: var(--primary-alt);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: background-color, color;
user-select: none;
:deep(button:hover) {
color: var(--white);
background-color: var(--danger);
}
&:hover {
color: var(--white);
background-color: var(--danger);
}
}
}
.placeholder {
position: absolute;
top: 50%;
left: 14px;
color: var(--foreground-subdued);
transform: translateY(-50%);
user-select: none;
pointer-events: none;
}
.placeholder {
position: absolute;
top: 50%;
left: 14px;
color: var(--foreground-subdued);
transform: translateY(-50%);
user-select: none;
pointer-events: none;
}
.content > :deep(*) {
display: inline-block;
white-space: nowrap;
}
</style>

View File

@@ -18,30 +18,32 @@
:autofocus="disabled !== true && autofocus"
:disabled="disabled"
:loading="loading"
:value="value === undefined ? field.schema.default_value : value"
:value="modelValue === undefined ? field.schema.default_value : modelValue"
:width="(field.meta && field.meta.width) || 'full'"
:type="field.type"
:collection="field.collection"
:field="field.field"
:primary-key="primaryKey"
:length="field.schema && field.schema.max_length"
@input="$emit('input', $event)"
@input="$emit('update:modelValue', $event)"
/>
<v-notice v-else type="warning">
{{ $t('interface_not_found', { interface: field.meta && field.meta.interface }) }}
{{ t('interface_not_found', { interface: field.meta && field.meta.interface }) }}
</v-notice>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed } from 'vue';
import { Field } from '@/types';
import { getInterfaces } from '@/interfaces';
import { getDefaultInterfaceForType } from '@/utils/get-default-interface-for-type';
import { InterfaceConfig } from '@/interfaces/types';
export default defineComponent({
emits: ['update:modelValue'],
props: {
field: {
type: Object as PropType<Field>,
@@ -59,7 +61,7 @@ export default defineComponent({
type: [Number, String],
default: null,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: null,
},
@@ -77,13 +79,15 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const { interfaces } = getInterfaces();
const interfaceExists = computed(() => {
return !!interfaces.value.find((inter: InterfaceConfig) => inter.id === props.field?.meta?.interface || 'input');
});
return { interfaceExists, getDefaultInterfaceForType };
return { t, interfaceExists, getDefaultInterfaceForType };
},
});
</script>

View File

@@ -2,11 +2,11 @@
<div class="label type-label" :class="{ disabled, edited: edited && !batchMode && !hasError }">
<v-checkbox
v-if="batchMode"
:input-value="batchActive"
:model-value="batchActive"
:value="field.field"
@change="$emit('toggle-batch', field)"
@update:model-value="$emit('toggle-batch', field)"
/>
<span @click="toggle" v-tooltip="edited ? $t('edited') : null">
<span @click="toggle" v-tooltip="edited ? t('edited') : null">
{{ field.name }}
<v-icon class="required" sup name="star" v-if="field.schema && field.schema.is_nullable === false" />
<v-icon v-if="!disabled" class="ctx-arrow" :class="{ active }" name="arrow_drop_down" />
@@ -15,10 +15,12 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType } from 'vue';
import { Field } from '@/types/';
export default defineComponent({
emits: ['toggle-batch'],
props: {
batchMode: {
type: Boolean,
@@ -53,6 +55,10 @@ export default defineComponent({
default: false,
},
},
setup() {
const { t } = useI18n();
return { t };
},
});
</script>
@@ -111,6 +117,7 @@ export default defineComponent({
content: '';
pointer-events: none;
}
> span {
margin-left: -16px;
padding-left: 16px;

View File

@@ -1,43 +1,56 @@
<template>
<v-list>
<v-list-item v-if="defaultValue === null || !isRequired" :disabled="value === null" @click="$emit('input', null)">
<v-list-item
v-if="defaultValue === null || !isRequired"
:disabled="modelValue === null"
clickable
@click="$emit('update:modelValue', null)"
>
<v-list-item-icon><v-icon name="delete_outline" /></v-list-item-icon>
<v-list-item-content>{{ $t('clear_value') }}</v-list-item-content>
<v-list-item-content>{{ t('clear_value') }}</v-list-item-content>
</v-list-item>
<v-list-item v-if="defaultValue !== null" :disabled="value === defaultValue" @click="$emit('input', defaultValue)">
<v-list-item
v-if="defaultValue !== null"
:disabled="modelValue === defaultValue"
clickable
@click="$emit('update:modelValue', defaultValue)"
>
<v-list-item-icon>
<v-icon name="settings_backup_restore" />
</v-list-item-icon>
<v-list-item-content>{{ $t('reset_to_default') }}</v-list-item-content>
<v-list-item-content>{{ t('reset_to_default') }}</v-list-item-content>
</v-list-item>
<v-list-item
v-if="initialValue"
:disabled="initialValue === undefined || value === initialValue"
:disabled="initialValue === undefined || modelValue === initialValue"
clickable
@click="$emit('unset', field)"
>
<v-list-item-icon>
<v-icon name="undo" />
</v-list-item-icon>
<v-list-item-content>{{ $t('undo_changes') }}</v-list-item-content>
<v-list-item-content>{{ t('undo_changes') }}</v-list-item-content>
</v-list-item>
<v-list-item @click="$emit('edit-raw')">
<v-list-item clickable @click="$emit('edit-raw')">
<v-list-item-icon><v-icon name="code" /></v-list-item-icon>
<v-list-item-content>{{ $t('raw_value') }}</v-list-item-content>
<v-list-item-content>{{ t('raw_value') }}</v-list-item-content>
</v-list-item>
</v-list>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed } from 'vue';
import { Field } from '@/types';
export default defineComponent({
emits: ['update:modelValue', 'unset', 'edit-raw'],
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: null,
},
@@ -47,6 +60,8 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const defaultValue = computed(() => {
const savedValue = props.field?.schema?.default_value;
return savedValue !== undefined ? savedValue : null;
@@ -56,7 +71,7 @@ export default defineComponent({
return props.field?.schema?.is_nullable === false;
});
return { defaultValue, isRequired };
return { t, defaultValue, isRequired };
},
});
</script>

View File

@@ -21,9 +21,9 @@
<form-field-menu
:field="field"
:value="_value"
:model-value="internalValue"
:initial-value="initialValue"
@input="emitValue($event)"
@update:model-value="emitValue($event)"
@unset="$emit('unset', $event)"
@edit-raw="showRaw = true"
/>
@@ -32,24 +32,24 @@
<form-field-interface
:autofocus="autofocus"
:value="_value"
:model-value="internalValue"
:field="field"
:loading="loading"
:batch-mode="batchMode"
:batch-active="batchActive"
:disabled="isDisabled"
:primary-key="primaryKey"
@input="emitValue($event)"
@update:model-value="emitValue($event)"
/>
<v-dialog v-model="showRaw" @esc="showRaw = false">
<v-card>
<v-card-title>{{ $t('edit_raw_value') }}</v-card-title>
<v-card-title>{{ t('edit_raw_value') }}</v-card-title>
<v-card-text>
<v-textarea class="raw-value" v-model="rawValue" :placeholder="$t('enter_raw_value')" />
<v-textarea class="raw-value" v-model="rawValue" :placeholder="t('enter_raw_value')" />
</v-card-text>
<v-card-actions>
<v-button @click="showRaw = false">{{ $t('done') }}</v-button>
<v-button @click="showRaw = false">{{ t('done') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
@@ -63,7 +63,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref } from 'vue';
import { Field } from '@/types/';
import { md } from '@/utils/md';
import FormFieldLabel from './form-field-label.vue';
@@ -72,9 +73,9 @@ import FormFieldInterface from './form-field-interface.vue';
import { ValidationError } from './types';
import { getJSType } from '@/utils/get-js-type';
import { isEqual } from 'lodash';
import { i18n } from '@/lang';
export default defineComponent({
emits: ['toggle-batch', 'unset', 'update:modelValue'],
components: { FormFieldLabel, FormFieldMenu, FormFieldInterface },
props: {
field: {
@@ -93,7 +94,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: undefined,
},
@@ -119,6 +120,8 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const isDisabled = computed(() => {
if (props.disabled) return true;
if (props.field?.meta?.readonly === true) return true;
@@ -133,14 +136,14 @@ export default defineComponent({
return null;
});
const _value = computed(() => {
if (props.value !== undefined) return props.value;
const internalValue = computed(() => {
if (props.modelValue !== undefined) return props.modelValue;
if (props.initialValue !== undefined) return props.initialValue;
return defaultValue.value;
});
const isEdited = computed<boolean>(() => {
return props.value !== undefined && isEqual(props.value, props.initialValue) === false;
return props.modelValue !== undefined && isEqual(props.modelValue, props.initialValue) === false;
});
const { showRaw, rawValue } = useRaw();
@@ -149,13 +152,13 @@ export default defineComponent({
if (!props.validationError) return null;
if (props.validationError.code === 'RECORD_NOT_UNIQUE') {
return i18n.t('validationError.unique');
return t('validationError.unique');
} else {
return i18n.t(`validationError.${props.validationError.type}`, props.validationError);
return t(`validationError.${props.validationError.type}`, props.validationError);
}
});
return { isDisabled, md, _value, emitValue, showRaw, rawValue, validationMessage, isEdited };
return { t, isDisabled, md, internalValue, emitValue, showRaw, rawValue, validationMessage, isEdited };
function emitValue(value: any) {
if (
@@ -165,7 +168,7 @@ export default defineComponent({
) {
emit('unset', props.field);
} else {
emit('input', value);
emit('update:modelValue', value);
}
}
@@ -180,30 +183,30 @@ export default defineComponent({
get() {
switch (type.value) {
case 'object':
return JSON.stringify(_value.value, null, '\t');
return JSON.stringify(internalValue.value, null, '\t');
case 'string':
case 'number':
case 'boolean':
default:
return _value.value;
return internalValue.value;
}
},
set(newRawValue: string) {
switch (type.value) {
case 'string':
emit('input', newRawValue);
emit('update:modelValue', newRawValue);
break;
case 'number':
emit('input', Number(newRawValue));
emit('update:modelValue', Number(newRawValue));
break;
case 'boolean':
emit('input', newRawValue === 'true');
emit('update:modelValue', newRawValue === 'true');
break;
case 'object':
emit('input', JSON.parse(newRawValue));
emit('update:modelValue', JSON.parse(newRawValue));
break;
default:
emit('input', newRawValue);
emit('update:modelValue', newRawValue);
break;
}
},

View File

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

View File

@@ -2,15 +2,15 @@
<div class="v-form" ref="el" :class="gridClass">
<v-notice type="danger" v-if="unknownValidationErrors.length > 0" class="full">
<div>
<p>{{ $t('unknown_validation_errors') }}</p>
<p>{{ t('unknown_validation_errors') }}</p>
<ul>
<li v-for="(validationError, index) of unknownValidationErrors" :key="index">
<strong v-if="validationError.field">{{ validationError.field }}:</strong>
<template v-if="validationError.code === 'RECORD_NOT_UNIQUE'">
{{ $t('validationError.unique', validationError) }}
{{ t('validationError.unique', validationError) }}
</template>
<template v-else>
{{ $t(`validationError.${validationError.code}`, validationError) }}
{{ t(`validationError.${validationError.code}`, validationError) }}
</template>
</li>
</ul>
@@ -22,7 +22,7 @@
:field="field"
:autofocus="index === firstEditableFieldIndex && autofocus"
:key="field.field"
:value="(edits || {})[field.field]"
:model-value="(modelValue || {})[field.field]"
:initial-value="(initialValues || {})[field.field]"
:disabled="disabled"
:batch-mode="batchMode"
@@ -30,7 +30,7 @@
:primary-key="primaryKey"
:loading="loading"
:validation-error="validationErrors.find((err) => err.field === field.field)"
@input="setValue(field, $event)"
@update:model-value="setValue(field, $event)"
@unset="unsetValue(field)"
@toggle-batch="toggleBatchField(field)"
/>
@@ -38,7 +38,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref, provide } from 'vue';
import { useFieldsStore } from '@/stores/';
import { Field, FieldRaw } from '@/types';
import { useElementSize } from '@/composables/use-element-size';
@@ -54,10 +55,8 @@ type FieldValues = {
};
export default defineComponent({
emits: ['update:modelValue'],
components: { FormField },
model: {
prop: 'edits',
},
props: {
collection: {
type: String,
@@ -71,7 +70,7 @@ export default defineComponent({
type: Object as PropType<FieldValues>,
default: null,
},
edits: {
modelValue: {
type: Object as PropType<FieldValues>,
default: null,
},
@@ -102,11 +101,13 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const el = ref<Element>();
const fieldsStore = useFieldsStore();
const values = computed(() => {
return Object.assign({}, props.initialValues, props.edits);
return Object.assign({}, props.initialValues, props.modelValue);
});
const { formFields, gridClass } = useForm();
@@ -134,6 +135,7 @@ export default defineComponent({
provide('values', values);
return {
t,
el,
formFields,
gridClass,
@@ -205,16 +207,16 @@ export default defineComponent({
}
function setValue(field: Field, value: any) {
const edits = props.edits ? clone(props.edits) : {};
const edits = props.modelValue ? clone(props.modelValue) : {};
edits[field.field] = value;
emit('input', edits);
emit('update:modelValue', edits);
}
function unsetValue(field: Field) {
if (field.field in props.edits || {}) {
const newEdits = { ...props.edits };
if (field.field in (props.modelValue || {})) {
const newEdits = { ...props.modelValue };
delete newEdits[field.field];
emit('input', newEdits);
emit('update:modelValue', newEdits);
}
}

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
import { defineComponent, ref } from 'vue';
export default defineComponent({
props: {

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 22 22"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,10 +1,10 @@
<template>
<span
class="v-icon"
:class="[sizeClass, { 'has-click': !disabled && hasClick, left, right }]"
:role="hasClick ? 'button' : null"
:class="[sizeClass, { 'has-click': !disabled && clickable, left, right }]"
:role="clickable ? 'button' : null"
@click="emitClick"
:tabindex="hasClick ? 0 : null"
:tabindex="clickable ? 0 : null"
:style="{ '--v-icon-color': color }"
>
<component v-if="customIconName" :is="customIconName" />
@@ -13,7 +13,7 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
import CustomIconDirectus from './custom-icons/directus.vue';
@@ -55,6 +55,7 @@ const customIcons: string[] = [
];
export default defineComponent({
emits: ['click'],
components: {
CustomIconDirectus,
CustomIconBookmarkSave,
@@ -99,13 +100,18 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
color: {
type: String,
default: null,
},
...sizeProps,
},
setup(props, { emit, listeners }) {
setup(props, { emit }) {
const sizeClass = computed<string | null>(() => {
if (props.sup) return 'sup';
return useSizeClass(props).value;
@@ -116,12 +122,9 @@ export default defineComponent({
return null;
});
const hasClick = computed<boolean>(() => 'click' in listeners);
return {
sizeClass,
customIconName,
hasClick,
emitClick,
};

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {

View File

@@ -1,29 +1,25 @@
<template>
<div
class="v-input"
@click="$emit('click', $event)"
:class="{ 'full-width': fullWidth, 'has-click': hasClick, disabled: disabled }"
>
<div class="v-input" @click="$emit('click', $event)" :class="classes">
<div v-if="$slots['prepend-outer']" class="prepend-outer">
<slot name="prepend-outer" :value="value" :disabled="disabled" />
<slot name="prepend-outer" :value="modelValue" :disabled="disabled" />
</div>
<div class="input" :class="{ disabled, active }">
<div v-if="$slots.prepend" class="prepend">
<slot name="prepend" :value="value" :disabled="disabled" />
<slot name="prepend" :value="modelValue" :disabled="disabled" />
</div>
<span v-if="prefix" class="prefix">{{ prefix }}</span>
<slot name="input">
<input
v-bind="$attrs"
v-bind="attributes"
v-focus="autofocus"
v-on="_listeners"
v-on="listeners"
:autocomplete="autocomplete"
:type="type"
:min="min"
:max="max"
:step="step"
:disabled="disabled"
:value="value"
:value="modelValue"
ref="input"
/>
</slot>
@@ -33,6 +29,7 @@
:class="{ disabled: !isStepUpAllowed }"
name="keyboard_arrow_up"
class="step-up"
clickable
@click="stepUp"
:disabled="!isStepUpAllowed"
/>
@@ -40,25 +37,28 @@
:class="{ disabled: !isStepDownAllowed }"
name="keyboard_arrow_down"
class="step-down"
clickable
@click="stepDown"
:disabled="!isStepDownAllowed"
/>
</span>
<div v-if="$slots.append" class="append">
<slot name="append" :value="value" :disabled="disabled" />
<slot name="append" :value="modelValue" :disabled="disabled" />
</div>
</div>
<div v-if="$slots['append-outer']" class="append-outer">
<slot name="append-outer" :value="value" :disabled="disabled" />
<slot name="append-outer" :value="modelValue" :disabled="disabled" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import { defineComponent, computed, ref } from 'vue';
import slugify from '@sindresorhus/slugify';
import { omit } from 'lodash';
export default defineComponent({
emits: ['click', 'keydown', 'update:modelValue'],
inheritAttrs: false,
props: {
autofocus: {
@@ -69,6 +69,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
prefix: {
type: String,
default: null,
@@ -81,7 +85,7 @@ export default defineComponent({
type: Boolean,
default: true,
},
value: {
modelValue: {
type: [String, Number],
default: null,
},
@@ -135,32 +139,36 @@ export default defineComponent({
default: 'off',
},
},
setup(props, { emit, listeners }) {
setup(props, { emit, attrs }) {
const input = ref<HTMLInputElement | null>(null);
const _listeners = computed(() => ({
...listeners,
const listeners = computed(() => ({
input: emitValue,
keydown: processValue,
blur: (e: Event) => {
trimIfEnabled();
listeners.blur?.(e);
attrs?.onBlur?.(e);
},
}));
const hasClick = computed(() => {
return listeners.click !== undefined;
});
const attributes = computed(() => omit(attrs, ['class']));
const classes = computed(() => [
{
'full-width': props.fullWidth,
'has-click': props.clickable,
disabled: props.disabled,
},
...((attrs.class || '') as string).split(' '),
]);
const isStepUpAllowed = computed(() => {
return props.disabled === false && (props.max === null || parseInt(String(props.value), 10) < props.max);
return props.disabled === false && (props.max === null || parseInt(String(props.modelValue), 10) < props.max);
});
const isStepDownAllowed = computed(() => {
return props.disabled === false && (props.min === null || parseInt(String(props.value), 10) > props.min);
return props.disabled === false && (props.min === null || parseInt(String(props.modelValue), 10) > props.min);
});
return { _listeners, hasClick, stepUp, stepDown, isStepUpAllowed, isStepDownAllowed, input };
return { listeners, attributes, classes, stepUp, stepDown, isStepUpAllowed, isStepDownAllowed, input };
function processValue(event: KeyboardEvent) {
if (!event.key) return;
@@ -201,8 +209,8 @@ export default defineComponent({
}
function trimIfEnabled() {
if (props.value && props.trim) {
emit('input', String(props.value).trim());
if (props.modelValue && props.trim) {
emit('update:modelValue', String(props.modelValue).trim());
}
}
@@ -210,12 +218,12 @@ export default defineComponent({
let value = (event.target as HTMLInputElement).value;
if (props.nullable === true && !value) {
emit('input', null);
emit('update:modelValue', null);
return;
}
if (props.type === 'number') {
emit('input', Number(value));
emit('update:modelValue', Number(value));
} else {
if (props.slug === true) {
const endsWithSpace = value.endsWith(' ');
@@ -230,7 +238,7 @@ export default defineComponent({
value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
emit('input', value);
emit('update:modelValue', value);
}
}
@@ -241,7 +249,7 @@ export default defineComponent({
input.value.stepUp();
if (input.value.value != null) {
return emit('input', Number(input.value.value));
return emit('update:modelValue', Number(input.value.value));
}
}
@@ -252,9 +260,9 @@ export default defineComponent({
input.value.stepDown();
if (input.value.value) {
return emit('input', Number(input.value.value));
return emit('update:modelValue', Number(input.value.value));
} else {
return emit('input', props.min || 0);
return emit('update:modelValue', props.min || 0);
}
}
},
@@ -396,6 +404,7 @@ body {
}
/* Firefox */
&[type='number'] {
-moz-appearance: textfield;
}
@@ -418,6 +427,7 @@ body {
input {
pointer-events: none;
.prefix,
.suffix {
color: var(--foreground-subdued);

View File

@@ -5,10 +5,11 @@
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs } from '@vue/composition-api';
import { defineComponent, PropType, toRefs } from 'vue';
import { useGroupableParent } from '@/composables/groupable';
export default defineComponent({
emits: ['update:modelValue'],
props: {
mandatory: {
type: Boolean,
@@ -22,7 +23,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
@@ -32,11 +33,11 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { value: selection, multiple, max, mandatory } = toRefs(props);
const { modelValue: selection, multiple, max, mandatory } = toRefs(props);
useGroupableParent(
{
selection: selection,
onSelectionChange: (newSelectionValues) => emit('input', newSelectionValues),
onSelectionChange: (newSelectionValues) => emit('update:modelValue', newSelectionValues),
},
{
multiple: multiple,

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent, toRefs } from '@vue/composition-api';
import { defineComponent, toRefs } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({

View File

@@ -7,6 +7,7 @@
:exact="exact"
:disabled="disabled"
:dense="dense"
clickable
@click="onClick"
>
<slot name="activator" :active="groupActive" />
@@ -23,10 +24,11 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({
emits: ['click'],
props: {
multiple: {
type: Boolean,
@@ -48,6 +50,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
scope: {
type: String,
default: undefined,
@@ -61,7 +67,7 @@ export default defineComponent({
default: false,
},
},
setup(props, { listeners, emit }) {
setup(props, { emit }) {
const { active: groupActive, toggle } = useGroupable({
group: props.scope,
value: props.value,
@@ -71,7 +77,7 @@ export default defineComponent({
function onClick(event: MouseEvent) {
if (props.to) return null;
if (listeners.click) return emit('click', event);
if (props.clickable) return emit('click', event);
event.stopPropagation();
toggle();

View File

@@ -11,7 +11,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-list-item-content {
display: flex;
flex-basis: 0;
@@ -23,28 +23,26 @@ body {
padding: var(--v-list-item-content-padding);
overflow: hidden;
font-family: var(--v-list-item-content-font-family);
}
.v-list.three-line &,
.v-list-item.three-line & {
align-self: stretch;
}
.v-list.three-line .v-list-item-content,
.v-list-item.three-line .v-list-item-content {
align-self: stretch;
}
::v-deep {
& > * {
flex-basis: 100%;
flex-grow: 1;
flex-shrink: 0;
line-height: 1.4;
.v-list-item-content > :deep(*) {
flex-basis: 100%;
flex-grow: 1;
flex-shrink: 0;
line-height: 1.4;
}
&:not(:last-child) {
margin-bottom: 2px;
}
}
}
.v-list-item-content > :slotted(*:not(:last-child)) {
margin-bottom: 2px;
}
.v-list:not(.large) &,
.v-list-item:not(.large) & {
--v-list-item-content-padding: 4px 0;
}
.v-list:not(.large) .v-list-item-content,
.v-list-item:not(.large) .v-list-item-content {
--v-list-item-content-padding: 4px 0;
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -30,6 +30,7 @@ export default defineComponent({
&:first-child {
margin-right: 12px;
}
&:last-child {
margin-left: 12px;
}
@@ -41,20 +42,24 @@ export default defineComponent({
#{$this} {
margin-top: 4px;
margin-bottom: 4px;
&:not(:only-child) {
&:first-child {
margin-right: 16px;
}
&:last-child {
margin-left: 16px;
}
}
}
&.large {
&.three-line,
&.two-line {
#{$this} {
align-self: flex-start;
&.center {
align-self: center;
}

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -61,11 +61,11 @@ body {
}
}
&.large #{$this} .v-icon {
&.large #{$this} :slotted(.v-icon) {
--v-icon-color: none;
}
&.disabled #{$this} .v-icon {
&.disabled #{$this} :slotted(.v-icon) {
--v-icon-color: var(--foreground-subdued) !important;
}
}

View File

@@ -1,14 +1,15 @@
<template>
<component
:is="component"
active-class="active"
v-bind="disabled === false && $attrs"
:active-class="!exact && to ? 'active' : null"
:exact-active-class="exact && to ? 'active' : null"
class="v-list-item"
:exact="exact"
:to="to"
:class="{
active,
dense,
link: isClickable,
link: isLink,
disabled,
dashed,
block,
@@ -17,15 +18,14 @@
:href="href"
:download="download"
:target="component === 'a' ? '_blank' : null"
v-on="disabled === false && $listeners"
>
<slot />
</component>
</template>
<script lang="ts">
import { Location } from 'vue-router';
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { RouteLocation } from 'vue-router';
import { defineComponent, PropType, computed } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({
@@ -39,7 +39,7 @@ export default defineComponent({
default: false,
},
to: {
type: [String, Object] as PropType<string | Location>,
type: [String, Object] as PropType<string | RouteLocation>,
default: null,
},
href: {
@@ -50,6 +50,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: false,
@@ -75,7 +79,7 @@ export default defineComponent({
default: false,
},
},
setup(props, { listeners }) {
setup(props) {
const component = computed<string>(() => {
if (props.to) return 'router-link';
if (props.href) return 'a';
@@ -86,9 +90,9 @@ export default defineComponent({
value: props.value,
});
const isClickable = computed(() => Boolean(props.to || props.href || listeners.click !== undefined));
const isLink = computed(() => Boolean(props.to || props.href || props.clickable));
return { component, isClickable };
return { component, isLink };
},
});
</script>
@@ -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);
}
}

View File

@@ -5,16 +5,13 @@
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs } from '@vue/composition-api';
import { defineComponent, PropType, toRefs } from 'vue';
import { useGroupableParent } from '@/composables/groupable';
export default defineComponent({
model: {
prop: 'activeItems',
event: 'input',
},
emits: ['update:modelValue'],
props: {
activeItems: {
modelValue: {
type: Array as PropType<(number | string)[]>,
default: null,
},
@@ -32,13 +29,12 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { activeItems, multiple, mandatory } = toRefs(props);
const { modelValue, multiple, mandatory } = toRefs(props);
useGroupableParent(
{
selection: activeItems,
selection: modelValue,
onSelectionChange: (newSelection) => {
emit('input', newSelection);
emit('update:modelValue', newSelection);
},
},
{
@@ -52,8 +48,8 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-list-padding: 4px 0;
--v-list-max-height: none;
--v-list-max-width: none;
@@ -65,9 +61,7 @@ body {
--v-list-background-color-hover: var(--background-normal);
--v-list-background-color-active: var(--background-normal);
}
</style>
<style lang="scss" scoped>
.v-list {
position: static;
display: block;
@@ -80,14 +74,18 @@ body {
color: var(--v-list-color);
line-height: 22px;
border-radius: var(--border-radius);
}
&.large {
--v-list-padding: 12px;
}
.large {
--v-list-padding: 12px;
}
::v-deep .v-divider {
max-width: calc(100% - 16px);
margin: 8px;
}
:slotted(.v-divider) {
max-width: calc(100% - 16px);
margin: 8px;
}
:slotted(*) {
pointer-events: all;
}
</style>

View File

@@ -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<HTMLElement | null>,
@@ -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,

View File

@@ -18,53 +18,54 @@
/>
</div>
<portal to="menu-outlet">
<div
v-if="isActive"
class="v-menu-popper"
:key="id"
:id="id"
:class="{ active: isActive, attached }"
:data-placement="popperPlacement"
:style="styles"
v-click-outside="{
handler: deactivate,
middleware: onClickOutsideMiddleware,
disabled: isActive === false || closeOnClick === false,
events: ['click'],
}"
>
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
<div class="v-menu-content" @click.stop="onContentClick">
<slot
:active="isActive"
v-bind="{
toggle: toggle,
active: isActive,
activate: activate,
deactivate: deactivate,
}"
/>
<teleport to="#menu-outlet">
<transition-bounce>
<div
v-if="isActive"
class="v-menu-popper"
:key="id"
:id="id"
:class="{ active: isActive, attached }"
:data-placement="popperPlacement"
:style="styles"
v-click-outside="{
handler: deactivate,
middleware: onClickOutsideMiddleware,
disabled: isActive === false || closeOnClick === false,
events: ['click'],
}"
>
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
<div class="v-menu-content" @click.stop="onContentClick">
<slot
v-bind="{
toggle: toggle,
active: isActive,
activate: activate,
deactivate: deactivate,
}"
/>
</div>
</div>
</div>
</portal>
</transition-bounce>
</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType, computed, watch } from '@vue/composition-api';
import { defineComponent, ref, PropType, computed, watch, nextTick } from 'vue';
import { usePopper } from './use-popper';
import { Placement } from '@popperjs/core';
import { nanoid } from 'nanoid';
import Vue from 'vue';
export default defineComponent({
emits: ['update:modelValue'],
props: {
placement: {
type: String as PropType<Placement>,
default: 'bottom',
},
value: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -138,9 +139,9 @@ export default defineComponent({
watch(isActive, (newActive) => {
if (newActive === true) {
reference.value = ((activator.value as HTMLElement)?.childNodes[0] as HTMLElement) || virtualReference.value;
reference.value = (activator.value?.children[0] as HTMLElement) || virtualReference.value;
Vue.nextTick(() => {
nextTick(() => {
popper.value = document.getElementById(id.value);
});
}
@@ -175,15 +176,15 @@ export default defineComponent({
const isActive = computed<boolean>({
get() {
if (props.value !== undefined) {
return props.value;
if (props.modelValue !== undefined) {
return props.modelValue;
}
return localIsActive.value;
},
async set(newActive) {
localIsActive.value = newActive;
emit('input', newActive);
emit('update:modelValue', newActive);
},
});

View File

@@ -6,7 +6,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import { defineComponent, computed, PropType } from 'vue';
export default defineComponent({
props: {
@@ -55,7 +55,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-notice {
display: flex;
align-items: center;
@@ -66,45 +66,43 @@ body {
color: var(--v-notice-color);
background-color: var(--v-notice-background-color);
border-radius: var(--border-radius);
}
.v-icon {
--v-icon-color: var(--v-notice-icon-color);
}
.v-icon {
--v-icon-color: var(--v-notice-icon-color);
}
&.info {
--v-notice-icon-color: var(--primary);
--v-notice-background-color: var(--background-normal);
--v-notice-color: var(--foreground-normal);
}
.info {
--v-notice-icon-color: var(--primary);
--v-notice-background-color: var(--background-normal);
--v-notice-color: var(--foreground-normal);
}
&.success {
--v-notice-icon-color: var(--success);
--v-notice-background-color: var(--success-alt);
--v-notice-color: var(--success);
}
.success {
--v-notice-icon-color: var(--success);
--v-notice-background-color: var(--success-alt);
--v-notice-color: var(--success);
}
&.warning {
--v-notice-icon-color: var(--warning);
--v-notice-background-color: var(--warning-alt);
--v-notice-color: var(--warning);
}
.warning {
--v-notice-icon-color: var(--warning);
--v-notice-background-color: var(--warning-alt);
--v-notice-color: var(--warning);
}
&.danger {
--v-notice-icon-color: var(--danger);
--v-notice-background-color: var(--danger-alt);
--v-notice-color: var(--danger);
}
.danger {
--v-notice-icon-color: var(--danger);
--v-notice-background-color: var(--danger-alt);
--v-notice-color: var(--danger);
}
&.center {
display: flex;
align-items: center;
justify-content: center;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
::v-deep {
a {
text-decoration: underline;
}
}
:slotted(a) {
text-decoration: underline;
}
</style>

View File

@@ -1,14 +1,15 @@
<template>
<div class="v-overlay" :class="{ active, absolute, 'has-click': hasClick }" @click="onClick">
<div class="v-overlay" :class="{ active, absolute, 'has-click': clickable }" @click="onClick">
<div class="overlay" />
<div v-if="active" class="content"><slot /></div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['click'],
props: {
active: {
type: Boolean,
@@ -18,11 +19,13 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
const hasClick = computed<boolean>(() => 'click' in listeners);
return { hasClick, onClick };
setup(props, { emit }) {
return { onClick };
function onClick(event: MouseEvent) {
emit('click', event);

View File

@@ -1,11 +1,11 @@
<template>
<div class="v-pagination">
<v-button class="previous" :disabled="disabled || value === 1" secondary icon small @click="toPrev">
<v-button class="previous" :disabled="disabled || modelValue === 1" secondary icon small @click="toPrev">
<v-icon name="chevron_left" />
</v-button>
<v-button
v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > totalVisible"
v-if="showFirstLast && modelValue > Math.ceil(totalVisible / 2) + 1 && length > totalVisible"
class="page"
@click="toPage(1)"
secondary
@@ -15,14 +15,14 @@
1
</v-button>
<span v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > totalVisible + 1" class="gap">
<span v-if="showFirstLast && modelValue > Math.ceil(totalVisible / 2) + 1 && length > totalVisible + 1" class="gap">
...
</span>
<v-button
v-for="page in visiblePages"
:key="page"
:class="{ active: value === page }"
:class="{ active: modelValue === page }"
class="page"
@click="toPage(page)"
secondary
@@ -32,13 +32,16 @@
{{ page }}
</v-button>
<span v-if="showFirstLast && value < length - Math.ceil(totalVisible / 2) && length > totalVisible + 1" class="gap">
<span
v-if="showFirstLast && modelValue < length - Math.ceil(totalVisible / 2) && length > totalVisible + 1"
class="gap"
>
...
</span>
<v-button
v-if="showFirstLast && value <= length - Math.ceil(totalVisible / 2) && length > totalVisible"
:class="{ active: value === length }"
v-if="showFirstLast && modelValue <= length - Math.ceil(totalVisible / 2) && length > totalVisible"
:class="{ active: modelValue === length }"
class="page"
@click="toPage(length)"
secondary
@@ -48,17 +51,18 @@
{{ length }}
</v-button>
<v-button class="next" :disabled="disabled || value === length" secondary icon small @click="toNext">
<v-button class="next" :disabled="disabled || modelValue === length" secondary icon small @click="toNext">
<v-icon name="chevron_right" />
</v-button>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import { isEmpty } from '@/utils/is-empty';
export default defineComponent({
emits: ['update:modelValue'],
props: {
disabled: {
type: Boolean,
@@ -75,7 +79,7 @@ export default defineComponent({
default: undefined,
validator: (val: number) => val >= 0,
},
value: {
modelValue: {
type: Number,
default: null,
},
@@ -96,15 +100,15 @@ export default defineComponent({
const pagesBeforeCurrentPage = Math.floor(props.totalVisible / 2);
const pagesAfterCurrentPage = Math.ceil(props.totalVisible / 2) - 1;
if (props.value <= pagesBeforeCurrentPage) {
if (props.modelValue <= pagesBeforeCurrentPage) {
startPage = 1;
endPage = props.totalVisible;
} else if (props.value + pagesAfterCurrentPage >= props.length) {
} else if (props.modelValue + pagesAfterCurrentPage >= props.length) {
startPage = props.length - props.totalVisible + 1;
endPage = props.length;
} else {
startPage = props.value - pagesBeforeCurrentPage;
endPage = props.value + pagesAfterCurrentPage;
startPage = props.modelValue - pagesBeforeCurrentPage;
endPage = props.modelValue + pagesAfterCurrentPage;
}
}
@@ -114,79 +118,77 @@ export default defineComponent({
return { toPage, toPrev, toNext, visiblePages };
function toPrev() {
toPage(props.value - 1);
toPage(props.modelValue - 1);
}
function toNext() {
toPage(props.value + 1);
toPage(props.modelValue + 1);
}
function toPage(page: number) {
emit('input', page);
emit('update:modelValue', page);
}
},
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-pagination-active-color: var(--primary);
}
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-pagination {
display: flex;
}
.gap {
display: none;
margin: 0 4px;
color: var(--foreground-subdued);
line-height: 2em;
}
@media (min-width: 600px) {
.gap {
display: none;
margin: 0 4px;
color: var(--foreground-subdued);
line-height: 2em;
@include breakpoint(small) {
display: inline;
}
}
.v-button {
--v-button-background-color-hover: var(--background-normal);
--v-button-background-color: var(--background-subdued);
--v-button-color: var(--foreground-normal);
margin: 0 2px;
vertical-align: middle;
&.page:not(.active) {
display: none;
@include breakpoint(small) {
display: inline;
}
}
& ::v-deep {
.small {
--v-button-min-width: 32px;
}
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&.active {
--v-button-background-color-hover: var(--primary);
--v-button-color-hover: var(--foreground-inverted);
--v-button-background-color: var(--primary);
--v-button-color: var(--foreground-inverted);
}
display: inline;
}
}
.v-button {
--v-button-background-color-hover: var(--background-normal);
--v-button-background-color: var(--background-subdued);
--v-button-color: var(--foreground-normal);
margin: 0 2px;
vertical-align: middle;
}
.v-button.page:not(.active) {
display: none;
}
@media (min-width: 600px) {
.v-button.page:not(.active) {
display: inline;
}
}
.v-button :deep(.small) {
--v-button-min-width: 32px;
}
.v-button:first-child {
margin-left: 0;
}
.v-button:last-child {
margin-right: 0;
}
.v-button.active {
--v-button-background-color-hover: var(--primary);
--v-button-color-hover: var(--foreground-inverted);
--v-button-background-color: var(--primary);
--v-button-color: var(--foreground-inverted);
}
</style>

View File

@@ -23,10 +23,11 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
emits: ['animationiteration'],
props: {
indeterminate: {
type: Boolean,

View File

@@ -22,9 +22,10 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['animationiteration'],
props: {
absolute: {
type: Boolean,

View File

@@ -15,19 +15,16 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:modelValue'],
props: {
value: {
type: String,
required: true,
},
inputValue: {
modelValue: {
type: String,
default: null,
},
@@ -54,7 +51,7 @@ export default defineComponent({
},
setup(props, { emit }) {
const isChecked = computed<boolean>(() => {
return props.inputValue === props.value;
return props.modelValue === props.value;
});
const icon = computed<string>(() => {
@@ -64,7 +61,7 @@ export default defineComponent({
return { isChecked, emitValue, icon };
function emitValue(): void {
emit('change', props.value);
emit('update:modelValue', props.value);
}
},
});

View File

@@ -15,25 +15,26 @@
v-else
:full-width="fullWidth"
readonly
:value="displayValue"
:model-value="displayValue"
clickable
@click="toggle"
:placeholder="placeholder"
:disabled="disabled"
:active="active"
>
<template #prepend><slot name="prepend" /></template>
<template v-if="$slots.prepend" #prepend><slot name="prepend" /></template>
<template #append><v-icon name="expand_more" :class="{ active }" /></template>
</v-input>
</template>
<v-list class="list">
<template v-if="showDeselect">
<v-list-item @click="$emit('input', null)" :disabled="value === null">
<v-list-item clickable @click="$emit('update:modelValue', null)" :disabled="modelValue === null">
<v-list-item-icon v-if="multiple === true">
<v-icon name="close" />
</v-list-item-icon>
<v-list-item-content>
{{ multiple ? $t('deselect_all') : $t('deselect') }}
{{ multiple ? t('deselect_all') : t('deselect') }}
</v-list-item-content>
<v-list-item-icon v-if="multiple === false">
<v-icon name="close" />
@@ -42,15 +43,15 @@
<v-divider />
</template>
<template v-for="(item, index) in _items">
<v-divider :key="index" v-if="item.divider === true" />
<template v-for="(item, index) in internalItems" :key="index">
<v-divider v-if="item.divider === true" />
<v-list-item
v-else
:key="item.text + item.value"
:active="multiple ? (value || []).includes(item.value) : value === item.value"
:active="multiple ? (modelValue || []).includes(item.value) : modelValue === item.value"
:disabled="item.disabled"
@click="multiple ? null : $emit('input', item.value)"
clickable
@click="multiple ? null : $emit('update:modelValue', item.value)"
>
<v-list-item-icon v-if="multiple === false && allowOther === false && itemIcon !== null && item.icon">
<v-icon :name="item.icon" />
@@ -59,11 +60,11 @@
<span v-if="multiple === false" class="item-text">{{ item.text }}</span>
<v-checkbox
v-else
:inputValue="value || []"
:model-value="modelValue || []"
:label="item.text"
:value="item.value"
:disabled="item.disabled"
@change="$emit('input', $event.length > 0 ? $event : null)"
@update:model-value="$emit('update:modelValue', $event.length > 0 ? $event : null)"
/>
</v-list-item-content>
</v-list-item>
@@ -73,9 +74,9 @@
<v-list-item-content>
<input
class="other-input"
@focus="otherValue ? $emit('input', otherValue) : null"
@focus="otherValue ? $emit('update:modelValue', otherValue) : null"
v-model="otherValue"
:placeholder="$t('other')"
:placeholder="t('other')"
/>
</v-list-item-content>
</v-list-item>
@@ -84,34 +85,34 @@
<v-list-item
v-for="otherValue in otherValues"
:key="otherValue.key"
:active="(value || []).includes(otherValue.value)"
:active="(modelValue || []).includes(otherValue.value)"
@click.stop
>
<v-list-item-icon>
<v-checkbox
:inputValue="value || []"
:model-value="modelValue || []"
:value="otherValue.value"
@change="$emit('input', $event.length > 0 ? $event : null)"
@update:model-value="$emit('update:modelValue', $event.length > 0 ? $event : null)"
/>
</v-list-item-icon>
<v-list-item-content>
<input
class="other-input"
:value="otherValue.value"
:placeholder="$t('other')"
:placeholder="t('other')"
v-focus
@input="setOtherValue(otherValue.key, $event.target.value)"
@blur="otherValue.value.length === 0 && setOtherValue(otherValue.key, null)"
/>
</v-list-item-content>
<v-list-item-icon>
<v-icon name="close" @click="setOtherValue(otherValue.key, null)" />
<v-icon name="close" clickable @click="setOtherValue(otherValue.key, null)" />
</v-list-item-icon>
</v-list-item>
<v-list-item @click.stop="addOtherValue()">
<v-list-item-icon><v-icon name="add" /></v-list-item-icon>
<v-list-item-content>{{ $t('other') }}</v-list-item-content>
<v-list-item-content>{{ t('other') }}</v-list-item-content>
</v-list-item>
</template>
</v-list>
@@ -119,8 +120,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRefs, Ref } from '@vue/composition-api';
import i18n from '@/lang';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, toRefs, Ref } from 'vue';
import { useCustomSelection, useCustomSelectionMultiple } from '@/composables/use-custom-selection';
import { get } from 'lodash';
@@ -128,6 +129,7 @@ type ItemsRaw = (string | any)[];
type InputValue = string[] | string;
export default defineComponent({
emits: ['update:modelValue'],
props: {
items: {
type: Array as PropType<ItemsRaw>,
@@ -149,7 +151,7 @@ export default defineComponent({
type: String,
default: 'disabled',
},
value: {
modelValue: {
type: [Array, String, Number, Boolean] as PropType<InputValue>,
default: null,
},
@@ -191,28 +193,24 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { _items } = useItems();
const { t } = useI18n();
const { internalItems } = useItems();
const { displayValue } = useDisplayValue();
const { value } = toRefs(props);
const { otherValue, usesOtherValue } = useCustomSelection(value as Ref<string>, _items, emit);
const { modelValue } = toRefs(props);
const { otherValue, usesOtherValue } = useCustomSelection(modelValue as Ref<string>, internalItems, (value) =>
emit('update:modelValue', value)
);
const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(
value as Ref<string[]>,
_items,
emit
modelValue as Ref<string[]>,
internalItems,
(value) => emit('update:modelValue', value)
);
return {
_items,
displayValue,
otherValue,
usesOtherValue,
otherValues,
addOtherValue,
setOtherValue,
};
return { t, internalItems, displayValue, otherValue, usesOtherValue, otherValues, addOtherValue, setOtherValue };
function useItems() {
const _items = computed(() => {
const internalItems = computed(() => {
const items = props.items.map((item) => {
if (typeof item === 'string') {
return {
@@ -234,50 +232,48 @@ export default defineComponent({
return items;
});
return { _items };
return { internalItems };
}
function useDisplayValue() {
const displayValue = computed(() => {
if (Array.isArray(props.value)) {
if (props.value.length < props.multiplePreviewThreshold) {
return props.value
if (Array.isArray(props.modelValue)) {
if (props.modelValue.length < props.multiplePreviewThreshold) {
return props.modelValue
.map((value) => {
return getTextForValue(value) || value;
})
.join(', ');
} else {
const itemCount = _items.value.length + otherValues.value.length;
const selectionCount = props.value.length;
const itemCount = internalItems.value.length + otherValues.value.length;
const selectionCount = props.modelValue.length;
if (itemCount === selectionCount) {
return i18n.t('all_items');
return t('all_items');
} else {
return i18n.tc('item_count', selectionCount);
return t('item_count', selectionCount);
}
}
}
return getTextForValue(props.value) || props.value;
return getTextForValue(props.modelValue) || props.modelValue;
});
return { displayValue };
function getTextForValue(value: string | number) {
return _items.value.find((item) => item.value === value)?.['text'];
return internalItems.value.find((item) => item.value === value)?.['text'];
}
}
},
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-select-font-family: var(--family-sans-serif);
}
</style>
<style lang="scss" scoped>
.list {
--v-list-min-width: 0;
}
@@ -290,19 +286,19 @@ body {
--v-input-font-family: var(--v-select-font-family);
cursor: pointer;
}
.v-icon {
transition: transform var(--medium) var(--transition-out);
.v-input .v-icon {
transition: transform var(--medium) var(--transition-out);
}
&.active {
transform: scaleY(-1);
transition-timing-function: var(--transition-in);
}
}
.v-input .v-icon.active {
transform: scaleY(-1);
transition-timing-function: var(--transition-in);
}
::v-deep input {
cursor: pointer;
}
.v-input :deep(input) {
cursor: pointer;
}
.other-input {
@@ -318,13 +314,13 @@ body {
width: max-content;
padding-right: 18px;
cursor: pointer;
}
.v-icon {
position: absolute;
}
.inline-display .v-icon {
position: absolute;
}
&.placeholder {
color: var(--foreground-subdued);
}
.inline-display.placeholder {
color: var(--foreground-subdued);
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {},

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -136,7 +136,7 @@ body {
transition: opacity var(--medium) var(--transition);
}
.fade-enter,
.fade-enter-from,
.fade-leave-to {
position: absolute;
opacity: 0;

View File

@@ -1,13 +1,13 @@
<template>
<div class="v-slider" :style="styles">
<div v-if="$slots['prepend']" class="prepend">
<slot name="prepend" :value="value" />
<div v-if="$slots.prepend" class="prepend">
<slot name="prepend" :value="modelValue" />
</div>
<div class="slider" :class="{ disabled }">
<input
:disabled="disabled"
type="range"
:value="value"
:value="modelValue"
:max="max"
:min="min"
:step="step"
@@ -20,22 +20,23 @@
</div>
<div v-if="showThumbLabel" class="thumb-label-wrapper">
<div class="thumb-label" :class="{ visible: alwaysShowValue }">
<slot name="thumb-label type-text" :value="value">
{{ value }}
<slot name="thumb-label type-text" :value="modelValue">
{{ modelValue }}
</slot>
</div>
</div>
</div>
<div v-if="$slots['append']" class="append">
<slot name="append" :value="value" />
<div v-if="$slots.append" class="append">
<slot name="append" :value="modelValue" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
emits: ['change', 'update:modelValue'],
props: {
disabled: {
type: Boolean,
@@ -65,16 +66,16 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Number,
default: 0,
},
},
setup(props, { emit }) {
const styles = computed(() => {
if (props.value === null) return { '--_v-slider-percentage': 50 };
if (props.modelValue === null) return { '--_v-slider-percentage': 50 };
let percentage = ((props.value - props.min) / (props.max - props.min)) * 100;
let percentage = ((props.modelValue - props.min) / (props.max - props.min)) * 100;
if (isNaN(percentage)) percentage = 0;
return { '--_v-slider-percentage': percentage };
});
@@ -92,7 +93,7 @@ export default defineComponent({
function onInput(event: InputEvent) {
const target = event.target as HTMLInputElement;
emit('input', Number(target.value));
emit('update:modelValue', Number(target.value));
}
},
});
@@ -244,6 +245,7 @@ body {
transform: translateX(-50%);
opacity: 0;
transition: opacity var(--fast) var(--transition);
&.visible {
opacity: 1;
}
@@ -253,6 +255,7 @@ body {
&:focus-within:not(.disabled) {
input {
height: 4px;
&::-webkit-slider-thumb {
width: 12px;
height: 12px;
@@ -269,6 +272,7 @@ body {
cursor: ew-resize;
}
}
.thumb-label {
opacity: 1;
}

View File

@@ -15,19 +15,16 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:modelValue'],
props: {
value: {
type: String,
default: null,
},
inputValue: {
modelValue: {
type: [Boolean, Array],
default: false,
},
@@ -42,18 +39,18 @@ export default defineComponent({
},
setup(props, { emit }) {
const isChecked = computed<boolean>(() => {
if (props.inputValue instanceof Array) {
return props.inputValue.includes(props.value);
if (props.modelValue instanceof Array) {
return props.modelValue.includes(props.value);
}
return props.inputValue === true;
return props.modelValue === true;
});
return { isChecked, toggleInput };
function toggleInput(): void {
if (props.inputValue instanceof Array) {
const newValue = [...props.inputValue];
if (props.modelValue instanceof Array) {
const newValue = [...props.modelValue];
if (isChecked.value === false) {
newValue.push(props.value);
@@ -61,9 +58,9 @@ export default defineComponent({
newValue.splice(newValue.indexOf(props.value), 1);
}
emit('change', newValue);
emit('update:modelValue', newValue);
} else {
emit('change', !isChecked.value);
emit('update:modelValue', !isChecked.value);
}
}
},

View File

@@ -8,11 +8,15 @@
@click="toggleManualSort"
scope="col"
>
<v-icon v-tooltip="$t('toggle_manual_sorting')" name="sort" small />
<v-icon v-tooltip="t('toggle_manual_sorting')" name="sort" small />
</th>
<th v-if="showSelect" class="select cell" scope="col">
<v-checkbox :inputValue="allItemsSelected" :indeterminate="someItemsSelected" @change="toggleSelectAll" />
<v-checkbox
:model-value="allItemsSelected"
:indeterminate="someItemsSelected"
@update:model-value="toggleSelectAll"
/>
</th>
<th v-for="header in headers" :key="header.value" :class="getClassesForHeader(header)" class="cell" scope="col">
@@ -27,7 +31,7 @@
name="sort"
class="sort-icon"
small
v-tooltip.top="$t(getTooltipForSortIcon(header))"
v-tooltip.top="t(getTooltipForSortIcon(header))"
/>
</div>
<span
@@ -45,12 +49,14 @@
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, PropType } from 'vue';
import useEventListener from '@/composables/use-event-listener';
import { Header, Sort } from '../types';
import { throttle, clone } from 'lodash';
export default defineComponent({
emits: ['update:sort', 'toggle-select-all', 'update:headers'],
props: {
headers: {
type: Array as PropType<Header[]>,
@@ -98,6 +104,8 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const dragging = ref<boolean>(false);
const dragStartX = ref<number>(0);
const dragStartWidth = ref<number>(0);
@@ -107,6 +115,7 @@ export default defineComponent({
useEventListener(window, 'pointerup', onMouseUp);
return {
t,
changeSort,
dragging,
dragHeader,
@@ -266,6 +275,7 @@ export default defineComponent({
.sortable {
cursor: pointer;
.sort-icon {
margin-left: 4px;
color: var(--foreground-subdued);

View File

@@ -1,33 +1,33 @@
<template functional>
<template>
<tr
class="table-row"
:class="{ subdued: props.subdued, clickable: props.hasClickListener }"
@click="listeners.click"
:class="{ subdued: subdued, clickable: hasClickListener }"
@click="$emit('click')"
:style="{
'--table-row-height': props.height + 2 + 'px',
'--table-row-height': height + 2 + 'px',
'--table-row-line-height': 1,
}"
>
<td v-if="props.showManualSort" class="manual cell" @click.stop>
<v-icon name="drag_handle" class="drag-handle" :class="{ 'sorted-manually': props.sortedManually }" />
<td v-if="showManualSort" class="manual cell" @click.stop>
<v-icon name="drag_handle" class="drag-handle" :class="{ 'sorted-manually': sortedManually }" />
</td>
<td v-if="props.showSelect" class="select cell" @click.stop>
<v-checkbox :inputValue="props.isSelected" @change="listeners['item-selected']" />
<td v-if="showSelect" class="select cell" @click.stop>
<v-checkbox :model-value="isSelected" @update:model-value="$emit('item-selected', $event)" />
</td>
<td class="cell" :class="`align-${header.align}`" v-for="header in props.headers" :key="header.value">
<slot :name="`item.${header.value}`" :item="props.item">
<td class="cell" :class="`align-${header.align}`" v-for="header in headers" :key="header.value">
<slot :name="`item.${header.value}`" :item="item">
<v-text-overflow
v-if="
header.value.split('.').reduce((acc, val) => {
return acc[val];
}, props.item)
}, item)
"
:text="
header.value.split('.').reduce((acc, val) => {
return acc[val];
}, props.item)
}, item)
"
/>
<value-null v-else />
@@ -35,17 +35,18 @@
</td>
<td class="spacer cell" />
<td v-if="$scopedSlots['item-append']" class="append cell" @click.stop>
<td v-if="$slots['item-append']" class="append cell" @click.stop>
<slot name="item-append" />
</td>
</tr>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
import { Header } from '../types';
export default defineComponent({
emits: ['click', 'item-selected'],
props: {
headers: {
type: Array as PropType<Header[]>,

View File

@@ -1,9 +1,7 @@
import VueI18n from 'vue-i18n';
export type Alignment = 'left' | 'center' | 'right';
export type HeaderRaw = {
text: string | VueI18n.TranslateResult;
text: string;
value: string;
align?: Alignment;
sortable?: boolean;

View File

@@ -1,14 +1,14 @@
<template>
<div class="v-table" :class="{ loading, inline, disabled }">
<table
:summary="_headers.map((header) => header.text).join(', ')"
:summary="internalHeaders.map((header) => header.text).join(', ')"
:style="{
'--grid-columns': columnStyle,
}"
>
<table-header
:headers.sync="_headers"
:sort.sync="_sort"
v-model:headers="internalHeaders"
v-model:sort="internalSort"
:show-select="showSelect"
:show-resize="showResize"
:some-items-selected="someItemsSelected"
@@ -20,7 +20,7 @@
:manual-sort-key="manualSortKey"
@toggle-select-all="onToggleSelectAll"
>
<template v-for="header in _headers" #[`header.${header.value}`]>
<template v-for="header in internalHeaders" #[`header.${header.value}`]>
<slot :header="header" :name="`header.${header.value}`" />
</template>
</table-header>
@@ -42,41 +42,42 @@
<draggable
:force-fallback="true"
v-else
v-model="_items"
v-model="internalItems"
:item-key="itemKey"
tag="tbody"
handle=".drag-handle"
:disabled="disabled || _sort.by !== manualSortKey"
:disabled="disabled || internalSort.by !== manualSortKey"
:set-data="hideDragImage"
@end="onSortChange"
>
<table-row
v-for="item in _items"
:key="item[itemKey]"
:headers="_headers"
:item="item"
:show-select="!disabled && showSelect"
:show-manual-sort="!disabled && showManualSort"
:is-selected="getSelectedState(item)"
:subdued="loading"
:sorted-manually="_sort.by === manualSortKey"
:has-click-listener="!disabled && hasRowClick"
:height="rowHeight"
@click="hasRowClick ? $emit('click:row', item) : null"
@item-selected="
onItemSelected({
item: item,
value: !getSelectedState(item),
})
"
>
<template v-for="header in _headers" #[`item.${header.value}`]>
<slot :item="item" :name="`item.${header.value}`" />
</template>
<template #item="{ element }">
<table-row
:headers="internalHeaders"
:item="element"
:show-select="!disabled && showSelect"
:show-manual-sort="!disabled && showManualSort"
:is-selected="getSelectedState(element)"
:subdued="loading"
:sorted-manually="internalSort.by === manualSortKey"
:has-click-listener="!disabled && clickable"
:height="rowHeight"
@click="clickable ? $emit('click:row', element) : null"
@item-selected="
onItemSelected({
item: element,
value: !getSelectedState(element),
})
"
>
<template v-for="header in internalHeaders" #[`item.${header.value}`]>
<slot :item="element" :name="`item.${header.value}`" />
</template>
<template v-if="hasItemAppendSlot" #item-append>
<slot name="item-append" :item="item" />
</template>
</table-row>
<template v-if="hasItemAppendSlot" #item-append>
<slot name="item-append" :item="element" />
</template>
</table-row>
</template>
</draggable>
</table>
<slot name="footer" />
@@ -84,13 +85,14 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref, PropType } from '@vue/composition-api';
import { defineComponent, computed, ref, PropType } from 'vue';
import { Header, HeaderRaw, Item, ItemSelectEvent, Sort } from './types';
import TableHeader from './table-header/';
import TableRow from './table-row/';
import { sortBy, clone, forEach, pick } from 'lodash';
import { i18n } from '@/lang/';
import draggable from 'vuedraggable';
// @TODO Use module import once vuedraggable exports an esm build or vite fixes umd imports
import Draggable from 'vuedraggable/src/vuedraggable.js';
import hideDragImage from '@/utils/hide-drag-image';
const HeaderDefaults: Header = {
@@ -102,14 +104,19 @@ const HeaderDefaults: Header = {
};
export default defineComponent({
emits: [
'click:row',
'update:sort',
'update:items',
'item-selected',
'update:modelValue',
'manual-sort',
'update:headers',
],
components: {
TableHeader,
TableRow,
draggable,
},
model: {
prop: 'selection',
event: 'select',
Draggable,
},
props: {
headers: {
@@ -148,7 +155,7 @@ export default defineComponent({
type: String,
default: null,
},
selection: {
modelValue: {
type: Array as PropType<any>,
default: () => [],
},
@@ -162,11 +169,11 @@ export default defineComponent({
},
loadingText: {
type: String,
default: i18n.t('loading'),
default: i18n.global.t('loading'),
},
noItemsText: {
type: String,
default: i18n.t('no_items'),
default: i18n.global.t('no_items'),
},
serverSort: {
type: Boolean,
@@ -188,9 +195,13 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners, slots }) {
const _headers = computed({
setup(props, { emit, slots }) {
const internalHeaders = computed({
get: () => {
return props.headers
.map((header: HeaderRaw) => ({
@@ -229,23 +240,23 @@ export default defineComponent({
// In case the sort prop isn't used, we'll use this local sort state as a fallback.
// This allows the table to allow inline sorting on column ootb without the need for
const _localSort = ref<Sort>({
const internalLocalSort = ref<Sort>({
by: null,
desc: false,
});
const _sort = computed({
get: () => props.sort || _localSort.value,
const internalSort = computed({
get: () => props.sort || internalLocalSort.value,
set: (newSort: Sort) => {
emit('update:sort', newSort);
_localSort.value = newSort;
internalLocalSort.value = newSort;
},
});
const hasItemAppendSlot = computed(() => slots['item-append'] !== undefined);
const fullColSpan = computed<string>(() => {
let length = _headers.value.length + 1; // +1 account for spacer
let length = internalHeaders.value.length + 1; // +1 account for spacer
if (props.showSelect) length++;
if (props.showManualSort) length++;
if (hasItemAppendSlot.value) length++;
@@ -253,35 +264,33 @@ export default defineComponent({
return `1 / span ${length}`;
});
const _items = computed({
const internalItems = computed({
get: () => {
if (props.serverSort === true || _sort.value.by === props.manualSortKey) {
if (props.serverSort === true || internalSort.value.by === props.manualSortKey) {
return props.items;
}
if (_sort.value.by === null) return props.items;
if (internalSort.value.by === null) return props.items;
const itemsSorted = sortBy(props.items, [_sort.value.by]);
if (_sort.value.desc === true) return itemsSorted.reverse();
const itemsSorted = sortBy(props.items, [internalSort.value.by]);
if (internalSort.value.desc === true) return itemsSorted.reverse();
return itemsSorted;
},
set: (value: Record<string, any>) => {
set: (value: Item[]) => {
emit('update:items', value);
},
});
const allItemsSelected = computed<boolean>(() => {
return props.loading === false && props.selection.length === props.items.length;
return props.loading === false && props.modelValue.length === props.items.length;
});
const someItemsSelected = computed<boolean>(() => {
return props.selection.length > 0 && allItemsSelected.value === false;
return props.modelValue.length > 0 && allItemsSelected.value === false;
});
const hasRowClick = computed<boolean>(() => 'click:row' in listeners);
const columnStyle = computed<string>(() => {
let gridTemplateColumns = _headers.value
let gridTemplateColumns = internalHeaders.value
.map((header) => {
return header.width ? `${header.width}px` : '160px';
})
@@ -298,16 +307,15 @@ export default defineComponent({
});
return {
_headers,
_items,
_sort,
internalHeaders,
internalItems,
internalSort,
allItemsSelected,
getSelectedState,
onItemSelected,
onToggleSelectAll,
someItemsSelected,
onSortChange,
hasRowClick,
fullColSpan,
columnStyle,
hasItemAppendSlot,
@@ -319,7 +327,7 @@ export default defineComponent({
emit('item-selected', event);
let selection = clone(props.selection) as any[];
let selection = clone(props.modelValue) as any[];
if (event.value === true) {
if (props.selectionUseKeys) {
@@ -337,13 +345,13 @@ export default defineComponent({
});
}
emit('select', selection);
emit('update:modelValue', selection);
}
function getSelectedState(item: Item) {
const selectedKeys = props.selectionUseKeys
? props.selection
: props.selection.map((item: any) => item[props.itemKey]);
? props.modelValue
: props.modelValue.map((item: any) => item[props.itemKey]);
return selectedKeys.includes(item[props.itemKey]);
}
@@ -353,14 +361,14 @@ export default defineComponent({
if (value === true) {
if (props.selectionUseKeys) {
emit(
'select',
'update:modelValue',
clone(props.items).map((item) => item[props.itemKey])
);
} else {
emit('select', clone(props.items));
emit('update:modelValue', clone(props.items));
}
} else {
emit('select', []);
emit('update:modelValue', []);
}
}
@@ -372,8 +380,8 @@ export default defineComponent({
function onSortChange(event: EndEvent) {
if (props.disabled) return;
const item = _items.value[event.oldIndex][props.itemKey];
const to = _items.value[event.newIndex][props.itemKey];
const item = internalItems.value[event.oldIndex][props.itemKey];
const to = internalItems.value[event.newIndex][props.itemKey];
emit('manual-sort', { item, to });
}
@@ -381,126 +389,122 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-table-height: auto;
--v-table-sticky-offset-top: 0;
--v-table-color: var(--foreground-normal);
--v-table-background-color: var(--background-input);
}
</style>
<style lang="scss" scoped>
.v-table {
position: relative;
height: var(--v-table-height);
overflow-y: auto;
}
table {
min-width: 100%;
border-collapse: collapse;
border-spacing: 0;
table {
min-width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
tbody {
display: contents;
}
table tbody {
display: contents;
}
::v-deep {
thead {
display: contents;
}
table :deep(thead) {
display: contents;
}
tr,
.loading-indicator {
display: grid;
grid-template-columns: var(--grid-columns);
}
table :deep(td),
table :deep(th) {
color: var(--v-table-color);
}
td,
th {
color: var(--v-table-color);
table :deep(tr),
table :deep(.loading-indicator) {
display: grid;
grid-template-columns: var(--grid-columns);
}
&.align-left {
text-align: left;
}
table :deep(td.align-left),
table :deep(th.align-left) {
text-align: left;
}
&.align-center {
text-align: center;
}
table :deep(td.align-center),
table :deep(th.align-center) {
text-align: center;
}
&.align-right {
text-align: right;
}
}
table :deep(td.align-right),
table :deep(th.align-right) {
text-align: right;
}
.loading-indicator {
position: relative;
z-index: 3;
table :deep(.loading-indicator) {
position: relative;
z-index: 3;
}
> th {
margin-right: var(--content-padding);
}
}
table :deep(.loading-indicator > th) {
margin-right: var(--content-padding);
}
.sortable-ghost {
.cell {
background-color: var(--background-subdued);
}
}
}
}
table :deep(.sortable-ghost .cell) {
background-color: var(--background-subdued);
}
&.loading {
table {
pointer-events: none;
}
.loading table {
pointer-events: none;
}
.loading-indicator {
height: auto;
padding: 0;
border: none;
.loading .loading-indicator {
height: auto;
padding: 0;
border: none;
}
.v-progress-linear {
--v-progress-linear-height: 2px;
--v-progress-linear-color: var(--border-normal-alt);
.loading .loading-indicator .v-progress-linear {
--v-progress-linear-height: 2px;
--v-progress-linear-color: var(--border-normal-alt);
position: absolute;
top: -2px;
left: 0;
width: 100%;
}
position: absolute;
top: -2px;
left: 0;
width: 100%;
}
th {
padding: 0;
}
.loading .loading-indicator th {
padding: 0;
}
&.sticky th {
position: sticky;
top: 48px;
z-index: 2;
}
}
}
.loading .loading-indicator.sticky th {
position: sticky;
top: 48px;
z-index: 2;
}
.loading-text,
.no-items-text {
text-align: center;
background-color: var(--background-input);
.loading-text,
.no-items-text {
text-align: center;
background-color: var(--background-input);
}
td {
padding: 16px;
color: var(--foreground-subdued);
}
}
.loading-text td,
.no-items-text td {
padding: 16px;
color: var(--foreground-subdued);
}
&.inline {
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
.inline {
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
}
table ::v-deep .table-row:last-of-type .cell {
border-bottom: none;
}
}
.inline table :deep(.table-row:last-of-type .cell) {
border-bottom: none;
}
.disabled {

Some files were not shown because too many files have changed in this diff Show More