mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into joins
This commit is contained in:
2
api/package-lock.json
generated
2
api/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
@@ -106,6 +106,7 @@
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"macos-release": "^2.4.1",
|
||||
"mime-types": "^2.1.27",
|
||||
"ms": "^2.1.2",
|
||||
"nanoid": "^3.1.12",
|
||||
"node-machine-id": "^1.1.12",
|
||||
|
||||
@@ -60,7 +60,6 @@ const newFieldSchema = Joi.object({
|
||||
field: Joi.string().required(),
|
||||
type: Joi.string().valid(...types, null),
|
||||
schema: Joi.object({
|
||||
comment: Joi.string().allow(null),
|
||||
default_value: Joi.any(),
|
||||
max_length: [Joi.number(), Joi.string(), Joi.valid(null)],
|
||||
is_nullable: Joi.bool(),
|
||||
|
||||
@@ -42,6 +42,8 @@ columns:
|
||||
column: id
|
||||
modified_on:
|
||||
type: timestamp
|
||||
nullable: false
|
||||
default: '$now'
|
||||
charset:
|
||||
type: string
|
||||
length: 50
|
||||
|
||||
@@ -135,15 +135,18 @@ fields:
|
||||
- field: key
|
||||
name: Key
|
||||
type: string
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: slug
|
||||
options:
|
||||
onlyOnCreate: false
|
||||
required: true
|
||||
width: half
|
||||
- field: fit
|
||||
name: Fit
|
||||
type: string
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: dropdown
|
||||
options:
|
||||
@@ -152,34 +155,35 @@ fields:
|
||||
text: Contain (preserve aspect ratio)
|
||||
- value: cover
|
||||
text: Cover (forces exact size)
|
||||
required: true
|
||||
width: half
|
||||
- field: width
|
||||
name: Width
|
||||
type: integer
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: numeric
|
||||
required: true
|
||||
width: half
|
||||
- field: height
|
||||
name: Height
|
||||
type: integer
|
||||
schema:
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: numeric
|
||||
required: true
|
||||
width: half
|
||||
- field: quality
|
||||
type: integer
|
||||
name: Quality
|
||||
schema:
|
||||
default_value: 80
|
||||
is_nullable: false
|
||||
meta:
|
||||
interface: slider
|
||||
options:
|
||||
max: 100
|
||||
min: 0
|
||||
step: 1
|
||||
required: true
|
||||
width: full
|
||||
template: '{{key}}'
|
||||
special: json
|
||||
|
||||
@@ -99,8 +99,9 @@ async function createTables(database: Knex) {
|
||||
if (columnInfo.default !== undefined) {
|
||||
let defaultValue = columnInfo.default;
|
||||
|
||||
if (isObject(defaultValue) || Array.isArray(defaultValue))
|
||||
if (isObject(defaultValue) || Array.isArray(defaultValue)) {
|
||||
defaultValue = JSON.stringify(defaultValue);
|
||||
}
|
||||
|
||||
if (defaultValue === '$now') {
|
||||
defaultValue = database!.fn.now();
|
||||
|
||||
@@ -72,7 +72,7 @@ function registerHooks(hooks: string[]) {
|
||||
registerHook(hook);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register hook "${hook}"`);
|
||||
logger.info(error);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ function registerEndpoints(endpoints: string[], router: Router) {
|
||||
registerEndpoint(endpoint);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register endpoint "${endpoint}"`);
|
||||
logger.info(error);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,22 +17,25 @@ import { ForbiddenException, FailedValidationException } from '../exceptions';
|
||||
import { uniq, merge, flatten } from 'lodash';
|
||||
import generateJoi from '../utils/generate-joi';
|
||||
import { ItemsService } from './items';
|
||||
import { PayloadService } from './payload';
|
||||
import { parseFilter } from '../utils/parse-filter';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
export class AuthorizationService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
payloadService: PayloadService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.payloadService = new PayloadService('directus_permissions', { knex: this.knex });
|
||||
}
|
||||
|
||||
async processAST(ast: AST, action: PermissionsAction = 'read'): Promise<AST> {
|
||||
const collectionsRequested = getCollectionsFromAST(ast);
|
||||
|
||||
const permissionsForCollections = await this.knex
|
||||
let permissionsForCollections = await this.knex
|
||||
.select<Permission[]>('*')
|
||||
.from('directus_permissions')
|
||||
.where({ action, role: this.accountability?.role })
|
||||
@@ -41,6 +44,11 @@ export class AuthorizationService {
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
);
|
||||
|
||||
permissionsForCollections = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permissionsForCollections
|
||||
)) as Permission[];
|
||||
|
||||
// If the permissions don't match the collections, you don't have permission to read all of them
|
||||
const uniqueCollectionsRequestedCount = uniq(
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
@@ -111,7 +119,7 @@ export class AuthorizationService {
|
||||
(permission) => permission.collection === collection
|
||||
)!;
|
||||
|
||||
const allowedFields = permissions.fields?.split(',') || [];
|
||||
const allowedFields = permissions.fields || [];
|
||||
|
||||
for (const childNode of ast.children) {
|
||||
if (childNode.type !== 'field') {
|
||||
@@ -213,21 +221,26 @@ export class AuthorizationService {
|
||||
permissions: {},
|
||||
validation: {},
|
||||
limit: null,
|
||||
fields: '*',
|
||||
fields: ['*'],
|
||||
presets: {},
|
||||
};
|
||||
} else {
|
||||
permission = await this.knex
|
||||
.select<Permission>('*')
|
||||
.select('*')
|
||||
.from('directus_permissions')
|
||||
.where({ action, collection, role: this.accountability?.role || null })
|
||||
.first();
|
||||
|
||||
permission = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permission as Item
|
||||
)) as Permission;
|
||||
|
||||
// Check if you have permission to access the fields you're trying to acces
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
const allowedFields = permission.fields?.split(',') || [];
|
||||
const allowedFields = permission.fields || [];
|
||||
|
||||
if (allowedFields.includes('*') === false) {
|
||||
for (const payload of payloads) {
|
||||
@@ -260,13 +273,16 @@ export class AuthorizationService {
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: column.name })
|
||||
.first();
|
||||
|
||||
const specials = (field?.special || '').split(',');
|
||||
|
||||
const hasGenerateSpecial = [
|
||||
'uuid',
|
||||
'date-created',
|
||||
'role-created',
|
||||
'user-created',
|
||||
].some((name) => specials.includes(name));
|
||||
|
||||
const isRequired =
|
||||
column.is_nullable === false &&
|
||||
column.has_auto_increment === false &&
|
||||
|
||||
@@ -260,7 +260,13 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
if (field.schema.default_value) {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
const defaultValue = field.schema.default_value.toLowerCase();
|
||||
|
||||
if (defaultValue === 'now()') {
|
||||
column.defaultTo(this.knex.fn.now());
|
||||
} else {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.schema.is_nullable !== undefined && field.schema.is_nullable === false) {
|
||||
|
||||
@@ -4,12 +4,13 @@ import sharp from 'sharp';
|
||||
import { parse as parseICC } from 'icc';
|
||||
import parseEXIF from 'exif-reader';
|
||||
import parseIPTC from '../utils/parse-iptc';
|
||||
import path from 'path';
|
||||
import { AbstractServiceOptions, File, PrimaryKey } from '../types';
|
||||
import { clone } from 'lodash';
|
||||
import cache from '../cache';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { extension } from 'mime-types';
|
||||
import path from 'path';
|
||||
|
||||
export class FilesService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
@@ -37,7 +38,10 @@ export class FilesService extends ItemsService {
|
||||
primaryKey = await this.create(payload);
|
||||
}
|
||||
|
||||
payload.filename_disk = primaryKey + path.extname(payload.filename_download);
|
||||
const fileExtension =
|
||||
(payload.type && extension(payload.type)) || path.extname(payload.filename_download);
|
||||
|
||||
payload.filename_disk = primaryKey + fileExtension;
|
||||
|
||||
if (!payload.type) {
|
||||
payload.type = 'application/octet-stream';
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
GraphQLInputObjectType,
|
||||
ObjectFieldNode,
|
||||
GraphQLID,
|
||||
ValueNode,
|
||||
FieldNode,
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLInt,
|
||||
@@ -26,11 +25,9 @@ import {
|
||||
StringValueNode,
|
||||
BooleanValueNode,
|
||||
ArgumentNode,
|
||||
GraphQLScalarType,
|
||||
GraphQLBoolean,
|
||||
ObjectValueNode,
|
||||
GraphQLUnionType,
|
||||
GraphQLUnionTypeConfig,
|
||||
} from 'graphql';
|
||||
import { getGraphQLType } from '../utils/get-graphql-type';
|
||||
import { RelationsService } from './relations';
|
||||
@@ -65,7 +62,7 @@ export class GraphQLService {
|
||||
this.knex = options?.knex || database;
|
||||
this.fieldsService = new FieldsService(options);
|
||||
this.collectionsService = new CollectionsService(options);
|
||||
this.relationsService = new RelationsService({ knex: this.knex });
|
||||
this.relationsService = new RelationsService(options);
|
||||
}
|
||||
|
||||
args = {
|
||||
@@ -138,6 +135,7 @@ export class GraphQLService {
|
||||
const relatedIsSystem = relationForField.one_collection!.startsWith(
|
||||
'directus_'
|
||||
);
|
||||
|
||||
const relatedType = relatedIsSystem
|
||||
? schema[relationForField.one_collection!.substring(9)].type
|
||||
: schema.items[relationForField.one_collection!].type;
|
||||
|
||||
@@ -25,7 +25,9 @@ export class RelationsService extends ItemsService {
|
||||
| ParsedRelation
|
||||
| ParsedRelation[]
|
||||
| null;
|
||||
|
||||
const filteredResults = await this.filterForbidden(results);
|
||||
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
@@ -58,6 +60,7 @@ export class RelationsService extends ItemsService {
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
);
|
||||
|
||||
const allowedFields = await this.permissionsService.getAllowedFields(
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
|
||||
@@ -26,7 +26,6 @@ export type FieldMeta = {
|
||||
interface: string | null;
|
||||
options: Record<string, any> | null;
|
||||
locked: boolean;
|
||||
required: boolean;
|
||||
readonly: boolean;
|
||||
hidden: boolean;
|
||||
sort: number | null;
|
||||
|
||||
@@ -9,5 +9,5 @@ export type Permission = {
|
||||
validation: Record<string, any>;
|
||||
limit: number | null;
|
||||
presets: Record<string, any> | null;
|
||||
fields: string | null;
|
||||
fields: string[] | null;
|
||||
};
|
||||
|
||||
2
app/package-lock.json
generated
2
app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"private": false,
|
||||
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
|
||||
"author": "Rijk van Zanten <rijk@rngr.org>",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<template #activator>{{ field.name || formatTitle(field.field) }}</template>
|
||||
<field-list-item
|
||||
v-for="childField in field.children"
|
||||
:key="childField.field"
|
||||
:key="field.field + childField.field"
|
||||
:parent="`${parent ? parent + '.' : ''}${field.field}`"
|
||||
:field="childField"
|
||||
:depth="depth - 1"
|
||||
|
||||
@@ -263,7 +263,7 @@ export default defineComponent({
|
||||
if (part.startsWith('{{') === false) {
|
||||
return `<span class="text">${part}</span>`;
|
||||
}
|
||||
const fieldKey = part.replaceAll(/({|})/g, '').trim();
|
||||
const fieldKey = part.replace(/({|})/g, '').trim();
|
||||
const field = findTree(tree.value, fieldKey.split('.'));
|
||||
|
||||
if (!field) return '';
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watch } from '@vue/composition-api';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
import uploadFile from '@/utils/upload-file';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection';
|
||||
import api from '@/api';
|
||||
import useItem from '@/composables/use-item';
|
||||
@@ -103,6 +104,10 @@ export default defineComponent({
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
fileId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fromUrl: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -155,16 +160,29 @@ export default defineComponent({
|
||||
try {
|
||||
numberOfFiles.value = files.length;
|
||||
|
||||
const uploadedFiles = await uploadFiles(Array.from(files), {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = Math.round(percentage.reduce((acc, cur) => (acc += cur)) / files.length);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
},
|
||||
preset: props.preset,
|
||||
});
|
||||
if (props.multiple === true) {
|
||||
const uploadedFiles = await uploadFiles(Array.from(files), {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = Math.round(
|
||||
percentage.reduce((acc, cur) => (acc += cur)) / files.length
|
||||
);
|
||||
done.value = percentage.filter((p) => p === 100).length;
|
||||
},
|
||||
preset: props.preset,
|
||||
});
|
||||
|
||||
if (uploadedFiles) {
|
||||
emit('input', props.multiple ? uploadedFiles : uploadedFiles[0]);
|
||||
uploadedFiles && emit('input', uploadedFiles);
|
||||
} else {
|
||||
const uploadedFile = await uploadFile(Array.from(files)[0], {
|
||||
onProgressChange: (percentage) => {
|
||||
progress.value = percentage;
|
||||
done.value = percentage === 100 ? 1 : 0;
|
||||
},
|
||||
fileId: props.fileId,
|
||||
preset: props.preset,
|
||||
});
|
||||
|
||||
uploadedFile && emit('input', uploadedFile);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -16,6 +16,20 @@ export function useCollection(collectionKey: string | Ref<string>) {
|
||||
return fieldsStore.getFieldsForCollection(collection.value);
|
||||
});
|
||||
|
||||
const defaults = computed(() => {
|
||||
if (!fields.value) return {};
|
||||
|
||||
const defaults: Record<string, any> = {};
|
||||
|
||||
for (const field of fields.value) {
|
||||
if (field.schema?.default_value) {
|
||||
defaults[field.field] = field.schema.default_value;
|
||||
}
|
||||
}
|
||||
|
||||
return defaults;
|
||||
});
|
||||
|
||||
const primaryKeyField = computed(() => {
|
||||
// Every collection has a primary key; rules of the land
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -32,5 +46,5 @@ export function useCollection(collectionKey: string | Ref<string>) {
|
||||
return info.value?.meta?.sort_field || null;
|
||||
});
|
||||
|
||||
return { info, fields, primaryKeyField, userCreatedField, sortField };
|
||||
return { info, fields, defaults, primaryKeyField, userCreatedField, sortField };
|
||||
}
|
||||
|
||||
@@ -9,14 +9,14 @@ import { APIError } from '@/types';
|
||||
export function useItem(collection: Ref<string>, primaryKey: Ref<string | number | null>) {
|
||||
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
|
||||
|
||||
const item = ref<any>(null);
|
||||
const item = ref<Record<string, any> | null>(null);
|
||||
const error = ref(null);
|
||||
const validationErrors = ref([]);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const deleting = ref(false);
|
||||
const archiving = ref(false);
|
||||
const edits = ref({});
|
||||
const edits = ref<Record<string, any>>({});
|
||||
const isNew = computed(() => primaryKey.value === '+');
|
||||
const isBatch = computed(() => typeof primaryKey.value === 'string' && primaryKey.value.includes(','));
|
||||
const isSingle = computed(() => !!collectionInfo.value?.meta?.singleton);
|
||||
@@ -203,7 +203,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
|
||||
if (unarchiveValue === 'false') unarchiveValue = false;
|
||||
|
||||
try {
|
||||
let value: any = item.value[field] === archiveValue ? unarchiveValue : archiveValue;
|
||||
let value: any = item.value && item.value[field] === archiveValue ? unarchiveValue : archiveValue;
|
||||
|
||||
if (value === 'true') value = true;
|
||||
if (value === 'false') value = false;
|
||||
|
||||
@@ -20,6 +20,8 @@ type Query = {
|
||||
export function useItems(collection: Ref<string>, query: Query) {
|
||||
const { primaryKeyField, sortField } = useCollection(collection);
|
||||
|
||||
let loadingTimeout: any = null;
|
||||
|
||||
const { limit, fields, sort, page, filters, searchQuery } = query;
|
||||
|
||||
const endpoint = computed(() => {
|
||||
@@ -103,9 +105,11 @@ export function useItems(collection: Ref<string>, query: Query) {
|
||||
return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems };
|
||||
|
||||
async function getItems() {
|
||||
if (loadingTimeout) return;
|
||||
|
||||
error.value = null;
|
||||
|
||||
const loadingTimeout = setTimeout(() => {
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loading.value = true;
|
||||
}, 250);
|
||||
|
||||
@@ -182,6 +186,7 @@ export function useItems(collection: Ref<string>, query: Query) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export default defineComponent({
|
||||
required: true,
|
||||
validator: (val: string) => ['dateTime', 'date', 'time', 'timestamp'].includes(val),
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'long',
|
||||
},
|
||||
relative: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -57,9 +61,18 @@ export default defineComponent({
|
||||
addSuffix: true,
|
||||
});
|
||||
} else {
|
||||
let format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
let format;
|
||||
if (props.format === 'long') {
|
||||
format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
} else if (props.format === 'short') {
|
||||
format = `${i18n.t('date-fns_date_short')} ${i18n.t('date-fns_time_short')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date_short'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time_short'));
|
||||
} else {
|
||||
format = props.format;
|
||||
}
|
||||
|
||||
displayValue.value = await localizedFormat(newValue, format);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,32 @@ export default defineDisplay(({ i18n }) => ({
|
||||
icon: 'query_builder',
|
||||
handler: DisplayDateTime,
|
||||
options: [
|
||||
{
|
||||
field: 'format',
|
||||
name: i18n.t('displays.datetime.format'),
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
options: {
|
||||
choices: [
|
||||
{ text: i18n.t('displays.datetime.long'), value: 'long' },
|
||||
{ text: i18n.t('displays.datetime.short'), value: 'short' },
|
||||
],
|
||||
allowOther: true,
|
||||
},
|
||||
note: i18n.t('displays.datetime.format_note'),
|
||||
},
|
||||
schema: {
|
||||
default_value: 'long',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'relative',
|
||||
name: i18n.t('displays.datetime.relative'),
|
||||
type: 'boolean',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'toggle',
|
||||
options: {
|
||||
label: i18n.t('displays.datetime.relative_label'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!collectionField" type="warning">
|
||||
<v-notice v-if="!collectionField && !collection" type="warning">
|
||||
{{ $t('collection_field_not_setup') }}
|
||||
</v-notice>
|
||||
<v-notice v-else-if="selectItems.length === 0" type="warning">
|
||||
@@ -27,6 +27,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
typeAllowList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
@@ -54,8 +58,8 @@ export default defineComponent({
|
||||
const values = inject('values', ref<Record<string, any>>({}));
|
||||
|
||||
const fields = computed(() => {
|
||||
if (!props.collectionField) return [];
|
||||
return fieldsStore.getFieldsForCollection(values.value[props.collectionField]);
|
||||
if (!props.collectionField && !props.collection) return [];
|
||||
return fieldsStore.getFieldsForCollection(props.collection || values.value[props.collectionField]);
|
||||
});
|
||||
|
||||
const selectItems = computed(() =>
|
||||
@@ -74,7 +78,7 @@ export default defineComponent({
|
||||
})
|
||||
);
|
||||
|
||||
return { selectItems };
|
||||
return { selectItems, values };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'note_add',
|
||||
component: InterfaceFile,
|
||||
types: ['uuid'],
|
||||
localTypes: ['file'],
|
||||
relationship: 'm2o',
|
||||
options: [],
|
||||
recommendedDisplays: ['file'],
|
||||
|
||||
@@ -97,6 +97,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortField: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: Array as PropType<(string | number | Record<string, any>)[] | null>,
|
||||
default: null,
|
||||
@@ -107,7 +111,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { collection, field, value, primaryKey } = toRefs(props);
|
||||
const { collection, field, value, primaryKey, sortField } = toRefs(props);
|
||||
|
||||
const { junction, junctionCollection, relation, relationCollection, relationInfo } = useRelation(
|
||||
collection,
|
||||
@@ -154,6 +158,7 @@ export default defineComponent({
|
||||
const { loading, error, items } = usePreview(
|
||||
value,
|
||||
fields,
|
||||
sortField,
|
||||
relationInfo,
|
||||
getNewSelectedItems,
|
||||
getUpdatedItems,
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'note_add',
|
||||
component: InterfaceFiles,
|
||||
types: ['alias'],
|
||||
localTypes: ['files'],
|
||||
relationship: 'm2m',
|
||||
options: [],
|
||||
recommendedDisplays: ['files'],
|
||||
|
||||
@@ -12,14 +12,24 @@
|
||||
"account_box",
|
||||
"account_circle",
|
||||
"add_shopping_cart",
|
||||
"addchart",
|
||||
"admin_panel_settings",
|
||||
"alarm",
|
||||
"alarm_add",
|
||||
"alarm_off",
|
||||
"alarm_on",
|
||||
"all_inbox",
|
||||
"all_out",
|
||||
"analytics",
|
||||
"anchor",
|
||||
"android",
|
||||
"announcement",
|
||||
"api",
|
||||
"app_blocking",
|
||||
"arrow_circle_down",
|
||||
"arrow_circle_up",
|
||||
"arrow_right_alt",
|
||||
"article",
|
||||
"aspect_ratio",
|
||||
"assessment",
|
||||
"assignment",
|
||||
@@ -30,15 +40,21 @@
|
||||
"assignment_turned_in",
|
||||
"autorenew",
|
||||
"backup",
|
||||
"backup_table",
|
||||
"batch_prediction",
|
||||
"book",
|
||||
"book_online",
|
||||
"bookmark",
|
||||
"bookmark_border",
|
||||
"bookmarks",
|
||||
"bug_report",
|
||||
"build",
|
||||
"build_circle",
|
||||
"cached",
|
||||
"calendar_today",
|
||||
"calendar_view_day",
|
||||
"camera_enhance",
|
||||
"cancel_schedule_send",
|
||||
"card_giftcard",
|
||||
"card_membership",
|
||||
"card_travel",
|
||||
@@ -47,10 +63,13 @@
|
||||
"check_circle_outline",
|
||||
"chrome_reader_mode",
|
||||
"class",
|
||||
"close_fullscreen",
|
||||
"code",
|
||||
"comment_bank",
|
||||
"commute",
|
||||
"compare_arrows",
|
||||
"contact_support",
|
||||
"contactless",
|
||||
"copyright",
|
||||
"credit_card",
|
||||
"dashboard",
|
||||
@@ -66,20 +85,26 @@
|
||||
"donut_large",
|
||||
"donut_small",
|
||||
"drag_indicator",
|
||||
"dynamic_form",
|
||||
"eco",
|
||||
"eject",
|
||||
"euro_symbol",
|
||||
"event",
|
||||
"event_seat",
|
||||
"exit_to_app",
|
||||
"explore",
|
||||
"explore_off",
|
||||
"extension",
|
||||
"face",
|
||||
"fact_check",
|
||||
"favorite",
|
||||
"favorite_border",
|
||||
"feedback",
|
||||
"filter_alt",
|
||||
"find_in_page",
|
||||
"find_replace",
|
||||
"fingerprint",
|
||||
"flaky",
|
||||
"flight_land",
|
||||
"flight_takeoff",
|
||||
"flip_to_back",
|
||||
@@ -89,13 +114,18 @@
|
||||
"get_app",
|
||||
"gif",
|
||||
"grade",
|
||||
"grading",
|
||||
"group_work",
|
||||
"help",
|
||||
"help_center",
|
||||
"help_outline",
|
||||
"highlight_alt",
|
||||
"highlight_off",
|
||||
"history",
|
||||
"history_toggle_off",
|
||||
"home",
|
||||
"horizontal_split",
|
||||
"hourglass_disabled",
|
||||
"hourglass_empty",
|
||||
"hourglass_full",
|
||||
"http",
|
||||
@@ -103,9 +133,11 @@
|
||||
"important_devices",
|
||||
"info",
|
||||
"input",
|
||||
"integration_instructions",
|
||||
"invert_colors",
|
||||
"label",
|
||||
"label_important",
|
||||
"label_off",
|
||||
"language",
|
||||
"launch",
|
||||
"line_style",
|
||||
@@ -113,21 +145,28 @@
|
||||
"list",
|
||||
"lock",
|
||||
"lock_open",
|
||||
"login",
|
||||
"loyalty",
|
||||
"markunread_mailbox",
|
||||
"maximize",
|
||||
"mediation",
|
||||
"minimize",
|
||||
"motorcycle",
|
||||
"note_add",
|
||||
"offline_bolt",
|
||||
"offline_pin",
|
||||
"online_prediction",
|
||||
"opacity",
|
||||
"open_in_browser",
|
||||
"open_in_full",
|
||||
"open_in_new",
|
||||
"open_with",
|
||||
"outlet",
|
||||
"pageview",
|
||||
"pan_tool",
|
||||
"payment",
|
||||
"pending",
|
||||
"pending_actions",
|
||||
"perm_camera_mic",
|
||||
"perm_contact_calendar",
|
||||
"perm_data_setting",
|
||||
@@ -139,27 +178,34 @@
|
||||
"pets",
|
||||
"picture_in_picture",
|
||||
"picture_in_picture_alt",
|
||||
"plagiarism",
|
||||
"play_for_work",
|
||||
"polymer",
|
||||
"power_settings_new",
|
||||
"pregnant_woman",
|
||||
"preview",
|
||||
"print",
|
||||
"privacy_tip",
|
||||
"query_builder",
|
||||
"question_answer",
|
||||
"quickreply",
|
||||
"receipt",
|
||||
"record_voice_over",
|
||||
"redeem",
|
||||
"remove_shopping_cart",
|
||||
"reorder",
|
||||
"report_problem",
|
||||
"request_page",
|
||||
"restore",
|
||||
"restore_from_trash",
|
||||
"restore_page",
|
||||
"room",
|
||||
"rounded_corner",
|
||||
"rowing",
|
||||
"rule",
|
||||
"schedule",
|
||||
"search",
|
||||
"search_off",
|
||||
"settings",
|
||||
"settings_applications",
|
||||
"settings_backup_restore",
|
||||
@@ -179,24 +225,35 @@
|
||||
"settings_voice",
|
||||
"shop",
|
||||
"shop_two",
|
||||
"shopping_bag",
|
||||
"shopping_basket",
|
||||
"shopping_cart",
|
||||
"smart_button",
|
||||
"source",
|
||||
"speaker_notes",
|
||||
"speaker_notes_off",
|
||||
"spellcheck",
|
||||
"star_rate",
|
||||
"stars",
|
||||
"store",
|
||||
"subject",
|
||||
"subtitles_off",
|
||||
"supervised_user_circle",
|
||||
"supervisor_account",
|
||||
"support",
|
||||
"swap_horiz",
|
||||
"swap_horizontal_circle",
|
||||
"swap_vert",
|
||||
"swap_vertical_circle",
|
||||
"sync_alt",
|
||||
"system_update_alt",
|
||||
"tab",
|
||||
"tab_unselected",
|
||||
"table_view",
|
||||
"text_rotate_up",
|
||||
"text_rotate_vertical",
|
||||
"text_rotation_angledown",
|
||||
"text_rotation_angleup",
|
||||
"text_rotation_down",
|
||||
"text_rotation_none",
|
||||
"theaters",
|
||||
@@ -208,6 +265,7 @@
|
||||
"today",
|
||||
"toll",
|
||||
"touch_app",
|
||||
"tour",
|
||||
"track_changes",
|
||||
"translate",
|
||||
"trending_down",
|
||||
@@ -216,6 +274,8 @@
|
||||
"turned_in",
|
||||
"turned_in_not",
|
||||
"update",
|
||||
"upgrade",
|
||||
"verified",
|
||||
"verified_user",
|
||||
"vertical_split",
|
||||
"view_agenda",
|
||||
@@ -227,15 +287,18 @@
|
||||
"view_list",
|
||||
"view_module",
|
||||
"view_quilt",
|
||||
"view_sidebar",
|
||||
"view_stream",
|
||||
"view_week",
|
||||
"visibility",
|
||||
"visibility_off",
|
||||
"voice_over_off",
|
||||
"watch_later",
|
||||
"wifi_protected_setup",
|
||||
"work",
|
||||
"work_off",
|
||||
"work_outline",
|
||||
"wysiwyg",
|
||||
"youtube_searched_for",
|
||||
"zoom_in",
|
||||
"zoom_out"
|
||||
@@ -245,6 +308,7 @@
|
||||
"name": "alert",
|
||||
"icons": [
|
||||
"add_alert",
|
||||
"auto_delete",
|
||||
"error",
|
||||
"error_outline",
|
||||
"notification_important",
|
||||
@@ -255,6 +319,7 @@
|
||||
"name": "av",
|
||||
"icons": [
|
||||
"4k",
|
||||
"5g",
|
||||
"add_to_queue",
|
||||
"airplay",
|
||||
"album",
|
||||
@@ -263,6 +328,7 @@
|
||||
"branding_watermark",
|
||||
"call_to_action",
|
||||
"closed_caption",
|
||||
"closed_caption_disabled",
|
||||
"control_camera",
|
||||
"equalizer",
|
||||
"explicit",
|
||||
@@ -281,8 +347,10 @@
|
||||
"games",
|
||||
"hd",
|
||||
"hearing",
|
||||
"hearing_disabled",
|
||||
"high_quality",
|
||||
"library_add",
|
||||
"library_add_check",
|
||||
"library_books",
|
||||
"library_music",
|
||||
"loop",
|
||||
@@ -300,6 +368,7 @@
|
||||
"pause_circle_outline",
|
||||
"play_arrow",
|
||||
"play_circle_filled",
|
||||
"play_circle_filled_white",
|
||||
"play_circle_outline",
|
||||
"playlist_add",
|
||||
"playlist_add_check",
|
||||
@@ -322,13 +391,16 @@
|
||||
"slow_motion_video",
|
||||
"snooze",
|
||||
"sort_by_alpha",
|
||||
"speed",
|
||||
"stop",
|
||||
"stop_circle",
|
||||
"subscriptions",
|
||||
"subtitles",
|
||||
"surround_sound",
|
||||
"video_call",
|
||||
"video_label",
|
||||
"video_library",
|
||||
"video_settings",
|
||||
"videocam",
|
||||
"videocam_off",
|
||||
"volume_down",
|
||||
@@ -342,6 +414,7 @@
|
||||
{
|
||||
"name": "communication",
|
||||
"icons": [
|
||||
"add_ic_call",
|
||||
"alternate_email",
|
||||
"business",
|
||||
"call",
|
||||
@@ -353,7 +426,7 @@
|
||||
"call_received",
|
||||
"call_split",
|
||||
"cancel_presentation",
|
||||
"cell_wifi",
|
||||
"wifi",
|
||||
"chat",
|
||||
"chat_bubble",
|
||||
"chat_bubble_outline",
|
||||
@@ -362,11 +435,17 @@
|
||||
"contact_mail",
|
||||
"contact_phone",
|
||||
"contacts",
|
||||
"desktop_access_disabled",
|
||||
"dialer_sip",
|
||||
"dialpad",
|
||||
"domain_disabled",
|
||||
"domain_verification",
|
||||
"duo",
|
||||
"email",
|
||||
"forum",
|
||||
"forward_to_inbox",
|
||||
"hourglass_bottom",
|
||||
"hourglass_top",
|
||||
"import_contacts",
|
||||
"import_export",
|
||||
"invert_colors_off",
|
||||
@@ -375,17 +454,31 @@
|
||||
"location_off",
|
||||
"location_on",
|
||||
"mail_outline",
|
||||
"mark_chat_read",
|
||||
"mark_chat_unread",
|
||||
"mark_email_read",
|
||||
"mark_email_unread",
|
||||
"message",
|
||||
"mobile_screen_share",
|
||||
"more_time",
|
||||
"nat",
|
||||
"no_sim",
|
||||
"pause_presentation",
|
||||
"person_add_disabled",
|
||||
"person_search",
|
||||
"phone",
|
||||
"phone_disabled",
|
||||
"phone_enabled",
|
||||
"phonelink_erase",
|
||||
"phonelink_lock",
|
||||
"phonelink_ring",
|
||||
"phonelink_setup",
|
||||
"portable_wifi_off",
|
||||
"present_to_all",
|
||||
"print_disabled",
|
||||
"qr_code",
|
||||
"qr_code_scanner",
|
||||
"read_more",
|
||||
"ring_volume",
|
||||
"rss_feed",
|
||||
"screen_share",
|
||||
@@ -400,7 +493,8 @@
|
||||
"textsms",
|
||||
"unsubscribe",
|
||||
"voicemail",
|
||||
"vpn_key"
|
||||
"vpn_key",
|
||||
"wifi_calling"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -410,14 +504,21 @@
|
||||
"add_box",
|
||||
"add_circle",
|
||||
"add_circle_outline",
|
||||
"amp_stories",
|
||||
"archive",
|
||||
"backspace",
|
||||
"ballot",
|
||||
"biotech",
|
||||
"block",
|
||||
"calculate",
|
||||
"clear",
|
||||
"content_copy",
|
||||
"content_cut",
|
||||
"content_paste",
|
||||
"create",
|
||||
"delete_sweep",
|
||||
"drafts",
|
||||
"dynamic_feed",
|
||||
"file_copy",
|
||||
"filter_list",
|
||||
"flag",
|
||||
@@ -427,6 +528,7 @@
|
||||
"how_to_reg",
|
||||
"how_to_vote",
|
||||
"inbox",
|
||||
"insights",
|
||||
"link",
|
||||
"link_off",
|
||||
"low_priority",
|
||||
@@ -435,6 +537,8 @@
|
||||
"move_to_inbox",
|
||||
"next_week",
|
||||
"outlined_flag",
|
||||
"policy",
|
||||
"push_pin",
|
||||
"redo",
|
||||
"remove",
|
||||
"remove_circle",
|
||||
@@ -448,6 +552,7 @@
|
||||
"select_all",
|
||||
"send",
|
||||
"sort",
|
||||
"square_foot",
|
||||
"text_format",
|
||||
"unarchive",
|
||||
"undo",
|
||||
@@ -462,6 +567,7 @@
|
||||
"access_alarm",
|
||||
"access_alarms",
|
||||
"access_time",
|
||||
"ad_units",
|
||||
"add_alarm",
|
||||
"add_to_home_screen",
|
||||
"airplanemode_active",
|
||||
@@ -469,7 +575,6 @@
|
||||
"battery_alert",
|
||||
"battery_charging_full",
|
||||
"battery_full",
|
||||
"battery_std",
|
||||
"battery_unknown",
|
||||
"bluetooth",
|
||||
"bluetooth_connected",
|
||||
@@ -484,15 +589,11 @@
|
||||
"devices",
|
||||
"dvr",
|
||||
"gps_fixed",
|
||||
"gps_not_fixed",
|
||||
"gps_off",
|
||||
"graphic_eq",
|
||||
"location_disabled",
|
||||
"location_searching",
|
||||
"mobile_friendly",
|
||||
"mobile_off",
|
||||
"network_cell",
|
||||
"network_wifi",
|
||||
"nfc",
|
||||
"screen_lock_landscape",
|
||||
"screen_lock_portrait",
|
||||
@@ -523,7 +624,6 @@
|
||||
"border_all",
|
||||
"border_bottom",
|
||||
"border_clear",
|
||||
"border_color",
|
||||
"border_horizontal",
|
||||
"border_inner",
|
||||
"border_left",
|
||||
@@ -540,9 +640,7 @@
|
||||
"format_align_right",
|
||||
"format_bold",
|
||||
"format_clear",
|
||||
"format_color_fill",
|
||||
"format_color_reset",
|
||||
"format_color_text",
|
||||
"format_indent_decrease",
|
||||
"format_indent_increase",
|
||||
"format_italic",
|
||||
@@ -559,9 +657,10 @@
|
||||
"format_textdirection_r_to_l",
|
||||
"format_underlined",
|
||||
"functions",
|
||||
"height",
|
||||
"highlight",
|
||||
"horizontal_rule",
|
||||
"insert_chart",
|
||||
"insert_chart_outlined",
|
||||
"insert_comment",
|
||||
"insert_drive_file",
|
||||
"insert_emoticon",
|
||||
@@ -576,6 +675,7 @@
|
||||
"multiline_chart",
|
||||
"notes",
|
||||
"pie_chart",
|
||||
"post_add",
|
||||
"publish",
|
||||
"scatter_plot",
|
||||
"score",
|
||||
@@ -583,7 +683,10 @@
|
||||
"show_chart",
|
||||
"space_bar",
|
||||
"strikethrough_s",
|
||||
"subscript",
|
||||
"superscript",
|
||||
"table_chart",
|
||||
"table_rows",
|
||||
"text_fields",
|
||||
"title",
|
||||
"vertical_align_bottom",
|
||||
@@ -595,6 +698,7 @@
|
||||
{
|
||||
"name": "file",
|
||||
"icons": [
|
||||
"attach_email",
|
||||
"attachment",
|
||||
"cloud",
|
||||
"cloud_circle",
|
||||
@@ -606,14 +710,21 @@
|
||||
"create_new_folder",
|
||||
"folder",
|
||||
"folder_open",
|
||||
"folder_shared"
|
||||
"folder_shared",
|
||||
"request_quote",
|
||||
"rule_folder",
|
||||
"snippet_folder",
|
||||
"text_snippet",
|
||||
"topic"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hardware",
|
||||
"icons": [
|
||||
"browser_not_supported",
|
||||
"cast",
|
||||
"cast_connected",
|
||||
"cast_for_education",
|
||||
"computer",
|
||||
"desktop_mac",
|
||||
"desktop_windows",
|
||||
@@ -646,6 +757,7 @@
|
||||
"phone_iphone",
|
||||
"phonelink",
|
||||
"phonelink_off",
|
||||
"point_of_sale",
|
||||
"power_input",
|
||||
"router",
|
||||
"scanner",
|
||||
@@ -663,6 +775,13 @@
|
||||
"watch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "home",
|
||||
"icons": [
|
||||
"sensor_door",
|
||||
"sensor_window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"icons": [
|
||||
@@ -673,6 +792,7 @@
|
||||
"assistant",
|
||||
"assistant_photo",
|
||||
"audiotrack",
|
||||
"bedtime",
|
||||
"blur_circular",
|
||||
"blur_linear",
|
||||
"blur_off",
|
||||
@@ -716,6 +836,7 @@
|
||||
"dehaze",
|
||||
"details",
|
||||
"edit",
|
||||
"euro",
|
||||
"exposure",
|
||||
"exposure_neg_1",
|
||||
"exposure_neg_2",
|
||||
@@ -746,6 +867,8 @@
|
||||
"flash_off",
|
||||
"flash_on",
|
||||
"flip",
|
||||
"flip_camera_android",
|
||||
"flip_camera_ios",
|
||||
"gradient",
|
||||
"grain",
|
||||
"grid_off",
|
||||
@@ -757,6 +880,7 @@
|
||||
"healing",
|
||||
"image",
|
||||
"image_aspect_ratio",
|
||||
"image_not_supported",
|
||||
"image_search",
|
||||
"iso",
|
||||
"landscape",
|
||||
@@ -773,6 +897,8 @@
|
||||
"looks_two",
|
||||
"loupe",
|
||||
"monochrome_photos",
|
||||
"motion_photos_on",
|
||||
"motion_photos_paused",
|
||||
"movie_creation",
|
||||
"movie_filter",
|
||||
"music_note",
|
||||
@@ -797,6 +923,7 @@
|
||||
"photo_size_select_small",
|
||||
"picture_as_pdf",
|
||||
"portrait",
|
||||
"receipt_long",
|
||||
"remove_red_eye",
|
||||
"rotate_90_degrees_ccw",
|
||||
"rotate_left",
|
||||
@@ -831,12 +958,20 @@
|
||||
"name": "maps",
|
||||
"icons": [
|
||||
"360",
|
||||
"add_business",
|
||||
"add_location",
|
||||
"add_location_alt",
|
||||
"add_road",
|
||||
"agriculture",
|
||||
"alt_route",
|
||||
"atm",
|
||||
"beenhere",
|
||||
"bike_scooter",
|
||||
"category",
|
||||
"cleaning_services",
|
||||
"compass_calibration",
|
||||
"departure_board",
|
||||
"design_services",
|
||||
"directions",
|
||||
"directions_bike",
|
||||
"directions_boat",
|
||||
@@ -849,10 +984,19 @@
|
||||
"directions_walk",
|
||||
"edit_attributes",
|
||||
"edit_location",
|
||||
"edit_road",
|
||||
"electric_bike",
|
||||
"electric_car",
|
||||
"electric_moped",
|
||||
"electric_scooter",
|
||||
"electrical_services",
|
||||
"ev_station",
|
||||
"fastfood",
|
||||
"flight",
|
||||
"handyman",
|
||||
"home_repair_service",
|
||||
"hotel",
|
||||
"hvac",
|
||||
"layers",
|
||||
"layers_clear",
|
||||
"local_activity",
|
||||
@@ -864,6 +1008,7 @@
|
||||
"local_convenience_store",
|
||||
"local_dining",
|
||||
"local_drink",
|
||||
"local_fire_department",
|
||||
"local_florist",
|
||||
"local_gas_station",
|
||||
"local_grocery_store",
|
||||
@@ -879,24 +1024,39 @@
|
||||
"local_phone",
|
||||
"local_pizza",
|
||||
"local_play",
|
||||
"local_police",
|
||||
"local_post_office",
|
||||
"local_printshop",
|
||||
"local_see",
|
||||
"local_shipping",
|
||||
"local_taxi",
|
||||
"map",
|
||||
"maps_ugc",
|
||||
"medical_services",
|
||||
"menu_book",
|
||||
"miscellaneous_services",
|
||||
"money",
|
||||
"moped",
|
||||
"multiple_stop",
|
||||
"museum",
|
||||
"my_location",
|
||||
"navigation",
|
||||
"near_me",
|
||||
"no_meals",
|
||||
"no_transfer",
|
||||
"not_listed_location",
|
||||
"pedal_bike",
|
||||
"person_pin",
|
||||
"person_pin_circle",
|
||||
"pest_control",
|
||||
"pest_control_rodent",
|
||||
"pin_drop",
|
||||
"place",
|
||||
"plumbing",
|
||||
"rate_review",
|
||||
"restaurant",
|
||||
"restaurant_menu",
|
||||
"run_circle",
|
||||
"satellite",
|
||||
"store_mall_directory",
|
||||
"streetview",
|
||||
@@ -908,12 +1068,15 @@
|
||||
"transfer_within_a_station",
|
||||
"transit_enterexit",
|
||||
"trip_origin",
|
||||
"two_wheeler",
|
||||
"wrong_location",
|
||||
"zoom_out_map"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "navigation",
|
||||
"icons": [
|
||||
"app_settings_alt",
|
||||
"apps",
|
||||
"arrow_back",
|
||||
"arrow_back_ios",
|
||||
@@ -926,30 +1089,47 @@
|
||||
"arrow_left",
|
||||
"arrow_right",
|
||||
"arrow_upward",
|
||||
"campaign",
|
||||
"cancel",
|
||||
"check",
|
||||
"chevron_left",
|
||||
"chevron_right",
|
||||
"close",
|
||||
"double_arrow",
|
||||
"east",
|
||||
"expand_less",
|
||||
"expand_more",
|
||||
"first_page",
|
||||
"fullscreen",
|
||||
"fullscreen_exit",
|
||||
"home_work",
|
||||
"last_page",
|
||||
"legend_toggle",
|
||||
"menu",
|
||||
"menu_open",
|
||||
"more_horiz",
|
||||
"more_vert",
|
||||
"north",
|
||||
"north_east",
|
||||
"north_west",
|
||||
"payments",
|
||||
"refresh",
|
||||
"south",
|
||||
"south_east",
|
||||
"south_west",
|
||||
"subdirectory_arrow_left",
|
||||
"subdirectory_arrow_right",
|
||||
"switch_left",
|
||||
"switch_right",
|
||||
"unfold_less",
|
||||
"unfold_more"
|
||||
"unfold_more",
|
||||
"west"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "notification",
|
||||
"icons": [
|
||||
"account_tree",
|
||||
"adb",
|
||||
"airline_seat_flat",
|
||||
"airline_seat_flat_angled",
|
||||
@@ -961,6 +1141,7 @@
|
||||
"airline_seat_recline_normal",
|
||||
"bluetooth_audio",
|
||||
"confirmation_number",
|
||||
"directions_off",
|
||||
"disc_full",
|
||||
"drive_eta",
|
||||
"enhanced_encryption",
|
||||
@@ -989,6 +1170,7 @@
|
||||
"sd_card",
|
||||
"sms",
|
||||
"sms_failed",
|
||||
"support_agent",
|
||||
"sync",
|
||||
"sync_disabled",
|
||||
"sync_problem",
|
||||
@@ -1010,57 +1192,135 @@
|
||||
"ac_unit",
|
||||
"airport_shuttle",
|
||||
"all_inclusive",
|
||||
"apartment",
|
||||
"baby_changing_station",
|
||||
"backpack",
|
||||
"bathtub",
|
||||
"beach_access",
|
||||
"business_center",
|
||||
"casino",
|
||||
"charging_station",
|
||||
"checkroom",
|
||||
"child_care",
|
||||
"child_friendly",
|
||||
"corporate_fare",
|
||||
"do_not_step",
|
||||
"do_not_touch",
|
||||
"dry",
|
||||
"elevator",
|
||||
"escalator",
|
||||
"escalator_warning",
|
||||
"family_restroom",
|
||||
"fire_extinguisher",
|
||||
"fitness_center",
|
||||
"free_breakfast",
|
||||
"golf_course",
|
||||
"grass",
|
||||
"hot_tub",
|
||||
"house",
|
||||
"kitchen",
|
||||
"meeting_room",
|
||||
"no_cell",
|
||||
"no_drinks",
|
||||
"no_flash",
|
||||
"no_food",
|
||||
"no_meeting_room",
|
||||
"no_photography",
|
||||
"no_stroller",
|
||||
"pool",
|
||||
"room_preferences",
|
||||
"room_service",
|
||||
"rv_hookup",
|
||||
"smoke_free",
|
||||
"smoking_rooms",
|
||||
"spa"
|
||||
"soap",
|
||||
"spa",
|
||||
"stairs",
|
||||
"storefront",
|
||||
"stroller",
|
||||
"tty",
|
||||
"umbrella",
|
||||
"wash",
|
||||
"wheelchair_pickup"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "social",
|
||||
"icons": [
|
||||
"6_ft_apart",
|
||||
"architecture",
|
||||
"cake",
|
||||
"clean_hands",
|
||||
"construction",
|
||||
"deck",
|
||||
"domain",
|
||||
"emoji_emotions",
|
||||
"emoji_events",
|
||||
"emoji_flags",
|
||||
"emoji_food_beverage",
|
||||
"emoji_nature",
|
||||
"emoji_objects",
|
||||
"emoji_people",
|
||||
"emoji_symbols",
|
||||
"emoji_transportation",
|
||||
"engineering",
|
||||
"fireplace",
|
||||
"group",
|
||||
"group_add",
|
||||
"history_edu",
|
||||
"king_bed",
|
||||
"location_city",
|
||||
"luggage",
|
||||
"military_tech",
|
||||
"mood",
|
||||
"mood_bad",
|
||||
"nights_stay",
|
||||
"no_luggage",
|
||||
"notifications",
|
||||
"notifications_active",
|
||||
"notifications_none",
|
||||
"notifications_off",
|
||||
"notifications_paused",
|
||||
"outdoor_grill",
|
||||
"pages",
|
||||
"party_mode",
|
||||
"people",
|
||||
"people_outline",
|
||||
"people_alt",
|
||||
"person",
|
||||
"person_add",
|
||||
"person_add_alt_1",
|
||||
"person_outline",
|
||||
"person_remove",
|
||||
"plus_one",
|
||||
"poll",
|
||||
"psychology",
|
||||
"public",
|
||||
"public_off",
|
||||
"school",
|
||||
"science",
|
||||
"self_improvement",
|
||||
"sentiment_dissatisfied",
|
||||
"sentiment_satisfied",
|
||||
"sentiment_very_dissatisfied",
|
||||
"sentiment_very_satisfied",
|
||||
"share",
|
||||
"single_bed",
|
||||
"sports",
|
||||
"sports_baseball",
|
||||
"sports_basketball",
|
||||
"sports_cricket",
|
||||
"sports_esports",
|
||||
"sports_football",
|
||||
"sports_golf",
|
||||
"sports_handball",
|
||||
"sports_hockey",
|
||||
"sports_kabaddi",
|
||||
"sports_mma",
|
||||
"sports_motorsports",
|
||||
"sports_rugby",
|
||||
"sports_soccer",
|
||||
"sports_tennis",
|
||||
"sports_volleyball",
|
||||
"thumb_down_alt",
|
||||
"thumb_up_alt",
|
||||
"whatshot"
|
||||
@@ -1076,7 +1336,9 @@
|
||||
"radio_button_unchecked",
|
||||
"star",
|
||||
"star_border",
|
||||
"star_half"
|
||||
"star_half",
|
||||
"toggle_off",
|
||||
"toggle_on"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'insert_photo',
|
||||
component: InterfaceImage,
|
||||
types: ['uuid'],
|
||||
localTypes: ['file'],
|
||||
relationship: 'm2o',
|
||||
options: [],
|
||||
recommendedDisplays: ['image'],
|
||||
|
||||
@@ -10,6 +10,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
component: InterfaceManyToMany,
|
||||
relationship: 'm2m',
|
||||
types: ['alias'],
|
||||
localTypes: ['m2m'],
|
||||
options: Options,
|
||||
recommendedDisplays: ['related-values'],
|
||||
}));
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
<div class="one-to-many" v-else>
|
||||
<v-table
|
||||
:loading="loading"
|
||||
:items="items"
|
||||
:items="sortedItems || items"
|
||||
:headers.sync="tableHeaders"
|
||||
show-resize
|
||||
inline
|
||||
:sort.sync="sort"
|
||||
@update:items="sortItems($event)"
|
||||
@click:row="editItem"
|
||||
:disabled="disabled"
|
||||
:show-manual-sort="sortField !== null"
|
||||
:manual-sort-key="sortField"
|
||||
>
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<render-display
|
||||
@@ -73,6 +77,7 @@ import useRelation from './use-relation';
|
||||
import usePreview from './use-preview';
|
||||
import useEdit from './use-edit';
|
||||
import useSelection from './use-selection';
|
||||
import useSort from './use-sort';
|
||||
|
||||
export default defineComponent({
|
||||
components: { DrawerItem, DrawerCollection },
|
||||
@@ -93,6 +98,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortField: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fields: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
@@ -103,7 +112,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { value, collection, field, fields } = toRefs(props);
|
||||
const { value, collection, field, fields, sortField } = toRefs(props);
|
||||
|
||||
function emitter(newVal: any[] | null) {
|
||||
emit('input', newVal);
|
||||
@@ -127,6 +136,7 @@ export default defineComponent({
|
||||
const { tableHeaders, items, loading, error } = usePreview(
|
||||
value,
|
||||
fields,
|
||||
sortField,
|
||||
relationInfo,
|
||||
getNewSelectedItems,
|
||||
getUpdatedItems,
|
||||
@@ -151,6 +161,8 @@ export default defineComponent({
|
||||
emitter
|
||||
);
|
||||
|
||||
const { sort, sortItems, sortedItems } = useSort(sortField, fields, items, emitter);
|
||||
|
||||
return {
|
||||
junction,
|
||||
relation,
|
||||
@@ -172,6 +184,9 @@ export default defineComponent({
|
||||
relatedPrimaryKey,
|
||||
get,
|
||||
editModalActive,
|
||||
sort,
|
||||
sortItems,
|
||||
sortedItems,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,15 @@
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="field half">
|
||||
<p class="type-label">{{ $t('sort_field') }}</p>
|
||||
<interface-field
|
||||
v-model="sortField"
|
||||
:collection="junctionCollection"
|
||||
:type-allow-list="['bigInteger', 'integer']"
|
||||
allowNone
|
||||
></interface-field>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,6 +61,19 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const sortField = computed({
|
||||
get() {
|
||||
return props.value?.sortField;
|
||||
},
|
||||
set(newFields: string) {
|
||||
emit('input', {
|
||||
...(props.value || {}),
|
||||
sortField: newFields,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const fields = computed({
|
||||
get() {
|
||||
return props.value?.fields;
|
||||
@@ -63,6 +85,7 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const junctionCollection = computed(() => {
|
||||
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
|
||||
const { field } = props.fieldData;
|
||||
@@ -71,12 +94,14 @@ export default defineComponent({
|
||||
);
|
||||
return junctionRelation?.many_collection || null;
|
||||
});
|
||||
|
||||
const junctionCollectionExists = computed(() => {
|
||||
return !!collectionsStore.state.collections.find(
|
||||
(collection) => collection.collection === junctionCollection.value
|
||||
);
|
||||
});
|
||||
return { fields, junctionCollection, junctionCollectionExists };
|
||||
|
||||
return { fields, sortField, junctionCollection, junctionCollectionExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cloneDeep, get } from 'lodash';
|
||||
export default function usePreview(
|
||||
value: Ref<(string | number | Record<string, any>)[] | null>,
|
||||
fields: Ref<string[]>,
|
||||
sortField: Ref<string | null>,
|
||||
relation: Ref<RelationInfo>,
|
||||
getNewSelectedItems: () => Record<string, any>[],
|
||||
getUpdatedItems: () => Record<string, any>[],
|
||||
@@ -29,7 +30,7 @@ export default function usePreview(
|
||||
|
||||
return fields.reduce((acc: string[], field) => {
|
||||
const sections = field.split('.');
|
||||
if (junctionField === sections[0] && sections.length === 2) acc.push(sections[1]);
|
||||
if (junctionField === sections[0] && sections.length >= 2) acc.push(sections.slice(1).join('.'));
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
@@ -124,6 +125,9 @@ export default function usePreview(
|
||||
if (filteredFields.includes(junctionPkField) === false) filteredFields.push(junctionPkField);
|
||||
if (filteredFields.includes(junctionField) === false) filteredFields.push(junctionField);
|
||||
|
||||
if (sortField.value !== null && filteredFields.includes(sortField.value) === false)
|
||||
filteredFields.push(sortField.value);
|
||||
|
||||
data = await request(junctionCollection, filteredFields, junctionPkField, primaryKeys);
|
||||
}
|
||||
|
||||
|
||||
35
app/src/interfaces/many-to-many/use-sort.ts
Normal file
35
app/src/interfaces/many-to-many/use-sort.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Ref, ref, computed } from '@vue/composition-api';
|
||||
import { Sort } from '@/components/v-table/types';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
export default function useSort(
|
||||
sortField: Ref<string | null>,
|
||||
fields: Ref<string[]>,
|
||||
items: Ref<Record<string, any>[]>,
|
||||
emit: (newVal: any[] | null) => void
|
||||
) {
|
||||
const sort = ref<Sort>({ by: sortField.value || fields.value[0], desc: false });
|
||||
|
||||
function sortItems(newItems: Record<string, any>[]) {
|
||||
const sField = sortField.value;
|
||||
if (sField === null) return;
|
||||
|
||||
const itemsSorted = newItems.map((item, i) => {
|
||||
item[sField] = i;
|
||||
return item;
|
||||
});
|
||||
|
||||
emit(itemsSorted);
|
||||
}
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
const sField = sortField.value;
|
||||
if (sField === null || sort.value.by !== sField) return null;
|
||||
|
||||
const desc = sort.value.desc;
|
||||
const sorted = sortBy(items.value, [sField]);
|
||||
return desc ? sorted.reverse() : sorted;
|
||||
});
|
||||
|
||||
return { sort, sortItems, sortedItems };
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
component: InterfaceManyToOne,
|
||||
types: ['uuid', 'string', 'text', 'integer', 'bigInteger'],
|
||||
relationship: 'm2o',
|
||||
localTypes: ['m2o'],
|
||||
options: Options,
|
||||
recommendedDisplays: ['related-values'],
|
||||
}));
|
||||
|
||||
@@ -9,6 +9,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
icon: 'arrow_right_alt',
|
||||
component: InterfaceOneToMany,
|
||||
types: ['alias'],
|
||||
localTypes: ['o2m'],
|
||||
relationship: 'o2m',
|
||||
options: Options,
|
||||
recommendedDisplays: ['related-values'],
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
<div class="one-to-many" v-else>
|
||||
<v-table
|
||||
:loading="loading"
|
||||
:items="displayItems"
|
||||
:items="sortedItems || items"
|
||||
:headers.sync="tableHeaders"
|
||||
show-resize
|
||||
inline
|
||||
:sort.sync="sort"
|
||||
@update:items="sortItems($event)"
|
||||
@click:row="editItem"
|
||||
:disabled="disabled"
|
||||
:show-manual-sort="sortField !== null"
|
||||
:manual-sort-key="sortField"
|
||||
>
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<render-display
|
||||
@@ -68,8 +72,8 @@ import { useCollectionsStore, useRelationsStore, useFieldsStore } from '@/stores
|
||||
import DrawerItem from '@/views/private/components/drawer-item';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection';
|
||||
import { Filter, Field } from '@/types';
|
||||
import { Header } from '@/components/v-table/types';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Header, Sort } from '@/components/v-table/types';
|
||||
import { isEqual, sortBy } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
components: { DrawerItem, DrawerCollection },
|
||||
@@ -94,6 +98,10 @@ export default defineComponent({
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
sortField: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -105,9 +113,10 @@ export default defineComponent({
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const { relation, relatedCollection, relatedPrimaryKeyField } = useRelation();
|
||||
const { tableHeaders, displayItems, loading, error } = useTable();
|
||||
const { tableHeaders, items, loading, error } = useTable();
|
||||
const { currentlyEditing, editItem, editsAtStart, stageEdits, cancelEdit } = useEdits();
|
||||
const { stageSelection, selectModalActive, selectionFilters } = useSelection();
|
||||
const { sort, sortItems, sortedItems } = useSort();
|
||||
|
||||
return {
|
||||
relation,
|
||||
@@ -122,8 +131,11 @@ export default defineComponent({
|
||||
stageSelection,
|
||||
selectModalActive,
|
||||
deleteItem,
|
||||
displayItems,
|
||||
items,
|
||||
sortItems,
|
||||
selectionFilters,
|
||||
sort,
|
||||
sortedItems,
|
||||
};
|
||||
|
||||
function getItem(id: string | number) {
|
||||
@@ -200,6 +212,31 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
function useSort() {
|
||||
const sort = ref<Sort>({ by: props.sortField || props.fields[0], desc: false });
|
||||
|
||||
function sortItems(newItems: Record<string, any>[]) {
|
||||
if (props.sortField === null) return;
|
||||
|
||||
const itemsSorted = newItems.map((item, i) => {
|
||||
item[props.sortField] = i;
|
||||
return item;
|
||||
});
|
||||
|
||||
emit('input', itemsSorted);
|
||||
}
|
||||
|
||||
const sortedItems = computed(() => {
|
||||
if (props.sortField === null || sort.value.by !== props.sortField) return null;
|
||||
|
||||
const desc = sort.value.desc;
|
||||
const sorted = sortBy(items.value, [props.sortField]);
|
||||
return desc ? sorted.reverse() : sorted;
|
||||
});
|
||||
|
||||
return { sort, sortItems, sortedItems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds info about the current relationship, like related collection, primary key field
|
||||
* of the other collection etc
|
||||
@@ -223,7 +260,7 @@ export default defineComponent({
|
||||
// values if it needs to. This allows the user to manually resize the columns for example
|
||||
const tableHeaders = ref<Header[]>([]);
|
||||
const loading = ref(false);
|
||||
const displayItems = ref<Record<string, any>[]>([]);
|
||||
const items = ref<Record<string, any>[]>([]);
|
||||
const error = ref(null);
|
||||
|
||||
watch(
|
||||
@@ -238,6 +275,9 @@ export default defineComponent({
|
||||
fields.push(pkField);
|
||||
}
|
||||
|
||||
if (props.sortField !== null && fields.includes(props.sortField) === false)
|
||||
fields.push(props.sortField);
|
||||
|
||||
try {
|
||||
const endpoint = relatedCollection.value.collection.startsWith('directus_')
|
||||
? `/${relatedCollection.value.collection.substring(9)}`
|
||||
@@ -261,7 +301,7 @@ export default defineComponent({
|
||||
const updatedItems = getUpdatedItems();
|
||||
const newItems = getNewItems();
|
||||
|
||||
displayItems.value = existingItems
|
||||
items.value = existingItems
|
||||
.map((item) => {
|
||||
const updatedItem = updatedItems.find((updated) => updated[pkField] === item[pkField]);
|
||||
if (updatedItem !== undefined) return updatedItem;
|
||||
@@ -311,7 +351,7 @@ export default defineComponent({
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { tableHeaders, displayItems, loading, error };
|
||||
return { tableHeaders, items, loading, error };
|
||||
}
|
||||
|
||||
function useEdits() {
|
||||
@@ -376,11 +416,11 @@ export default defineComponent({
|
||||
const selectModalActive = ref(false);
|
||||
|
||||
const selectedPrimaryKeys = computed<(number | string)[]>(() => {
|
||||
if (displayItems.value === null) return [];
|
||||
if (items.value === null) return [];
|
||||
|
||||
const pkField = relatedPrimaryKeyField.value.field;
|
||||
|
||||
return displayItems.value
|
||||
return items.value
|
||||
.filter((currentItem) => pkField in currentItem)
|
||||
.map((currentItem) => currentItem[pkField]);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
:inject="relatedCollectionExists ? null : { fields: newFields, collections: newCollections, relations }"
|
||||
/>
|
||||
</div>
|
||||
<div class="field half">
|
||||
<p class="type-label">{{ $t('sort_field') }}</p>
|
||||
<interface-field
|
||||
v-model="sortField"
|
||||
:collection="relatedCollection"
|
||||
:type-allow-list="['bigInteger', 'integer']"
|
||||
allowNone
|
||||
></interface-field>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +71,18 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
const sortField = computed({
|
||||
get() {
|
||||
return props.value?.sortField;
|
||||
},
|
||||
set(newFields: string) {
|
||||
emit('input', {
|
||||
...(props.value || {}),
|
||||
sortField: newFields,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const relatedCollection = computed(() => {
|
||||
if (!props.fieldData || !props.relations || props.relations.length === 0) return null;
|
||||
const { field } = props.fieldData;
|
||||
@@ -77,7 +98,7 @@ export default defineComponent({
|
||||
);
|
||||
});
|
||||
|
||||
return { fields, relatedCollection, relatedCollectionExists };
|
||||
return { fields, sortField, relatedCollection, relatedCollectionExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
||||
set(newVal: FieldMeta[] | null) {
|
||||
const fields = (newVal || []).map((meta: Record<string, any>) => ({
|
||||
field: meta.field,
|
||||
name: meta.field,
|
||||
name: meta.name || meta.field,
|
||||
type: meta.type,
|
||||
meta,
|
||||
}));
|
||||
@@ -53,6 +53,21 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const repeaterFields: DeepPartial<Field>[] = [
|
||||
{
|
||||
name: i18n.tc('name'),
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'full',
|
||||
sort: 1,
|
||||
options: {
|
||||
font: 'monospace',
|
||||
placeholder: i18n.t('interfaces.repeater.field_name_placeholder'),
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.tc('field', 1),
|
||||
field: 'field',
|
||||
@@ -60,7 +75,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
sort: 1,
|
||||
sort: 2,
|
||||
options: {
|
||||
font: 'monospace',
|
||||
placeholder: i18n.t('interfaces.repeater.field_name_placeholder'),
|
||||
@@ -75,7 +90,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
sort: 2,
|
||||
sort: 3,
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
@@ -98,13 +113,13 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
sort: 3,
|
||||
sort: 4,
|
||||
options: {
|
||||
choices: fieldTypes
|
||||
}
|
||||
choices: fieldTypes,
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'string'
|
||||
default_value: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -114,10 +129,10 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'interface',
|
||||
width: 'half',
|
||||
sort: 4,
|
||||
sort: 5,
|
||||
options: {
|
||||
typeField: 'type'
|
||||
}
|
||||
typeField: 'type',
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
@@ -128,7 +143,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'full',
|
||||
sort: 5,
|
||||
sort: 6,
|
||||
options: {
|
||||
placeholder: i18n.t('interfaces.repeater.field_note_placeholder'),
|
||||
},
|
||||
@@ -142,7 +157,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'interface-options',
|
||||
width: 'full',
|
||||
sort: 6,
|
||||
sort: 7,
|
||||
options: {
|
||||
interfaceField: 'interface',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-item class="row" v-slot:default="{ active, toggle }" :active="true" :watch="false">
|
||||
<v-item class="row" v-slot:default="{ active, toggle }" :active="initialActive" :watch="false">
|
||||
<repeater-row-header
|
||||
:template="template"
|
||||
:value="value"
|
||||
@@ -39,6 +39,10 @@ export default defineComponent({
|
||||
type: Array as PropType<Partial<Field>[]>,
|
||||
default: () => [],
|
||||
},
|
||||
initialActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@delete="removeItem(row)"
|
||||
:disabled="disabled"
|
||||
:headerPlaceholder="headerPlaceholder"
|
||||
:initialActive="addedIndex === index"
|
||||
/>
|
||||
</draggable>
|
||||
<button @click="addNew" class="add-new" v-if="showAddNew">
|
||||
@@ -32,7 +33,7 @@ export default defineComponent({
|
||||
components: { RepeaterRow, Draggable },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
default: null,
|
||||
},
|
||||
fields: {
|
||||
@@ -61,7 +62,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const selection = ref<number[]>([]);
|
||||
const addedIndex = ref<number | null>(null);
|
||||
|
||||
const _template = computed(() => {
|
||||
if (props.template === null) return props.fields.length > 0 ? `{{${props.fields[0].field}}}` : '';
|
||||
@@ -76,11 +77,11 @@ export default defineComponent({
|
||||
return false;
|
||||
});
|
||||
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, selection, _template };
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, addedIndex, _template };
|
||||
|
||||
function updateValues(index: number, updatedValues: any) {
|
||||
emitValue(
|
||||
props.value.map((item, i) => {
|
||||
props.value.map((item: any, i: number) => {
|
||||
if (i === index) {
|
||||
return updatedValues;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function removeItem(row: any) {
|
||||
selection.value = [];
|
||||
addedIndex.value = null;
|
||||
if (props.value) {
|
||||
emitValue(props.value.filter((existingItem) => existingItem !== row));
|
||||
} else {
|
||||
@@ -111,6 +112,8 @@ export default defineComponent({
|
||||
newDefaults[field.field!] = field.schema?.default_value;
|
||||
});
|
||||
|
||||
addedIndex.value = props.value === null ? 0 : props.value.length;
|
||||
|
||||
if (props.value !== null) {
|
||||
emitValue([...props.value, newDefaults]);
|
||||
} else {
|
||||
|
||||
@@ -131,7 +131,7 @@ export default defineComponent({
|
||||
|
||||
if (props.capitalization === 'auto-format') val = formatTitle(val, new RegExp(whitespace));
|
||||
|
||||
val = val.replaceAll(/ +/g, whitespace);
|
||||
val = val.replace(/ +/g, whitespace);
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { Component } from 'vue';
|
||||
import { Field, types } from '@/types';
|
||||
import { Field, types, localTypes } from '@/types';
|
||||
|
||||
export type InterfaceConfig = {
|
||||
id: string;
|
||||
@@ -10,6 +10,7 @@ export type InterfaceConfig = {
|
||||
component: Component;
|
||||
options: DeepPartial<Field>[] | Component;
|
||||
types: typeof types[number][];
|
||||
localTypes?: readonly typeof localTypes[number][];
|
||||
relationship?: null | 'm2o' | 'o2m' | 'm2m' | 'translations';
|
||||
hideLabel?: boolean;
|
||||
hideLoader?: boolean;
|
||||
|
||||
@@ -9,7 +9,17 @@
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.tox .tox-tbtn svg {
|
||||
.tox .tox-listbox__select-chevron svg,
|
||||
.tox .tox-collection__item-caret svg {
|
||||
fill: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.tox .tox-swatches__picker-btn svg {
|
||||
fill: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.tox .tox-tbtn svg,
|
||||
.tox .tox-tbtn:hover svg {
|
||||
fill: var(--foreground-normal);
|
||||
}
|
||||
|
||||
@@ -97,6 +107,10 @@
|
||||
left 0 top 0 var(--background-subdued);
|
||||
}
|
||||
|
||||
.tox .tox-pop__dialog .tox-toolbar {
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
body.dark .tox .tox-toolbar,
|
||||
body.dark .tox .tox-toolbar__primary,
|
||||
body.dark .tox .tox-toolbar__overflow {
|
||||
@@ -113,13 +127,34 @@ body.dark .tox .tox-toolbar__overflow {
|
||||
}
|
||||
}
|
||||
|
||||
.tox .tox-swatches__picker-btn,
|
||||
.tox .tox-swatches__picker-btn:hover,
|
||||
.tox .tox-swatches__picker-btn:active,
|
||||
.tox .tox-split-button:hover {
|
||||
-webkit-box-shadow: unset;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.tox .tox-tbtn--enabled,
|
||||
.tox .tox-tbtn--enabled:hover,
|
||||
.tox .tox-tbtn:hover {
|
||||
.tox .tox-split-button:hover,
|
||||
.tox .tox-tbtn:hover,
|
||||
.tox .tox-split-button:focus {
|
||||
color: var(--foreground-normal);
|
||||
background: var(--border-normal);
|
||||
}
|
||||
|
||||
.tox .tox-swatches__picker-btn:hover {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tox .tox-swatch:hover,
|
||||
.tox .tox-swatch:focus {
|
||||
-webkit-transform: scale(1.2);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.mce-content-body {
|
||||
margin: 20px;
|
||||
}
|
||||
@@ -206,20 +241,16 @@ body.dark .tox .tox-toolbar__overflow {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.tox .tox-collection--list .tox-collection__item--enabled,
|
||||
.tox .tox-collection--list .tox-collection__item--active {
|
||||
color: var(--foreground-normal) !important;
|
||||
background-color: var(--background-page) !important;
|
||||
}
|
||||
|
||||
.tox .tox-collection--list .tox-collection__item--enabled {
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-page);
|
||||
background-color: var(--background-normal-alt) !important;
|
||||
}
|
||||
|
||||
.tox .tox-textfield:focus,
|
||||
.tox .tox-selectfield select:focus,
|
||||
.tox .tox-textarea:focus {
|
||||
border-color: var(--foreground-subdued);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.tox .tox-button {
|
||||
@@ -314,8 +345,39 @@ body.dark .tox .tox-toolbar__overflow {
|
||||
background-color: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.tox .tox-pop__dialog,
|
||||
.tox:not([dir='rtl']) .tox-toolbar__group:not(:last-of-type),
|
||||
.tox .tox-collection--list .tox-collection__group {
|
||||
border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
|
||||
.tox .tox-insert-table-picker__label {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.tox .tox-insert-table-picker > div {
|
||||
border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
.tox .tox-insert-table-picker .tox-insert-table-picker__selected {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.tox .tox-pop.tox-pop--top::after {
|
||||
border-bottom-color: var(--background-subdued);
|
||||
}
|
||||
|
||||
.tox .tox-pop.tox-pop--top::before {
|
||||
border-bottom-color: var(--border-normal);
|
||||
}
|
||||
|
||||
.tox .tox-dialog-wrap__backdrop .tox-rgba-preview {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.tox .tox-dialog__body-nav-item {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"datetime": {
|
||||
"datetime": "Datetime",
|
||||
"description": "Display values related to time",
|
||||
"format": "Format",
|
||||
"format_note": "The custom format accetps the __[Date Field Symbol Table](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)__",
|
||||
"long": "Long",
|
||||
"short": "Short",
|
||||
"relative": "Relative",
|
||||
"relative_label": "Show relative time, eg: 5 minutes ago"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
"create_user": "Create User",
|
||||
"create_webhook": "Create Webhook",
|
||||
|
||||
"invite_users": "Invite Users",
|
||||
"email_examples": "admin@example.com, user@example.com...",
|
||||
"invite": "Invite",
|
||||
"emails": "Emails",
|
||||
|
||||
"connection_excellent": "Excellent Connection",
|
||||
"connection_good": "Good Connection",
|
||||
"connection_fair": "Fair Connection",
|
||||
@@ -388,8 +393,9 @@
|
||||
|
||||
"date-fns_datetime": "PPP h:mma",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "h:mma",
|
||||
"date-fns_time": "h:mm:ss a",
|
||||
"date-fns_date_short": "MMM d, u",
|
||||
"date-fns_time_short": "h:mma",
|
||||
"date-fns_date_short_no_year": "MMM d",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
@@ -786,6 +792,7 @@
|
||||
"create_folder": "Create Folder",
|
||||
"folder_name": "Folder Name...",
|
||||
"add_file": "Add File",
|
||||
"replace_file": "Replace File",
|
||||
|
||||
"no_results": "No Results",
|
||||
"no_results_copy": "Adjust or clear search filters to see results.",
|
||||
|
||||
@@ -337,6 +337,7 @@ export default defineComponent({
|
||||
const fields = [primaryKeyField.value.field];
|
||||
|
||||
if (imageSource.value) {
|
||||
fields.push(`${imageSource.value}.modified_on`);
|
||||
fields.push(`${imageSource.value}.type`);
|
||||
fields.push(`${imageSource.value}.filename_disk`);
|
||||
fields.push(`${imageSource.value}.storage`);
|
||||
@@ -344,6 +345,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (props.collection === 'directus_files' && imageSource.value === '$file') {
|
||||
fields.push('modified_on');
|
||||
fields.push('type');
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type File = {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
type: string;
|
||||
modified_on: Date;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
@@ -101,7 +102,7 @@ export default defineComponent({
|
||||
key = 'system-medium-contain';
|
||||
}
|
||||
|
||||
return getRootPath() + `assets/${props.file.id}?key=${key}`;
|
||||
return getRootPath() + `assets/${props.file.id}?key=${key}&modified=${props.file.modified_on}`;
|
||||
});
|
||||
|
||||
const svgSource = computed(() => {
|
||||
@@ -109,7 +110,7 @@ export default defineComponent({
|
||||
if (props.file.type.startsWith('image') === false) return null;
|
||||
if (props.file.type.includes('svg') === false) return null;
|
||||
|
||||
return getRootPath() + `assets/${props.file.id}`;
|
||||
return getRootPath() + `assets/${props.file.id}&modified=${props.file.modified_on}`;
|
||||
});
|
||||
|
||||
const selectionIcon = computed(() => {
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
rounded
|
||||
icon
|
||||
:loading="saving"
|
||||
:disabled="saveAllowed === false || hasEdits === false"
|
||||
:disabled="isSavable === false"
|
||||
v-tooltip.bottom="saveAllowed ? $t('save') : $t('not_allowed')"
|
||||
@click="saveAndQuit"
|
||||
>
|
||||
@@ -143,7 +143,7 @@
|
||||
<template #append-outer>
|
||||
<save-options
|
||||
v-if="collectionInfo.meta && collectionInfo.meta.singleton !== true"
|
||||
:disabled="hasEdits === false"
|
||||
:disabled="isSavable === false"
|
||||
@save-and-stay="saveAndStay"
|
||||
@save-and-add-new="saveAndAddNew"
|
||||
@save-as-copy="saveAsCopyAndNavigate"
|
||||
@@ -267,7 +267,7 @@ export default defineComponent({
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue | null>(null);
|
||||
|
||||
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
|
||||
const { info: collectionInfo, defaults, primaryKeyField } = useCollection(collection);
|
||||
|
||||
const {
|
||||
isNew,
|
||||
@@ -288,7 +288,24 @@ export default defineComponent({
|
||||
validationErrors,
|
||||
} = useItem(collection, primaryKey);
|
||||
|
||||
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
|
||||
const hasEdits = computed(() => Object.keys(edits.value).length > 0);
|
||||
|
||||
const isSavable = computed(() => {
|
||||
if (saveAllowed.value === false) return false;
|
||||
|
||||
if (
|
||||
!primaryKeyField.value?.schema?.has_auto_increment &&
|
||||
!primaryKeyField.value?.meta?.special?.includes('uuid')
|
||||
) {
|
||||
return !!edits.value?.[primaryKeyField.value.field];
|
||||
}
|
||||
|
||||
if (isNew.value === true) {
|
||||
return Object.keys(defaults.value).length > 0 || hasEdits.value;
|
||||
}
|
||||
|
||||
return hasEdits.value;
|
||||
});
|
||||
|
||||
const confirmDelete = ref(false);
|
||||
const confirmArchive = ref(false);
|
||||
@@ -324,9 +341,7 @@ export default defineComponent({
|
||||
useShortcut('meta+shift+s', saveAndAddNew, form);
|
||||
|
||||
const navigationGuard: NavigationGuard = (to, from, next) => {
|
||||
const hasEdits = Object.keys(edits.value).length > 0;
|
||||
|
||||
if (hasEdits) {
|
||||
if (hasEdits.value) {
|
||||
confirmLeave.value = true;
|
||||
leaveTo.value = to.fullPath;
|
||||
return next(false);
|
||||
@@ -343,7 +358,7 @@ export default defineComponent({
|
||||
error,
|
||||
isNew,
|
||||
edits,
|
||||
hasEdits,
|
||||
isSavable,
|
||||
saving,
|
||||
collectionInfo,
|
||||
saveAndQuit,
|
||||
@@ -389,14 +404,14 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function saveAndQuit() {
|
||||
if (saveAllowed.value === false || hasEdits.value === false) return;
|
||||
if (isSavable.value === false) return;
|
||||
|
||||
await save();
|
||||
if (props.singleton === false) router.push(`/collections/${props.collection}`);
|
||||
}
|
||||
|
||||
async function saveAndStay() {
|
||||
if (saveAllowed.value === false || hasEdits.value === false) return;
|
||||
if (isSavable.value === false) return;
|
||||
|
||||
const savedItem: Record<string, any> = await save();
|
||||
|
||||
@@ -410,7 +425,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function saveAndAddNew() {
|
||||
if (saveAllowed.value === false || hasEdits.value === false) return;
|
||||
if (isSavable.value === false) return;
|
||||
|
||||
await save();
|
||||
|
||||
@@ -470,6 +485,12 @@ export default defineComponent({
|
||||
return { deleteAllowed, saveAllowed, archiveAllowed, updateAllowed };
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
return (this as any).navigationGuard(to, from, next);
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
return (this as any).navigationGuard(to, from, next);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -54,12 +54,9 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
htmlString = htmlString.replaceAll(
|
||||
hintRegex,
|
||||
(match: string, type: string, title: string, body: string) => {
|
||||
return `<div class="hint ${type}"><p class="hint-title">${title}</p><p class="hint-body">${body}</p></div>`;
|
||||
}
|
||||
);
|
||||
htmlString = htmlString.replace(hintRegex, (match: string, type: string, title: string, body: string) => {
|
||||
return `<div class="hint ${type}"><p class="hint-title">${title}</p><p class="hint-body">${body}</p></div>`;
|
||||
});
|
||||
|
||||
html.value = htmlString;
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ export default defineModule(({ i18n }) => {
|
||||
for (const doc of directory.children) {
|
||||
if (doc.type === 'file') {
|
||||
routes.push({
|
||||
path: '/' + doc.path.replace('.md', '').replaceAll('\\', '/'),
|
||||
path: '/' + doc.path.replace('.md', '').replace(/\\/g, '/'),
|
||||
component: StaticDocs,
|
||||
});
|
||||
} else if (doc.type === 'directory') {
|
||||
if (doc.path && doc.children && doc.children.length > 0)
|
||||
routes.push({
|
||||
path: '/' + doc.path.replaceAll('\\', '/'),
|
||||
redirect: '/' + doc.children![0].path.replace('.md', '').replaceAll('\\', '/'),
|
||||
path: '/' + doc.path.replace(/\\/g, '/'),
|
||||
redirect: '/' + doc.children![0].path.replace('.md', '').replace(/\\/g, '/'),
|
||||
});
|
||||
|
||||
routes.push(...parseRoutes(doc));
|
||||
|
||||
@@ -64,10 +64,17 @@
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>{{ $t('file') }}</dt>
|
||||
<dd>
|
||||
<a :href="`${getRootPath()}assets/${file.id}`" target="_blank">{{ $t('open') }}</a>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt>{{ $t('folder') }}</dt>
|
||||
<dd>
|
||||
<button @click="$emit('move-folder')">{{ folder ? folder.name : $t('file_library') }}</button>
|
||||
<router-link :to="folderLink">{{ folder ? folder.name : $t('file_library') }}</router-link>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -115,6 +122,7 @@ import i18n from '@/lang';
|
||||
import marked from 'marked';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
import api from '@/api';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -140,9 +148,20 @@ export default defineComponent({
|
||||
|
||||
const { creationDate, modificationDate } = useDates();
|
||||
const { userCreated, userModified } = useUser();
|
||||
const { folder } = useFolder();
|
||||
const { folder, folderLink } = useFolder();
|
||||
|
||||
return { readableMimeType, size, creationDate, modificationDate, userCreated, userModified, folder, marked };
|
||||
return {
|
||||
readableMimeType,
|
||||
size,
|
||||
creationDate,
|
||||
modificationDate,
|
||||
userCreated,
|
||||
userModified,
|
||||
folder,
|
||||
marked,
|
||||
folderLink,
|
||||
getRootPath,
|
||||
};
|
||||
|
||||
function useDates() {
|
||||
const creationDate = ref<string | null>(null);
|
||||
@@ -230,16 +249,23 @@ export default defineComponent({
|
||||
|
||||
function useFolder() {
|
||||
type Folder = {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const loading = ref(false);
|
||||
const folder = ref<Folder | null>(null);
|
||||
|
||||
const folderLink = computed(() => {
|
||||
if (folder.value === null) {
|
||||
return `/files`;
|
||||
}
|
||||
return `/files/?folder=${folder.value.id}`;
|
||||
});
|
||||
|
||||
watch(() => props.file, fetchFolder, { immediate: true });
|
||||
|
||||
return { folder };
|
||||
return { folder, folderLink };
|
||||
|
||||
async function fetchFolder() {
|
||||
if (!props.file) return null;
|
||||
|
||||
57
app/src/modules/files/components/replace-file.vue
Normal file
57
app/src/modules/files/components/replace-file.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<v-dialog :active="active" @toggle="$emit('toggle', false)" @esc="$emit('toggle', false)">
|
||||
<v-card v-if="file">
|
||||
<v-card-title>{{ $t('replace_file') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-upload :preset="preset" :file-id="file.id" @input="uploaded" from-url />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="$emit('toggle', false)">{{ $t('done') }}</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import router from '@/router';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
file: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
preset: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
return { uploaded };
|
||||
function uploaded() {
|
||||
emit('toggle', false);
|
||||
emit('replaced');
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.add-new {
|
||||
--v-button-background-color: var(--primary-25);
|
||||
--v-button-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--primary-50);
|
||||
--v-button-color-hover: var(--primary);
|
||||
}
|
||||
</style>
|
||||
@@ -119,15 +119,13 @@
|
||||
:width="item.width"
|
||||
:height="item.height"
|
||||
:title="item.title"
|
||||
@click="previewActive = true"
|
||||
@click="replaceFileDialogActive = true"
|
||||
/>
|
||||
|
||||
<file-lightbox v-if="item" :id="item.id" v-model="previewActive" />
|
||||
|
||||
<image-editor
|
||||
v-if="item && item.type.startsWith('image')"
|
||||
:id="item.id"
|
||||
@refresh="changeCacheBuster"
|
||||
@refresh="refresh"
|
||||
v-model="editActive"
|
||||
/>
|
||||
|
||||
@@ -156,7 +154,7 @@
|
||||
</v-dialog>
|
||||
|
||||
<template #sidebar>
|
||||
<file-info-sidebar-detail :file="item" @move-folder="moveToDialogActive = true" />
|
||||
<file-info-sidebar-detail :file="item" />
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_files"
|
||||
@@ -169,6 +167,8 @@
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<replace-file v-model="replaceFileDialogActive" @replaced="refresh" :file="item" />
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +184,6 @@ import SaveOptions from '@/views/private/components/save-options';
|
||||
import FilePreview from '@/views/private/components/file-preview';
|
||||
import ImageEditor from '@/views/private/components/image-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import FileLightbox from '@/views/private/components/file-lightbox';
|
||||
import { useFieldsStore } from '@/stores/';
|
||||
import { Field } from '@/types';
|
||||
import FileInfoSidebarDetail from '../components/file-info-sidebar-detail.vue';
|
||||
@@ -194,6 +193,7 @@ import api from '@/api';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
import FilesNotFound from './not-found.vue';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
import ReplaceFile from '../components/replace-file.vue';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -220,10 +220,10 @@ export default defineComponent({
|
||||
SaveOptions,
|
||||
FilePreview,
|
||||
ImageEditor,
|
||||
FileLightbox,
|
||||
FileInfoSidebarDetail,
|
||||
FolderPicker,
|
||||
FilesNotFound,
|
||||
ReplaceFile,
|
||||
},
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -236,6 +236,7 @@ export default defineComponent({
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
const fieldsStore = useFieldsStore();
|
||||
const replaceFileDialogActive = ref(false);
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue | null>(null);
|
||||
|
||||
@@ -256,13 +257,15 @@ export default defineComponent({
|
||||
|
||||
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
|
||||
const confirmDelete = ref(false);
|
||||
const cacheBuster = ref(nanoid());
|
||||
const editActive = ref(false);
|
||||
const previewActive = ref(false);
|
||||
const fileSrc = computed(() => {
|
||||
return (
|
||||
getRootPath() + `assets/${props.primaryKey}?cache-buster=${cacheBuster.value}&key=system-large-contain`
|
||||
);
|
||||
if (item.value && item.value.modified_on) {
|
||||
return (
|
||||
getRootPath() +
|
||||
`assets/${props.primaryKey}?cache-buster=${item.value.modified_on}&key=system-large-contain`
|
||||
);
|
||||
}
|
||||
return getRootPath() + `assets/${props.primaryKey}?key=system-large-contain`;
|
||||
});
|
||||
|
||||
// These are the fields that will be prevented from showing up in the form because they'll be shown in the sidebar
|
||||
@@ -318,10 +321,7 @@ export default defineComponent({
|
||||
saveAndStay,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
changeCacheBuster,
|
||||
cacheBuster,
|
||||
editActive,
|
||||
previewActive,
|
||||
revisionsDrawerDetail,
|
||||
formFields,
|
||||
confirmLeave,
|
||||
@@ -335,12 +335,10 @@ export default defineComponent({
|
||||
fileSrc,
|
||||
form,
|
||||
to,
|
||||
replaceFileDialogActive,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
cacheBuster.value = nanoid();
|
||||
}
|
||||
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => {
|
||||
if (!item?.value?.folder) {
|
||||
@@ -405,7 +403,7 @@ export default defineComponent({
|
||||
const selectedFolder = ref<number | null>();
|
||||
|
||||
watch(item, () => {
|
||||
selectedFolder.value = item.value.folder;
|
||||
selectedFolder.value = item.value?.folder || null;
|
||||
});
|
||||
|
||||
return { moveToDialogActive, moving, moveToFolder, selectedFolder };
|
||||
|
||||
@@ -67,6 +67,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
|
||||
if (inter.system === true) return false;
|
||||
|
||||
const matchesType = inter.types.includes(state.fieldData?.type || 'alias');
|
||||
const matchesLocalType = inter.localTypes?.includes(type);
|
||||
let matchesRelation = false;
|
||||
|
||||
if (type === 'standard' || type === 'presentation') {
|
||||
@@ -81,7 +82,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
|
||||
matchesRelation = inter.relationship === type;
|
||||
}
|
||||
|
||||
return matchesType && matchesRelation;
|
||||
return matchesType && matchesRelation && (matchesLocalType === undefined || matchesLocalType);
|
||||
})
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
});
|
||||
|
||||
@@ -207,6 +207,7 @@ import { getInterfaces } from '@/interfaces';
|
||||
import router from '@/router';
|
||||
import notify from '@/utils/notify';
|
||||
import { i18n } from '@/lang';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getLocalTypeForField } from '../../get-local-type';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -323,8 +324,8 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
async function saveDuplicate() {
|
||||
const newField: any = {
|
||||
...props.field,
|
||||
const newField: Record<string, any> = {
|
||||
...cloneDeep(props.field),
|
||||
field: duplicateName.value,
|
||||
collection: duplicateTo.value,
|
||||
};
|
||||
@@ -334,6 +335,10 @@ export default defineComponent({
|
||||
delete newField.meta.sort;
|
||||
}
|
||||
|
||||
if (newField.schema) {
|
||||
delete newField.schema.comment;
|
||||
}
|
||||
|
||||
delete newField.name;
|
||||
|
||||
duplicating.value = true;
|
||||
|
||||
@@ -304,7 +304,6 @@ export default defineComponent({
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'full',
|
||||
required: true,
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
@@ -342,6 +341,7 @@ export default defineComponent({
|
||||
},
|
||||
schema: {
|
||||
default_value: 'draft',
|
||||
is_nullable: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button rounded icon @click="userInviteModalActive = true" v-tooltip.bottom="$t('invite_users')">
|
||||
<v-icon name="person_add" />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
@@ -55,6 +59,8 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<users-invite v-model="userInviteModalActive" :role="primaryKey" />
|
||||
|
||||
<div class="roles">
|
||||
<v-notice v-if="adminEnabled" type="info">
|
||||
{{ $t('admins_have_all_permissions') }}
|
||||
@@ -85,9 +91,11 @@ import SettingsNavigation from '../../../components/navigation.vue';
|
||||
import router from '@/router';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import { useUserStore } from '@/stores/';
|
||||
import { useUserStore, usePermissionsStore } from '@/stores/';
|
||||
import RoleInfoSidebarDetail from './components/role-info-sidebar-detail.vue';
|
||||
import PermissionsOverview from './components/permissions-overview.vue';
|
||||
import UsersInvite from '@/views/private/components/users-invite';
|
||||
import usersCreate from '../../../../../../../api/dist/cli/commands/users/create';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -95,7 +103,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-item',
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoSidebarDetail, PermissionsOverview },
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoSidebarDetail, PermissionsOverview, UsersInvite },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -108,7 +116,8 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userInviteModalActive = ref(false);
|
||||
const { primaryKey } = toRefs(props);
|
||||
|
||||
const { edits, item, saving, loading, error, save, remove, deleting, isBatch } = useItem(
|
||||
@@ -124,7 +133,7 @@ export default defineComponent({
|
||||
const values = {
|
||||
...item.value,
|
||||
...edits.value,
|
||||
};
|
||||
} as Record<string, any>;
|
||||
|
||||
return !!values.admin_access;
|
||||
});
|
||||
@@ -142,6 +151,7 @@ export default defineComponent({
|
||||
deleting,
|
||||
isBatch,
|
||||
adminEnabled,
|
||||
userInviteModalActive,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,7 +115,7 @@ export default defineComponent({
|
||||
const title = computed(() => {
|
||||
if (loading.value) return i18n.t('loading');
|
||||
if (isNew.value) return i18n.t('creating_webhook');
|
||||
return item.value.name;
|
||||
return item.value?.name;
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -49,6 +49,16 @@
|
||||
<v-icon name="edit" outline />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
v-if="canInviteUsers"
|
||||
rounded
|
||||
icon
|
||||
@click="userInviteModalActive = true"
|
||||
v-tooltip.bottom="$t('invite_users')"
|
||||
>
|
||||
<v-icon name="person_add" />
|
||||
</v-button>
|
||||
|
||||
<v-button rounded icon :to="addNewLink" v-tooltip.bottom="$t('create_user')">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
@@ -58,6 +68,8 @@
|
||||
<users-navigation :current-role="queryFilters && queryFilters.role" />
|
||||
</template>
|
||||
|
||||
<users-invite v-if="canInviteUsers" v-model="userInviteModalActive" />
|
||||
|
||||
<component
|
||||
class="layout"
|
||||
ref="layoutRef"
|
||||
@@ -104,6 +116,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, PropType } from '@vue/composition-api';
|
||||
import UsersNavigation from '../components/navigation.vue';
|
||||
import UsersInvite from '@/views/private/components/users-invite';
|
||||
|
||||
import { i18n } from '@/lang';
|
||||
import api from '@/api';
|
||||
@@ -111,6 +124,7 @@ import { LayoutComponent } from '@/layouts/types';
|
||||
import usePreset from '@/composables/use-preset';
|
||||
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
import { useUserStore, usePermissionsStore } from '@/stores';
|
||||
import marked from 'marked';
|
||||
import useNavigation from '../composables/use-navigation';
|
||||
|
||||
@@ -120,7 +134,7 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-collection',
|
||||
components: { UsersNavigation, LayoutSidebarDetail, SearchInput },
|
||||
components: { UsersNavigation, LayoutSidebarDetail, SearchInput, UsersInvite },
|
||||
props: {
|
||||
queryFilters: {
|
||||
type: Object as PropType<Record<string, string>>,
|
||||
@@ -130,6 +144,9 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const { roles } = useNavigation();
|
||||
const layoutRef = ref<LayoutComponent | null>(null);
|
||||
const userInviteModalActive = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
@@ -157,7 +174,23 @@ export default defineComponent({
|
||||
return filters.value;
|
||||
});
|
||||
|
||||
const canInviteUsers = computed(() => {
|
||||
const isAdmin = !!userStore.state.currentUser?.role?.admin_access;
|
||||
|
||||
if (isAdmin) return true;
|
||||
|
||||
const usersCreatePermission = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.collection === 'directus_users' && permission.action === 'create'
|
||||
);
|
||||
const rolesReadPermission = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.collection === 'directus_roles' && permission.action === 'read'
|
||||
);
|
||||
|
||||
return !!usersCreatePermission && !!rolesReadPermission;
|
||||
});
|
||||
|
||||
return {
|
||||
canInviteUsers,
|
||||
_filters,
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
@@ -175,6 +208,7 @@ export default defineComponent({
|
||||
searchQuery,
|
||||
marked,
|
||||
clearFilters,
|
||||
userInviteModalActive,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
|
||||
@@ -384,7 +384,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function refreshCurrentUser() {
|
||||
if (userStore.state.currentUser!.id === item.value.id) {
|
||||
if (userStore.state.currentUser!.id === item.value?.id) {
|
||||
await userStore.hydrate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ const fakeFilesField: Field = {
|
||||
display_options: null,
|
||||
hidden: false,
|
||||
locked: true,
|
||||
required: false,
|
||||
translations: null,
|
||||
readonly: true,
|
||||
width: 'full',
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.type-label {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
// full by default
|
||||
|
||||
@@ -60,7 +60,6 @@ export type FieldMeta = {
|
||||
options: null | Record<string, any>;
|
||||
display_options: null | Record<string, any>;
|
||||
readonly: boolean;
|
||||
required: boolean;
|
||||
sort: number | null;
|
||||
special: string[] | null;
|
||||
translations: null | Translations[];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { usePermissionsStore, useUserStore } from '@/stores';
|
||||
import { Permission } from '@/types';
|
||||
import generateJoi from '@/utils/generate-joi';
|
||||
|
||||
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any>) {
|
||||
export function isAllowed(collection: string, action: Permission['action'], value: Record<string, any> | null) {
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export default async function uploadFile(
|
||||
onProgressChange?: (percentage: number) => void;
|
||||
notifications?: boolean;
|
||||
preset?: Record<string, any>;
|
||||
fileId?: string;
|
||||
}
|
||||
) {
|
||||
const progressHandler = options?.onProgressChange || (() => undefined);
|
||||
@@ -25,9 +26,17 @@ export default async function uploadFile(
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
const response = await api.post(`/files`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
let response = null;
|
||||
|
||||
if (options?.fileId) {
|
||||
response = await api.patch(`/files/${options.fileId}`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
} else {
|
||||
response = await api.post(`/files`, formData, {
|
||||
onUploadProgress,
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.notifications) {
|
||||
notify({
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<v-drawer v-model="_active" :title="$t('select_item')" @cancel="cancel">
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="`layout-${localLayout}`"
|
||||
:collection="collection"
|
||||
@@ -7,6 +15,7 @@
|
||||
:filters="filters"
|
||||
:layout-query.sync="localQuery"
|
||||
:layout-options.sync="localOptions"
|
||||
:search-query="searchQuery"
|
||||
@update:selection="onSelect"
|
||||
select-mode
|
||||
class="layout"
|
||||
@@ -19,12 +28,6 @@
|
||||
<v-info :title="$tc('item_count', 0)" :icon="collectionInfo.icon" center />
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<template #actions>
|
||||
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
@@ -33,8 +36,10 @@ import { defineComponent, PropType, ref, computed, toRefs, onUnmounted } from '@
|
||||
import { Filter } from '@/types';
|
||||
import usePreset from '@/composables/use-preset';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchInput },
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
@@ -65,7 +70,7 @@ export default defineComponent({
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
const { layout, layoutOptions, layoutQuery } = usePreset(collection);
|
||||
const { layout, layoutOptions, layoutQuery, searchQuery } = usePreset(collection);
|
||||
|
||||
// This is a local copy of the layout. This means that we can sync it the layout without
|
||||
// having use-preset auto-save the values
|
||||
@@ -73,7 +78,18 @@ export default defineComponent({
|
||||
const localOptions = ref(layoutOptions.value);
|
||||
const localQuery = ref(layoutQuery.value);
|
||||
|
||||
return { save, cancel, _active, _selection, onSelect, localLayout, localOptions, localQuery, collectionInfo };
|
||||
return {
|
||||
save,
|
||||
cancel,
|
||||
_active,
|
||||
_selection,
|
||||
onSelect,
|
||||
localLayout,
|
||||
localOptions,
|
||||
localQuery,
|
||||
collectionInfo,
|
||||
searchQuery,
|
||||
};
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<div class="file-preview" v-if="type">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG, 'max-size': inModal === false }" @click="$emit('click')">
|
||||
<div
|
||||
v-if="type === 'image'"
|
||||
class="image"
|
||||
:class="{ svg: isSVG, 'max-size': inModal === false }"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<img
|
||||
:src="src"
|
||||
:width="width"
|
||||
:height="height"
|
||||
:style="{
|
||||
'maxWidth': width ? width + 'px' : '100%'
|
||||
maxWidth: width ? width + 'px' : '100%',
|
||||
}"
|
||||
:alt="title"
|
||||
/>
|
||||
<v-icon v-if="inModal === false" name="fullscreen" />
|
||||
<v-icon v-if="inModal === false" name="upload" />
|
||||
</div>
|
||||
|
||||
<video v-else-if="type === 'video'" controls :src="src" />
|
||||
@@ -106,9 +111,9 @@ audio {
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: inherit;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
max-height: inherit;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
|
||||
4
app/src/views/private/components/users-invite/index.ts
Normal file
4
app/src/views/private/components/users-invite/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import UsersInvite from './users-invite.vue';
|
||||
|
||||
export { UsersInvite };
|
||||
export default UsersInvite;
|
||||
121
app/src/views/private/components/users-invite/users-invite.vue
Normal file
121
app/src/views/private/components/users-invite/users-invite.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<v-dialog :active="active" @toggle="$emit('toggle', $event)" @esc="$emit('toggle', false)">
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('invite_users') }}</v-card-title>
|
||||
|
||||
<v-card-text class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('emails') }}</div>
|
||||
<interface-tags
|
||||
v-model="emails"
|
||||
:placeholder="$t('email_examples')"
|
||||
icon-right="email"
|
||||
whitespace=""
|
||||
/>
|
||||
</div>
|
||||
<div class="field" v-if="role === null">
|
||||
<div class="type-label">{{ $t('role') }}</div>
|
||||
<v-select v-model="roleSelected" :items="roles" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="$emit('toggle', false)">{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="inviteUsers" :disabled="emails === null || emails.length === 0" :loading="loading">
|
||||
{{ $t('invite') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, PropType, watch } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { useNotificationsStore } from '@/stores';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const notifications = useNotificationsStore();
|
||||
const emails = ref<string[]>([]);
|
||||
const roles = ref<Record<string, any>[]>([]);
|
||||
const roleSelected = ref<string | null>(props.role);
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
loadRoles();
|
||||
}
|
||||
);
|
||||
|
||||
return { emails, inviteUsers, roles, roleSelected, loading };
|
||||
|
||||
async function inviteUsers() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all(
|
||||
emails.value.map((email) => {
|
||||
return api.post('/users/invite', {
|
||||
email,
|
||||
role: roleSelected.value,
|
||||
});
|
||||
})
|
||||
);
|
||||
emit('toggle', false);
|
||||
} catch (err) {
|
||||
notifications.add({
|
||||
title: i18n.t('server_error'),
|
||||
text: err.message,
|
||||
persist: true,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const response = await api.get('/roles');
|
||||
|
||||
roles.value = response.data.data.map((role: Record<string, any>) => ({
|
||||
text: role.name,
|
||||
value: role.id,
|
||||
}));
|
||||
|
||||
if (roles.value.length > 0 && roleSelected.value === null) {
|
||||
roleSelected.value = roles.value[0].value;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.v-card-text {
|
||||
--v-form-vertical-gap: 20px;
|
||||
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -148,7 +148,7 @@ module.exports = function registerHook({ services, exceptions }) {
|
||||
|
||||
return {
|
||||
// Force everything to be admin-only at all times
|
||||
'items.*.*': async function({ item, accountability }) {
|
||||
'items.*': async function({ item, accountability }) {
|
||||
if (accountability.admin !== true) throw new ForbiddenException();
|
||||
},
|
||||
// Sync with external recipes service, cancel creation on failure
|
||||
|
||||
2
docs/package-lock.json
generated
2
docs/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/docs",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@directus/docs",
|
||||
"private": false,
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"docs",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"command": {
|
||||
"bootstrap": {
|
||||
"npmClientArgs": [
|
||||
|
||||
148
package-lock.json
generated
148
package-lock.json
generated
@@ -8216,13 +8216,6 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -8260,83 +8253,6 @@
|
||||
"worker-rpc": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
|
||||
@@ -8382,18 +8298,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
@@ -16008,6 +15912,7 @@
|
||||
"lodash": "^4.17.19",
|
||||
"macos-release": "^2.4.1",
|
||||
"memcached": "^2.2.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"ms": "^2.1.2",
|
||||
"mssql": "^6.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
@@ -16021,6 +15926,7 @@
|
||||
"pg": "^8.4.1",
|
||||
"pino": "^6.4.1",
|
||||
"pino-colada": "^2.1.0",
|
||||
"qs": "^6.9.4",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
@@ -16083,6 +15989,11 @@
|
||||
"integrity": "sha512-64/bYByMrhWULUaCd+6/72c9PMWhiVFs3EVxl9Ct6a3v/U8+rKgqP2w+kKg/BIGgMJyB+Bk/eNivT32Al+Jghw==",
|
||||
"optional": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
|
||||
@@ -18575,6 +18486,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"description": "A small installer util that will create a directory, add boilerplate folders, and install Directus through npm.",
|
||||
"main": "lib/index.js",
|
||||
"bin": "./lib/index.js",
|
||||
|
||||
2
packages/format-title/package-lock.json
generated
2
packages/format-title/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/format-title",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/format-title",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"description": "Custom string formatter that converts any string into [Title Case](http://www.grammar-monster.com/lessons/capital_letters_title_case.htm)",
|
||||
"keywords": [
|
||||
"title-case",
|
||||
|
||||
2
packages/spec/package-lock.json
generated
2
packages/spec/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "9.0.0-rc.1",
|
||||
"version": "9.0.0-rc.2",
|
||||
"description": "OpenAPI Specification of the Directus API",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user