mirror of
https://github.com/directus/directus.git
synced 2026-01-30 13:27:55 -05:00
Merge branch 'main' into aggregation
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
app/vite.config.js
|
||||
|
||||
@@ -25,7 +25,7 @@ module.exports = {
|
||||
overrides: [
|
||||
// Parse rollup configration as module
|
||||
{
|
||||
files: ['rollup.config.js'],
|
||||
files: ['rollup.config.js', 'vite.config.js'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
@@ -38,7 +38,7 @@ module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier-vue/recommended',
|
||||
@@ -58,8 +58,6 @@ module.exports = {
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
// Allow unused variables when they begin with an underscore
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
// Disable validity checks on v-slot directive (consider to enable this rule later on)
|
||||
'vue/valid-v-slot': 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
25
app/.gitignore
vendored
25
app/.gitignore
vendored
@@ -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
|
||||
@@ -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
149
app/index.html
Normal 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…</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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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…</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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import VButtonGroup from './v-button-group.vue';
|
||||
|
||||
export { VButtonGroup };
|
||||
export default VButtonGroup;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template function>
|
||||
<template>
|
||||
<div class="v-card-subtitle"><slot /></div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<div class="v-card-text"><slot /></div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<div class="v-card-title type-label"><slot /></div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 22 22"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template functional>
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[]>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user