mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into api-openapi
This commit is contained in:
24
.github/workflows/create-release.yml
vendored
Normal file
24
.github/workflows/create-release.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: create-release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
body: |
|
||||
Directus ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"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.",
|
||||
|
||||
@@ -62,7 +62,7 @@ const newFieldSchema = Joi.object({
|
||||
schema: Joi.object({
|
||||
comment: Joi.string().allow(null),
|
||||
default_value: Joi.any(),
|
||||
max_length: [Joi.number(), Joi.string()],
|
||||
max_length: [Joi.number(), Joi.string(), Joi.valid(null)],
|
||||
is_nullable: Joi.bool(),
|
||||
}).unknown(),
|
||||
/** @todo base this on default validation */
|
||||
|
||||
@@ -67,3 +67,18 @@ data:
|
||||
status: 36
|
||||
name: 300
|
||||
|
||||
- collection: directus_roles
|
||||
layout: tabular
|
||||
layout_query:
|
||||
tabular:
|
||||
fields:
|
||||
- icon
|
||||
- name
|
||||
- description
|
||||
layout_options:
|
||||
tabular:
|
||||
widths:
|
||||
icon: 36
|
||||
name: 248
|
||||
description: 500
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ fields:
|
||||
- collection: directus_roles
|
||||
field: icon
|
||||
interface: icon
|
||||
display: icon
|
||||
locked: true
|
||||
sort: 2
|
||||
width: half
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
export class CollectionNotFoundException extends BaseException {
|
||||
constructor(collection: string) {
|
||||
super(`Collection "${collection}" doesn't exist.`, 404, 'COLLECTION_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
export class FieldNotFoundException extends BaseException {
|
||||
constructor(collection: string, field: string) {
|
||||
super(
|
||||
`Field "${field}" in collection "${collection}" doesn't exist.`,
|
||||
404,
|
||||
'FIELD_NOT_FOUND'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,6 @@ type Extensions = {
|
||||
|
||||
export class ForbiddenException extends BaseException {
|
||||
constructor(message = `You don't have permission to access this.`, extensions?: Extensions) {
|
||||
super(message, 403, 'NO_PERMISSION', extensions);
|
||||
super(message, 403, 'FORBIDDEN', extensions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
export * from './base';
|
||||
export * from './collection-not-found';
|
||||
export * from './failed-validation';
|
||||
export * from './field-not-found';
|
||||
export * from './forbidden';
|
||||
export * from './hit-rate-limit';
|
||||
export * from './invalid-credentials';
|
||||
export * from './invalid-otp';
|
||||
export * from './invalid-payload';
|
||||
export * from './invalid-query';
|
||||
export * from './item-limit';
|
||||
export * from './item-not-found';
|
||||
export * from './route-not-found';
|
||||
export * from './service-unavailable';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
export class ItemLimitException extends BaseException {
|
||||
constructor(message: string) {
|
||||
super(message, 400, 'ITEM_LIMIT_REACHED');
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { BaseException } from './base';
|
||||
|
||||
export class ItemNotFoundException extends BaseException {
|
||||
constructor(id: string | number | (string | number)[], collection: string) {
|
||||
super(`Item "${id}" doesn't exist in "${collection}".`, 404, 'ITEM_NOT_FOUND');
|
||||
}
|
||||
}
|
||||
@@ -67,9 +67,7 @@ export class FieldsService {
|
||||
return data as Field;
|
||||
});
|
||||
|
||||
const aliasQuery = this.knex
|
||||
.select<any[]>('*')
|
||||
.from('directus_fields');
|
||||
const aliasQuery = this.knex.select<any[]>('*').from('directus_fields');
|
||||
|
||||
if (collection) {
|
||||
aliasQuery.andWhere('collection', collection);
|
||||
@@ -95,7 +93,7 @@ export class FieldsService {
|
||||
const data = {
|
||||
collection: field.collection,
|
||||
field: field.field,
|
||||
type: field.special,
|
||||
type: field.special[0],
|
||||
schema: null,
|
||||
meta: field,
|
||||
};
|
||||
@@ -192,9 +190,19 @@ export class FieldsService {
|
||||
|
||||
// Check if field already exists, either as a column, or as a row in directus_fields
|
||||
if (await this.schemaInspector.hasColumn(collection, field.field)) {
|
||||
throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
|
||||
} else if (!!await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()) {
|
||||
throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
|
||||
throw new InvalidPayloadException(
|
||||
`Field "${field.field}" already exists in collection "${collection}"`
|
||||
);
|
||||
} else if (
|
||||
!!(await this.knex
|
||||
.select('id')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: field.field })
|
||||
.first())
|
||||
) {
|
||||
throw new InvalidPayloadException(
|
||||
`Field "${field.field}" already exists in collection "${collection}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (field.schema) {
|
||||
|
||||
@@ -8,8 +8,7 @@ import path from 'path';
|
||||
import { AbstractServiceOptions, File, PrimaryKey } from '../types';
|
||||
import { clone } from 'lodash';
|
||||
import cache from '../cache';
|
||||
import notFound from '../controllers/not-found';
|
||||
import { ItemNotFoundException } from '../exceptions';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
|
||||
export class FilesService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
@@ -94,7 +93,7 @@ export class FilesService extends ItemsService {
|
||||
let files = await super.readByKey(keys, { fields: ['id', 'storage'] });
|
||||
|
||||
if (!files) {
|
||||
throw new ItemNotFoundException(key, 'directus_files');
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
files = Array.isArray(files) ? files : [files];
|
||||
|
||||
@@ -368,7 +368,7 @@ export class ItemsService implements AbstractService {
|
||||
data:
|
||||
snapshots && Array.isArray(snapshots)
|
||||
? JSON.stringify(snapshots?.[index])
|
||||
: snapshots,
|
||||
: JSON.stringify(snapshots),
|
||||
delta: JSON.stringify(payloadWithoutAliases),
|
||||
}));
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ import { format, formatISO } from 'date-fns';
|
||||
type Action = 'create' | 'read' | 'update';
|
||||
|
||||
type Transformers = {
|
||||
[type: string]: (
|
||||
action: Action,
|
||||
value: any,
|
||||
payload: Partial<Item>,
|
||||
accountability: Accountability | null
|
||||
) => Promise<any>;
|
||||
[type: string]: (context: {
|
||||
action: Action;
|
||||
value: any;
|
||||
payload: Partial<Item>;
|
||||
accountability: Accountability | null;
|
||||
}) => Promise<any>;
|
||||
};
|
||||
|
||||
export class PayloadService {
|
||||
@@ -48,7 +48,7 @@ export class PayloadService {
|
||||
* in order to work
|
||||
*/
|
||||
public transformers: Transformers = {
|
||||
async hash(action, value) {
|
||||
async hash({ action, value }) {
|
||||
if (!value) return;
|
||||
|
||||
if (action === 'create' || action === 'update') {
|
||||
@@ -57,14 +57,14 @@ export class PayloadService {
|
||||
|
||||
return value;
|
||||
},
|
||||
async uuid(action, value) {
|
||||
async uuid({ action, value }) {
|
||||
if (action === 'create' && !value) {
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
async 'file-links'(action, value, payload) {
|
||||
async 'file-links'({ action, value, payload }) {
|
||||
if (action === 'read' && payload && payload.storage && payload.filename_disk) {
|
||||
const publicKey = `STORAGE_${payload.storage.toUpperCase()}_PUBLIC_URL`;
|
||||
|
||||
@@ -77,14 +77,14 @@ export class PayloadService {
|
||||
// This is an non-existing column, so there isn't any data to save
|
||||
return undefined;
|
||||
},
|
||||
async boolean(action, value) {
|
||||
async boolean({ action, value }) {
|
||||
if (action === 'read') {
|
||||
return value === true || value === 1 || value === '1';
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
async json(action, value) {
|
||||
async json({ action, value }) {
|
||||
if (action === 'read') {
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
@@ -97,37 +97,36 @@ export class PayloadService {
|
||||
|
||||
return value;
|
||||
},
|
||||
async conceal(action, value) {
|
||||
async conceal({ action, value }) {
|
||||
if (action === 'read') return value ? '**********' : null;
|
||||
return value;
|
||||
},
|
||||
async 'user-created'(action, value, payload, accountability) {
|
||||
async 'user-created'({ action, value, payload, accountability }) {
|
||||
if (action === 'create') return accountability?.user || null;
|
||||
return value;
|
||||
},
|
||||
async 'user-updated'(action, value, payload, accountability) {
|
||||
async 'user-updated'({ action, value, payload, accountability }) {
|
||||
if (action === 'update') return accountability?.user || null;
|
||||
return value;
|
||||
},
|
||||
async 'role-created'(action, value, payload, accountability) {
|
||||
async 'role-created'({ action, value, payload, accountability }) {
|
||||
if (action === 'create') return accountability?.role || null;
|
||||
return value;
|
||||
},
|
||||
async 'role-updated'(action, value, payload, accountability) {
|
||||
async 'role-updated'({ action, value, payload, accountability }) {
|
||||
if (action === 'update') return accountability?.role || null;
|
||||
return value;
|
||||
},
|
||||
async 'date-created'(action, value) {
|
||||
async 'date-created'({ action, value }) {
|
||||
if (action === 'create') return new Date();
|
||||
return value;
|
||||
},
|
||||
async 'date-updated'(action, value) {
|
||||
async 'date-updated'({ action, value }) {
|
||||
if (action === 'update') return new Date();
|
||||
return value;
|
||||
},
|
||||
async csv(action, value) {
|
||||
async csv({ action, value }) {
|
||||
if (!value) return;
|
||||
// if (Array.isArray(value) && action === 'read') return value;
|
||||
if (action === 'read') return value.split(',');
|
||||
|
||||
if (Array.isArray(value)) return value.join(',');
|
||||
@@ -214,7 +213,12 @@ export class PayloadService {
|
||||
|
||||
for (const special of fieldSpecials) {
|
||||
if (this.transformers.hasOwnProperty(special)) {
|
||||
value = await this.transformers[special](action, value, payload, accountability);
|
||||
value = await this.transformers[special]({
|
||||
action,
|
||||
value,
|
||||
payload,
|
||||
accountability,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,13 +238,17 @@ export class PayloadService {
|
||||
type: getLocalType(column.type),
|
||||
}));
|
||||
|
||||
const dateColumns = columnsWithType.filter((column) => ['dateTime', 'date', 'timestamp'].includes(column.type));
|
||||
const dateColumns = columnsWithType.filter((column) =>
|
||||
['dateTime', 'date', 'timestamp'].includes(column.type)
|
||||
);
|
||||
|
||||
if (dateColumns.length === 0) return payloads;
|
||||
|
||||
for (const dateColumn of dateColumns) {
|
||||
for (const payload of payloads) {
|
||||
const value: Date = payload[dateColumn.name];
|
||||
let value: string | Date = payload[dateColumn.name];
|
||||
|
||||
if (typeof value === 'string') value = new Date(value);
|
||||
|
||||
if (value) {
|
||||
if (dateColumn.type === 'timestamp') {
|
||||
@@ -345,20 +353,19 @@ export class PayloadService {
|
||||
});
|
||||
|
||||
for (const relation of relationsToProcess) {
|
||||
const relatedRecords: Partial<Item>[] = payload[relation.one_field]
|
||||
.map(
|
||||
(record: string | number | Partial<Item>) => {
|
||||
if (typeof record === 'string' || typeof record === 'number') {
|
||||
record = {
|
||||
[relation.many_primary]: record
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
[relation.many_field]: parent || payload[relation.one_primary],
|
||||
}
|
||||
const relatedRecords: Partial<Item>[] = payload[relation.one_field].map(
|
||||
(record: string | number | Partial<Item>) => {
|
||||
if (typeof record === 'string' || typeof record === 'number') {
|
||||
record = {
|
||||
[relation.many_primary]: record,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...record,
|
||||
[relation.many_field]: parent || payload[relation.one_primary],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const itemsService = new ItemsService(relation.many_collection, {
|
||||
|
||||
@@ -1,8 +1,40 @@
|
||||
import { AbstractServiceOptions } from '../types';
|
||||
import { AbstractServiceOptions, PermissionsAction } from '../types';
|
||||
import { ItemsService } from '../services/items';
|
||||
|
||||
export class PermissionsService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
super('directus_permissions', options);
|
||||
}
|
||||
|
||||
async getAllowedCollections(role: string | null, action: PermissionsAction) {
|
||||
const query = this.knex
|
||||
.select('collection')
|
||||
.from('directus_permissions')
|
||||
.where({ role, action });
|
||||
const results = await query;
|
||||
return results.map((result) => result.collection);
|
||||
}
|
||||
|
||||
async getAllowedFields(role: string | null, action: PermissionsAction, collection?: string) {
|
||||
const query = this.knex
|
||||
.select('collection', 'fields')
|
||||
.from('directus_permissions')
|
||||
.where({ role, action });
|
||||
|
||||
if (collection) {
|
||||
query.andWhere({ collection });
|
||||
}
|
||||
|
||||
const results = await query;
|
||||
|
||||
const fieldsPerCollection: Record<string, string[]> = {};
|
||||
|
||||
for (const result of results) {
|
||||
const { collection, fields } = result;
|
||||
if (!fieldsPerCollection[collection]) fieldsPerCollection[collection] = [];
|
||||
fieldsPerCollection[collection].push(...(fields || '').split(','));
|
||||
}
|
||||
|
||||
return fieldsPerCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
import { ItemsService } from './items';
|
||||
import { AbstractServiceOptions } from '../types';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Query,
|
||||
Item,
|
||||
PrimaryKey,
|
||||
PermissionsAction,
|
||||
Relation,
|
||||
} from '../types';
|
||||
import { PermissionsService } from './permissions';
|
||||
|
||||
/**
|
||||
* @TODO update foreign key constraints when relations are updated
|
||||
*/
|
||||
|
||||
export class RelationsService extends ItemsService {
|
||||
permissionsService: PermissionsService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
super('directus_relations', options);
|
||||
this.permissionsService = new PermissionsService(options);
|
||||
}
|
||||
|
||||
async readByQuery(query: Query): Promise<null | Item | Item[]> {
|
||||
const results = (await super.readByQuery(query)) as Relation | Relation[] | null;
|
||||
const filteredResults = await this.filterForbidden(results);
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
readByKey(
|
||||
keys: PrimaryKey[],
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Item[]>;
|
||||
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Item>;
|
||||
async readByKey(
|
||||
key: PrimaryKey | PrimaryKey[],
|
||||
query: Query = {},
|
||||
action: PermissionsAction = 'read'
|
||||
): Promise<null | Item | Item[]> {
|
||||
const results = (await super.readByKey(key as any, query, action)) as
|
||||
| Relation
|
||||
| Relation[]
|
||||
| null;
|
||||
const filteredResults = await this.filterForbidden(results);
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
private async filterForbidden(relations: Relation | Relation[] | null) {
|
||||
if (relations === null) return null;
|
||||
if (this.accountability === null || this.accountability?.admin === true) return relations;
|
||||
|
||||
const allowedCollections = await this.permissionsService.getAllowedCollections(
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
);
|
||||
const allowedFields = await this.permissionsService.getAllowedFields(
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
);
|
||||
|
||||
relations = Array.isArray(relations) ? relations : [relations];
|
||||
|
||||
return relations.filter((relation) => {
|
||||
const collectionsAllowed =
|
||||
allowedCollections.includes(relation.many_collection) &&
|
||||
allowedCollections.includes(relation.one_collection);
|
||||
|
||||
const fieldsAllowed =
|
||||
allowedFields[relation.one_collection] &&
|
||||
allowedFields[relation.many_collection] &&
|
||||
(allowedFields[relation.many_collection].includes('*') ||
|
||||
allowedFields[relation.many_collection].includes(relation.many_field)) &&
|
||||
(allowedFields[relation.one_collection].includes('*') ||
|
||||
allowedFields[relation.one_collection].includes(relation.one_field));
|
||||
|
||||
return collectionsAllowed && fieldsAllowed;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ItemsService } from './items';
|
||||
import { AbstractServiceOptions, PrimaryKey, Revision } from '../types';
|
||||
import { InvalidPayloadException, ItemNotFoundException } from '../exceptions';
|
||||
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
|
||||
|
||||
/**
|
||||
* @TODO only return data / delta based on permissions you have for the requested collection
|
||||
@@ -13,7 +13,7 @@ export class RevisionsService extends ItemsService {
|
||||
|
||||
async revert(pk: PrimaryKey) {
|
||||
const revision = (await super.readByKey(pk)) as Revision | null;
|
||||
if (!revision) throw new ItemNotFoundException(pk, 'directus_revisions');
|
||||
if (!revision) throw new ForbiddenException();
|
||||
|
||||
if (!revision.data)
|
||||
throw new InvalidPayloadException(`Revision doesn't contain data to revert to`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"private": false,
|
||||
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
|
||||
"author": "Rijk van Zanten <rijk@rngr.org>",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<v-notice v-if="!availableFields || availableFields.length === 0">
|
||||
{{ $t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
|
||||
</v-notice>
|
||||
|
||||
<draggable v-else v-model="selectedFields" draggable=".draggable" :set-data="hideDragImage" class="v-field-select">
|
||||
<v-chip
|
||||
v-for="(field, index) in selectedFields"
|
||||
@@ -40,7 +41,7 @@
|
||||
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType, computed } from '@vue/composition-api';
|
||||
import FieldListItem from '../v-field-template/field-list-item.vue';
|
||||
import { useFieldsStore } from '@/stores';
|
||||
import { Field } from '@/types/';
|
||||
import { Field, Collection, Relation } from '@/types';
|
||||
import Draggable from 'vuedraggable';
|
||||
import useFieldTree from '@/composables/use-field-tree';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
@@ -66,6 +67,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
inject: {
|
||||
type: Object as PropType<{ fields: Field[]; collections: Collection[]; relations: Relation[] } | null>,
|
||||
default: () => ({ fields: [], collections: [], relations: [] }),
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
@@ -74,7 +79,10 @@ export default defineComponent({
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info, primaryKeyField, fields: fieldsInCollection, sortField } = useCollection(collection);
|
||||
const { tree } = useFieldTree(collection, true);
|
||||
const { tree } = useFieldTree(collection, {
|
||||
fields: props.inject?.fields.filter((field) => field.collection === props.collection) || [],
|
||||
relations: props.inject?.relations || [],
|
||||
});
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
@@ -98,7 +106,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const availableFields = computed(() => {
|
||||
return filterTree(tree.value);
|
||||
return parseTree(tree.value);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -122,7 +130,7 @@ export default defineComponent({
|
||||
return findTree(fieldObject.children, fieldSections.slice(1));
|
||||
}
|
||||
|
||||
function filterTree(tree: FieldTree[] | undefined, prefix = '') {
|
||||
function parseTree(tree: FieldTree[] | undefined, prefix = '') {
|
||||
if (tree === undefined) return undefined;
|
||||
|
||||
const newTree: FieldTree[] = tree.map((field) => {
|
||||
@@ -130,7 +138,7 @@ export default defineComponent({
|
||||
name: field.name,
|
||||
field: field.field,
|
||||
disabled: _value.value.includes(prefix + field.field),
|
||||
children: filterTree(field.children, prefix + field.field + '.'),
|
||||
children: parseTree(field.children, prefix + field.field + '.'),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
:disabled="field.disabled"
|
||||
@click="$emit('add', `${parent ? parent + '.' : ''}${field.field}`)"
|
||||
>
|
||||
<v-list-item-content>{{ field.name }}</v-list-item-content>
|
||||
<v-list-item-content>{{ field.name || formatTitle(field.field) }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-group v-else>
|
||||
<template #activator>{{ field.name }}</template>
|
||||
<template #activator>{{ field.name || formatTitle(field.field) }}</template>
|
||||
<field-list-item
|
||||
v-for="childField in field.children"
|
||||
:key="childField.field"
|
||||
@@ -22,6 +22,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { FieldTree } from './types';
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'field-list-item',
|
||||
@@ -39,5 +40,8 @@ export default defineComponent({
|
||||
default: 2,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return { formatTitle };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { defineComponent, PropType, computed, ref, provide } from '@vue/composit
|
||||
import { useFieldsStore } from '@/stores/';
|
||||
import { Field, FilterOperator } from '@/types';
|
||||
import { useElementSize } from '@/composables/use-element-size';
|
||||
import { clone } from 'lodash';
|
||||
import { clone, cloneDeep } from 'lodash';
|
||||
import marked from 'marked';
|
||||
import FormField from './form-field.vue';
|
||||
import useFormFields from '@/composables/use-form-fields';
|
||||
@@ -106,7 +106,7 @@ export default defineComponent({
|
||||
function useForm() {
|
||||
const fields = computed(() => {
|
||||
if (props.collection) {
|
||||
return fieldsStore.state.fields.filter((field) => field.collection === props.collection);
|
||||
return fieldsStore.getFieldsForCollection(props.collection)
|
||||
}
|
||||
|
||||
if (props.fields) {
|
||||
@@ -123,7 +123,7 @@ export default defineComponent({
|
||||
|
||||
return formFields.value.map((field: Field) => {
|
||||
if (field.schema?.is_primary_key === true) {
|
||||
const fieldClone = clone(field) as any;
|
||||
const fieldClone = cloneDeep(field) as any;
|
||||
if (!fieldClone.meta) fieldClone.meta = {};
|
||||
fieldClone.meta.readonly = true;
|
||||
return fieldClone;
|
||||
|
||||
@@ -123,7 +123,7 @@ export default defineComponent({
|
||||
},
|
||||
trim: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, listeners }) {
|
||||
@@ -175,6 +175,7 @@ export default defineComponent({
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
emit('keydown', event);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
:disabled="item.disabled"
|
||||
@click="multiple ? null : $emit('input', item.value)"
|
||||
>
|
||||
<v-list-item-icon v-if="multiple === false && allowOther === false && itemIcon !== null && item.icon">
|
||||
<v-icon :name="item.icon" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<span v-if="multiple === false" class="item-text">{{ item.text }}</span>
|
||||
<v-checkbox
|
||||
@@ -142,6 +145,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'value',
|
||||
},
|
||||
itemIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: [Array, String, Number] as PropType<InputValue>,
|
||||
default: null,
|
||||
@@ -215,6 +222,7 @@ export default defineComponent({
|
||||
return {
|
||||
text: item[props.itemText],
|
||||
value: item[props.itemValue],
|
||||
icon: item[props.itemIcon],
|
||||
disabled: item.disabled,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,20 +3,16 @@ import { FieldTree } from './types';
|
||||
import { useFieldsStore, useRelationsStore } from '@/stores/';
|
||||
import { Field, Relation } from '@/types';
|
||||
|
||||
export default function useFieldTree(collection: Ref<string>, showHidden = false) {
|
||||
export default function useFieldTree(collection: Ref<string>, inject?: { fields: Field[]; relations: Relation[] }) {
|
||||
const fieldsStore = useFieldsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const tree = computed<FieldTree[]>(() => {
|
||||
return fieldsStore
|
||||
.getFieldsForCollection(collection.value)
|
||||
return [...fieldsStore.getFieldsForCollection(collection.value), ...(inject?.fields || [])]
|
||||
.filter((field: Field) => {
|
||||
let shown = (field.meta?.special || []).includes('alias') === false;
|
||||
|
||||
if (showHidden === false && field.meta?.hidden === true) {
|
||||
shown = false;
|
||||
}
|
||||
|
||||
const shown =
|
||||
field.meta?.special?.includes('alias') !== true &&
|
||||
field.meta?.special?.includes('no-data') !== true;
|
||||
return shown;
|
||||
})
|
||||
.map((field: Field) => parseField(field, []));
|
||||
@@ -31,7 +27,14 @@ export default function useFieldTree(collection: Ref<string>, showHidden = false
|
||||
return fieldInfo;
|
||||
}
|
||||
|
||||
const relations = relationsStore.getRelationsForField(field.collection, field.field);
|
||||
const relations = [
|
||||
...relationsStore.getRelationsForField(field.collection, field.field),
|
||||
...(inject?.relations || []).filter(
|
||||
(relation) =>
|
||||
(relation.many_collection === field.collection && relation.many_field === field.field) ||
|
||||
(relation.one_collection === field.collection && relation.one_field === field.field)
|
||||
),
|
||||
];
|
||||
|
||||
if (relations.length > 0) {
|
||||
const relatedFields = relations
|
||||
@@ -43,13 +46,12 @@ export default function useFieldTree(collection: Ref<string>, showHidden = false
|
||||
|
||||
if (relation.junction_field === field.field) return [];
|
||||
|
||||
return fieldsStore
|
||||
.getFieldsForCollection(relatedCollection)
|
||||
.filter(
|
||||
(field: Field) =>
|
||||
field.meta?.hidden === false &&
|
||||
(field.meta?.special || []).includes('alias') === false
|
||||
);
|
||||
return fieldsStore.getFieldsForCollection(relatedCollection).filter((field: Field) => {
|
||||
const shown =
|
||||
field.meta?.special?.includes('alias') !== true &&
|
||||
field.meta?.special?.includes('no-data') !== true;
|
||||
return shown;
|
||||
});
|
||||
})
|
||||
.flat()
|
||||
.map((childField: Field) => parseField(childField, [...parents, field]));
|
||||
|
||||
@@ -15,10 +15,7 @@ document.body.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
|
||||
document.body.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
if (event.repeat || !event.key) return;
|
||||
|
||||
const key = mapKeys(event.key);
|
||||
keysdown.delete(key.toLowerCase());
|
||||
keysdown.delete(key.toUpperCase());
|
||||
keysdown.clear();
|
||||
});
|
||||
|
||||
export default function useShortcut(
|
||||
|
||||
@@ -5,7 +5,13 @@
|
||||
<div v-else class="form-grid">
|
||||
<div class="field full">
|
||||
<p class="type-label">{{ $t('select_fields') }}</p>
|
||||
<v-field-select :collection="junctionCollection" v-model="fields" />
|
||||
<v-field-select
|
||||
:collection="junctionCollection"
|
||||
v-model="fields"
|
||||
:inject="
|
||||
junctionCollectionExists ? null : { fields: newFields, collections: newCollections, relations }
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -14,7 +20,8 @@
|
||||
import { Field } from '@/types';
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { useRelationsStore } from '@/stores/';
|
||||
import { Relation } from '@/types';
|
||||
import { Relation, Collection } from '@/types';
|
||||
import { useCollectionsStore } from '../../stores';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -34,8 +41,17 @@ export default defineComponent({
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
newCollections: {
|
||||
type: Array as PropType<Collection[]>,
|
||||
default: () => [],
|
||||
},
|
||||
newFields: {
|
||||
type: Array as PropType<Field[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const fields = computed({
|
||||
@@ -59,7 +75,13 @@ export default defineComponent({
|
||||
return junctionRelation?.many_collection || null;
|
||||
});
|
||||
|
||||
return { fields, junctionCollection };
|
||||
const junctionCollectionExists = computed(() => {
|
||||
return !!collectionsStore.state.collections.find(
|
||||
(collection) => collection.collection === junctionCollection.value
|
||||
);
|
||||
});
|
||||
|
||||
return { fields, junctionCollection, junctionCollectionExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -161,7 +161,9 @@ export default function usePreview({
|
||||
|
||||
const junctionPrimaryKey = junctionCollectionPrimaryKeyField.value.field;
|
||||
|
||||
return (value.value || []).filter((stagedEdit: any) => !stagedEdit.$delete && !stagedEdit[junctionPrimaryKey]);
|
||||
return (value.value || []).filter(
|
||||
(stagedEdit: any) => !stagedEdit.$delete && !stagedEdit[junctionPrimaryKey] && stagedEdit.$new === true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
<div v-else class="form-grid">
|
||||
<div class="field full">
|
||||
<p class="type-label">{{ $t('select_fields') }}</p>
|
||||
<v-field-select :collection="relatedCollection" v-model="fields" />
|
||||
<v-field-select
|
||||
:collection="relatedCollection"
|
||||
v-model="fields"
|
||||
:inject="relatedCollectionExists ? null : { fields: newFields, collections: newCollections, relations }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Field, Relation } from '@/types';
|
||||
import { Field, Relation, Collection } from '@/types';
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { useRelationsStore } from '@/stores/';
|
||||
import { useRelationsStore, useCollectionsStore } from '@/stores/';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -33,8 +37,17 @@ export default defineComponent({
|
||||
type: Object as PropType<any>,
|
||||
default: null,
|
||||
},
|
||||
newCollections: {
|
||||
type: Array as PropType<Collection[]>,
|
||||
default: () => [],
|
||||
},
|
||||
newFields: {
|
||||
type: Array as PropType<Field[]>,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
const relationsStore = useRelationsStore();
|
||||
|
||||
const fields = computed({
|
||||
@@ -58,7 +71,13 @@ export default defineComponent({
|
||||
return relatedRelation?.many_collection || null;
|
||||
});
|
||||
|
||||
return { fields, relatedCollection };
|
||||
const relatedCollectionExists = computed(() => {
|
||||
return !!collectionsStore.state.collections.find(
|
||||
(collection) => collection.collection === relatedCollection.value
|
||||
);
|
||||
});
|
||||
|
||||
return { fields, relatedCollection, relatedCollectionExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-for="(row, index) in value"
|
||||
:key="index"
|
||||
:value="row"
|
||||
:template="template"
|
||||
:template="_template"
|
||||
:fields="fields"
|
||||
@input="updateValues(index, $event)"
|
||||
@delete="removeItem(row)"
|
||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
addLabel: {
|
||||
type: String,
|
||||
@@ -58,6 +58,11 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const selection = ref<number[]>([]);
|
||||
|
||||
const _template = computed(() => {
|
||||
if(props.template === null) return props.fields.length > 0 ? `{{${ props.fields[0].field}}}` : ''
|
||||
return props.template
|
||||
})
|
||||
|
||||
const showAddNew = computed(() => {
|
||||
if (props.disabled) return false;
|
||||
if (props.value === null) return true;
|
||||
@@ -66,7 +71,7 @@ export default defineComponent({
|
||||
return false;
|
||||
});
|
||||
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, selection };
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, selection, _template };
|
||||
|
||||
function updateValues(index: number, updatedValues: any) {
|
||||
emitValue(
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
"related_values": "Related Values",
|
||||
|
||||
"last_page": "Last Page",
|
||||
"last_login": "Last Login",
|
||||
"last_access": "Last Access",
|
||||
|
||||
"fill_template": "Fill with Template Value",
|
||||
|
||||
@@ -587,7 +587,7 @@
|
||||
"errors": {
|
||||
"COLLECTION_NOT_FOUND": "Collection doesn't exist.",
|
||||
"FIELD_NOT_FOUND": "Field not found.",
|
||||
"NO_PERMISSION": "Forbidden.",
|
||||
"FORBIDDEN": "Forbidden.",
|
||||
"INVALID_CREDENTIALS": "Wrong username or password.",
|
||||
"INVALID_OTP": "Wrong one-time password.",
|
||||
"INVALID_PAYLOAD": "Invalid payload.",
|
||||
|
||||
@@ -36,7 +36,8 @@
|
||||
"datetime": "Datetime",
|
||||
"description": "Enter dates and times",
|
||||
"include_seconds": "Include Seconds",
|
||||
"set_to_now": "Set to Now"
|
||||
"set_to_now": "Set to Now",
|
||||
"use_24": "Use 24-Hour Format"
|
||||
},
|
||||
"display-template": {
|
||||
"display-template": "Display Template",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</draggable>
|
||||
|
||||
<v-checkbox
|
||||
v-for="field in fieldsInCollection.filter((field) => fields.includes(field.field) === false)"
|
||||
v-for="field in availableFields.filter((field) => fields.includes(field.field) === false)"
|
||||
v-model="fields"
|
||||
:key="field.field"
|
||||
:value="field.field"
|
||||
@@ -261,6 +261,10 @@ export default defineComponent({
|
||||
return count;
|
||||
});
|
||||
|
||||
const availableFields = computed(() => {
|
||||
return fieldsInCollection.value.filter((field) => field.meta?.special?.includes('no-data') !== true);
|
||||
});
|
||||
|
||||
useShortcut(
|
||||
'meta+a',
|
||||
() => {
|
||||
@@ -299,6 +303,7 @@ export default defineComponent({
|
||||
activeFilterCount,
|
||||
refresh,
|
||||
resetPresetAndRefresh,
|
||||
availableFields,
|
||||
};
|
||||
|
||||
async function resetPresetAndRefresh() {
|
||||
@@ -360,13 +365,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
const fields =
|
||||
_layoutQuery.value?.fields ||
|
||||
fieldsInCollection.value
|
||||
.filter((field: Field) => {
|
||||
return field.schema?.is_primary_key === false;
|
||||
})
|
||||
.slice(0, 4)
|
||||
.map(({ field }) => field);
|
||||
_layoutQuery.value?.fields || fieldsInCollection.value.slice(0, 4).map(({ field }) => field);
|
||||
|
||||
return fields;
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
secondary
|
||||
exact
|
||||
v-tooltip.bottom="$t('back')"
|
||||
@click="$router.go(-1)"
|
||||
:to="'/collections/' + collection"
|
||||
>
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
|
||||
@@ -24,16 +24,16 @@ const sections: Section[] = [
|
||||
to: '/docs/getting-started/introduction',
|
||||
},
|
||||
{
|
||||
name: 'Troubleshooting',
|
||||
to: '/docs/getting-started/troubleshooting',
|
||||
name: 'Technical Support',
|
||||
to: '/docs/getting-started/technical-support',
|
||||
},
|
||||
{
|
||||
name: 'Contributing',
|
||||
to: '/docs/getting-started/contributing',
|
||||
},
|
||||
{
|
||||
name: 'Supporting Directus',
|
||||
to: '/docs/getting-started/supporting-directus',
|
||||
name: 'Backing Directus',
|
||||
to: '/docs/getting-started/backing-directus',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -173,6 +173,10 @@ const sections: Section[] = [
|
||||
name: 'Error Codes',
|
||||
to: '/docs/reference/error-codes',
|
||||
},
|
||||
{
|
||||
name: 'Item Rules',
|
||||
to: '/docs/reference/item-rules',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -285,6 +285,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
table {
|
||||
min-width: 100%;
|
||||
margin: 40px 0;
|
||||
padding: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<files-not-found v-if="!loading && !item" />
|
||||
<private-view v-else :title="loading || !item ? $t('loading') : item.title">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact @click="$router.go(-1)">
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="to">
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
@@ -286,6 +286,11 @@ export default defineComponent({
|
||||
.filter((field: Field) => fieldsDenyList.includes(field.field) === false);
|
||||
});
|
||||
|
||||
const to = computed(() => {
|
||||
if(item.value?.folder !== undefined) return `/files?folder=${item.value.folder}`
|
||||
else return '/files'
|
||||
})
|
||||
|
||||
const { formFields } = useFormFields(fieldsFiltered);
|
||||
|
||||
const confirmLeave = ref(false);
|
||||
@@ -327,6 +332,7 @@ export default defineComponent({
|
||||
selectedFolder,
|
||||
fileSrc,
|
||||
form,
|
||||
to
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
:collection="collection"
|
||||
:field-data="fieldData"
|
||||
:relations="relations"
|
||||
:new-fields="newFields"
|
||||
:new-collections="newCollections"
|
||||
:is="`interface-options-${selectedInterface.id}`"
|
||||
v-else
|
||||
/>
|
||||
@@ -115,9 +117,9 @@ export default defineComponent({
|
||||
return interfaces.value.find((inter) => inter.id === state.fieldData.meta.interface);
|
||||
});
|
||||
|
||||
const { fieldData, relations } = toRefs(state);
|
||||
const { fieldData, relations, newCollections, newFields } = toRefs(state);
|
||||
|
||||
return { fieldData, relations, selectItems, selectedInterface };
|
||||
return { fieldData, relations, selectItems, selectedInterface, newCollections, newFields };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -659,7 +659,7 @@ function initLocalStore(
|
||||
delete state.fieldData.schema;
|
||||
state.fieldData.type = null;
|
||||
|
||||
state.fieldData.meta.special = ['alias'];
|
||||
state.fieldData.meta.special = ['alias', 'no-data'];
|
||||
}
|
||||
|
||||
if (type === 'standard') {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<private-view :title="collectionInfo && collectionInfo.name">
|
||||
<template #headline>{{ $t('settings_data_model') }}</template>
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon exact @click="$router.go(-1)">
|
||||
<v-button class="header-icon" rounded icon exact to="/settings/data-model">
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
@@ -28,14 +28,16 @@ export function getLocalTypeForField(
|
||||
return 'translations';
|
||||
}
|
||||
|
||||
const relationForCurrent = relations.find(
|
||||
(relation: Relation) =>
|
||||
const relationForCurrent = relations.find((relation: Relation) => {
|
||||
return (
|
||||
(relation.many_collection === collection && relation.many_field === field) ||
|
||||
(relation.one_collection === collection && relation.one_field === field)
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
if (relationForCurrent?.many_collection === collection && relationForCurrent?.many_field === field)
|
||||
if (relationForCurrent?.many_collection === collection && relationForCurrent?.many_field === field) {
|
||||
return 'm2o';
|
||||
}
|
||||
|
||||
if (relations[0].one_collection === 'directus_files' || relations[1].one_collection === 'directus_files') {
|
||||
return 'files';
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<router-link :to="user.last_page">{{ user.last_page }}</router-link>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-if="user.last_login">
|
||||
<dt>{{ $t('last_login') }}</dt>
|
||||
<dd>{{ user.last_login }}</dd>
|
||||
<div v-if="user.last_access">
|
||||
<dt>{{ $t('last_access') }}</dt>
|
||||
<dd>{{ lastAccessDate }}</dd>
|
||||
</div>
|
||||
<div v-if="user.created_on">
|
||||
<dt>{{ $t('created_on') }}</dt>
|
||||
@@ -36,8 +36,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, ref, watch } from '@vue/composition-api';
|
||||
import marked from 'marked';
|
||||
import localizedFormat from '../../../utils/localized-format';
|
||||
import i18n from '../../../lang';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -51,7 +53,21 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return { marked };
|
||||
const lastAccessDate = ref('');
|
||||
|
||||
watch(
|
||||
props,
|
||||
async () => {
|
||||
if (!props.user) return;
|
||||
lastAccessDate.value = await localizedFormat(
|
||||
new Date(props.user.last_access),
|
||||
String(i18n.t('date-fns_date_short'))
|
||||
);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { marked, lastAccessDate };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<private-view :title="title">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact @click="$router.go(-1)">
|
||||
<v-button class="header-icon" rounded icon secondary exact to="/users">
|
||||
<v-icon name="arrow_back" />
|
||||
</v-button>
|
||||
</template>
|
||||
@@ -270,11 +270,11 @@ export default defineComponent({
|
||||
'id',
|
||||
'external_id',
|
||||
'last_page',
|
||||
'last_login',
|
||||
'created_on',
|
||||
'created_by',
|
||||
'modified_by',
|
||||
'modified_on',
|
||||
'last_access',
|
||||
];
|
||||
|
||||
const fieldsFiltered = computed(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="sso-links">
|
||||
<template v-if="providers.length > 0">
|
||||
<template v-if="providers && providers.length > 0">
|
||||
<v-divider />
|
||||
|
||||
<a class="sso-link" v-for="provider in providers" :key="provider.name" :href="provider.link">
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<div class="layout-options">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('layout') }}</div>
|
||||
<v-select :items="layouts" item-text="name" item-value="id" v-model="layout" />
|
||||
<v-select :items="layouts" item-text="name" item-value="id" item-icon="icon" v-model="layout">
|
||||
<template v-if="currentLayout.icon" #prepend>
|
||||
<v-icon :name="currentLayout.icon" />
|
||||
</template>
|
||||
</v-select>
|
||||
</div>
|
||||
|
||||
<portal-target name="layout-options" class="portal-contents" />
|
||||
|
||||
@@ -114,10 +114,10 @@ This module aggregates all files within the project into one consolidated librar
|
||||
|
||||
Similar to other [Item Detail](#) pages, this page provides a custom form for viewing assets and embeds. Directus ships with a full-featured system for digital asset management, with the following fields:
|
||||
|
||||
* **Title** — Pulled from @TODO, falls back to a formatted version of the filename
|
||||
* **Description** — Pulled from @TODO
|
||||
* **Tags** — Pulled from @TODO
|
||||
* **Location** — Pulled from @TODO
|
||||
* **Title** — Pulled from the file metadata if available, falls back to a formatted version of the filename
|
||||
* **Description** — Pulled from the file metadata if available
|
||||
* **Tags** — Pulled from the file metadata if available
|
||||
* **Location** — Pulled from the file metadata if available
|
||||
* **Storage** — The storage adapter where the asset is saved (readonly)
|
||||
* **Filename Disk** — The actual name of the file within the storage adapter
|
||||
* **Filename Download** — The name used when downloading the file via _Content-Disposition_
|
||||
@@ -130,7 +130,7 @@ The sidebar's info component also includes the following readonly details:
|
||||
* **Created** — The timestamp of when the file was uploaded to the project
|
||||
* **Owner** — The Directus user that uploaded the file to the project
|
||||
* **Folder** — The current parent folder that contains the file
|
||||
* **Metadata** — [Metadata](#) @TODO
|
||||
* **Metadata** — [Metadata](#) JSON dump of the file's EXIF, IPTC, and ICC information
|
||||
|
||||
::: Extending Files
|
||||
While the fields included out-of-the-box are locked from schema changes, you can extend Directus Files to include additional proprietary fields within [Settings > Data Model](#).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Supporting Directus
|
||||
# Backing Directus
|
||||
|
||||
> Directus is both _premium_ and _free_ — two things that don't often go together. It takes significant resources to maintain and advance the platform. If you'd like to help keep Directus active and open-source please consider supporting it through one of the methods below.
|
||||
|
||||
@@ -14,18 +14,14 @@ Monthly donations via [GitHub Sponsors](#) are the most reliable form of financi
|
||||
* [WoLfulus](https://github.com/WoLfulus) — DevOps
|
||||
* [Nitwel](https://github.com/Nitwel) — Developer
|
||||
|
||||
### Sponsored Work
|
||||
|
||||
[Commissioned Features](#) and [Expedited Fixes](#) are great ways to financially support the development of Directus, and improve the codebase for the community.
|
||||
|
||||
### Merch
|
||||
|
||||
Our merchandise is a great way to support Directus — you get some swag, and we get some financial support and advertising. Simply make an appropriate financial donation through [GitHub Sponsors](#), message us with sizing/shipping details, and we'll get it shipped!
|
||||
|
||||
### Commissioned Features
|
||||
|
||||
If you need a specific feature added to Directus faster than the normal development timeline, [reach out to us](#) for a quote. Our parent agency will often help subsidize the cost of developing new features if they pass our [80/20 Rule](#) and can be merged into the core codebase. Other custom/proprietary development will be built bespoke within our robust extension system.
|
||||
|
||||
### Expedited Fixes
|
||||
|
||||
We triage all reported bugs based on priority and how long the fix is estimated to take. If you need a specific issue resolved sooner, [reach out to us](#) for a quote.
|
||||
|
||||
## Other Contributions
|
||||
|
||||
Below are several ways anyone can help improve our ecosystem.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Troubleshooting
|
||||
# Technical Support
|
||||
|
||||
> Directus is offered completely free and open-source for anyone wishing to self-host the platform.
|
||||
> Directus is offered completely free and open-source for anyone wishing to self-host the platform. There are many resources, both free and paid, to help you get up-and-running smoothly.
|
||||
|
||||
## Community Support
|
||||
|
||||
@@ -12,6 +12,14 @@ Our [Discord](https://discord.gg/directus) community is another great way to get
|
||||
|
||||
Premium support is included with our Enterprise Cloud service. On-Demand Cloud customers and On-Premise users interested in learning more about our monthly retainer agreements should contact us at [support@directus.io](mailto:support@directus.io).
|
||||
|
||||
## Commissioned Features
|
||||
|
||||
If you need a specific feature added to Directus faster than the normal development timeline, [reach out to us](#) for a quote. Our parent agency will often help subsidize the cost of developing new features if they pass our [80/20 Rule](#) and can be merged into the core codebase. Other custom/proprietary development will be built bespoke within our robust extension system.
|
||||
|
||||
## Expedited Fixes
|
||||
|
||||
We triage all reported bugs based on priority and how long the fix is estimated to take. If you need a specific issue resolved sooner, [reach out to us](#) for a quote.
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### Does Directus handle deploying or migrating projects?
|
||||
@@ -24,10 +32,18 @@ Additionally, since Directus stores all of your data in pure SQL, you can use al
|
||||
|
||||
Not currently. Directus has been built specifically for wrapping _relational_ databases. While we could force Mongo to use tables, columns, and rows via Mongoose object modeling, that approach to non-structured doesn't make a lot of sense. We realize many users are interested in this feature, and will continue to explore its possibility.
|
||||
|
||||
### Why haven't you added X feature, or fixed Y issue yet?
|
||||
### Why haven't you added this feature, or fixed that issue yet?
|
||||
|
||||
Directus is primarily a free and open-source project, maintained by a small core team and community contributors who donate their time and resources.
|
||||
|
||||
Our platform is feature-rich, however we strictly adhere to our [80/20 Rule](#) to avoid a messy/bloated codebase. Directus is also quite stable, however new issues still arise, some of which may be triaged with a lower prioritization.
|
||||
|
||||
If you require more expeditious updates, you can contact us regarding [sponsoring expedited fixes](#) or [commissioning new features](#). You can also [submit a pull request](#) — after all, it is open-source!
|
||||
|
||||
### Can you give an ETA for this feature/fix?
|
||||
|
||||
If it is [sponsored work](#), then yes, delivery dates are part of our statement of work agreements. Otherwise, most likely not. This is open-source software, work is prioritized internally, and all timelines are subject to change.
|
||||
|
||||
### But this is an emergency, my very important project requires it now!
|
||||
|
||||
We understand, and are here to help. If you need something prioritized, you can reach out to us to discuss [premium support](#), [sponsoring expedited fixes](#) or [commissioning new features](#).
|
||||
@@ -22,22 +22,13 @@ Next, you will want to define your event. You can trigger your custom hook with
|
||||
|
||||
```
|
||||
<scope>.<action>(.<before>)
|
||||
// eg: items.create
|
||||
// eg: files.create
|
||||
// eg: server.start
|
||||
// eg: collections.*
|
||||
// eg: users.update.before
|
||||
```
|
||||
|
||||
While hooks for _items_ also require the collection to be defined:
|
||||
|
||||
```
|
||||
<scope>.<action>.<collection>(.<before>)
|
||||
// eg: items.create.articles
|
||||
// eg: items.update.customers
|
||||
// eg: items.update.*
|
||||
// eg: items.create.invoices.before
|
||||
```
|
||||
|
||||
### Scope
|
||||
|
||||
The scope determines the API endpoint that is triggered. The `*` wildcard can also be used to include all scopes.
|
||||
@@ -50,38 +41,34 @@ Currently all system tables are available as event scopes except for `directus_m
|
||||
|
||||
Defines the triggering operation within the specified context (see chart below). The `*` wildcard can also be used to include all actions available to the scope.
|
||||
|
||||
### Collection
|
||||
|
||||
Events in the "Items" scope also require the collection to be defined. The `*` wildcard can also be used to include all collections.
|
||||
|
||||
### Before
|
||||
|
||||
Many scopes (see chart below) support an optional `.before` suffix for running a _blocking_ hook prior to the event being fired. This allows you to check and/or modify the event's payload before it is processed.
|
||||
|
||||
* `items.create.<collection>` (Non Blocking)
|
||||
* `items.create.<collection>.before` (Blocking)
|
||||
* `items.create` (Non Blocking)
|
||||
* `items.create.before` (Blocking)
|
||||
|
||||
### Event Format Options
|
||||
|
||||
| Scope | Actions | Collection | Before |
|
||||
|---------------|-----------------------------------|------------|----------|
|
||||
| `items` | `create`, `update` and `delete` | Required | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | No | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | No | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | No | Optional |
|
||||
| `files` | `create`, `update` and `delete` | No | Optional |
|
||||
| `folders` | `create`, `update` and `delete` | No | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | No | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | No | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | No | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | No | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | No | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | No | Optional |
|
||||
| `users` | `create`, `update` and `delete` | No | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | No | Optional |
|
||||
| `server` | `start` and `error`† | No | No |
|
||||
| `auth` | `success`†, `fail`† and `refresh`† | No | No |
|
||||
| `request` | `get`†, `patch`† `post`† and `delete`† | No | No |
|
||||
| Scope | Actions | Before |
|
||||
|---------------|----------------------------------------|----------|
|
||||
| `items` | `create`, `update` and `delete` | Optional |
|
||||
| `activity` | `create`, `update` and `delete` | Optional |
|
||||
| `collections` | `create`, `update` and `delete` | Optional |
|
||||
| `fields` | `create`, `update` and `delete` | Optional |
|
||||
| `files` | `create`, `update` and `delete` | Optional |
|
||||
| `folders` | `create`, `update` and `delete` | Optional |
|
||||
| `permissions` | `create`, `update` and `delete` | Optional |
|
||||
| `presets` | `create`, `update` and `delete` | Optional |
|
||||
| `relations` | `create`, `update` and `delete` | Optional |
|
||||
| `revisions` | `create`, `update` and `delete` | Optional |
|
||||
| `roles` | `create`, `update` and `delete` | Optional |
|
||||
| `settings` | `create`, `update` and `delete` | Optional |
|
||||
| `users` | `create`, `update` and `delete` | Optional |
|
||||
| `webhooks` | `create`, `update` and `delete` | Optional |
|
||||
| `server` | `start` and `error`† | No |
|
||||
| `auth` | `success`†, `fail`† and `refresh`† | No |
|
||||
| `request` | `get`†, `patch`† `post`† and `delete`† | No |
|
||||
|
||||
† TBD
|
||||
|
||||
@@ -92,7 +79,7 @@ Each custom hook is registered to its event scope using a function with the foll
|
||||
```js
|
||||
module.exports = function registerHook() {
|
||||
return {
|
||||
'items.create.articles': function() {
|
||||
'items.create': function() {
|
||||
axios.post('http://example.com/webhook');
|
||||
}
|
||||
}
|
||||
@@ -107,21 +94,21 @@ The register function (eg: `module.exports = function registerHook()`) must retu
|
||||
|
||||
The `registerHook` function receives a context parameter with the following properties:
|
||||
|
||||
* `services` — All API interal services [Learn More](#)
|
||||
* `exceptions` — API exception objects that can be used for throwing "proper" errors [Learn More](#)
|
||||
* `database` — Knex instance that is connected to the current database [Learn More](#)
|
||||
* `env` — Parsed environment variables [Learn More](#)
|
||||
* `services` — All API interal services
|
||||
* `exceptions` — API exception objects that can be used for throwing "proper" errors
|
||||
* `database` — Knex instance that is connected to the current database
|
||||
* `env` — Parsed environment variables
|
||||
|
||||
### Event Handler Function
|
||||
|
||||
The event handler function (eg: `'items.create.articles': function()`) recieves a context parameter with the following properties:
|
||||
The event handler function (eg: `'items.create': function()`) recieves a context parameter with the following properties:
|
||||
|
||||
* `event` — Full event string [Learn More](#)
|
||||
* `accountability` — Information about the current user [Learn More](#)
|
||||
* `collection` — Collection that is being modified [Learn More](#)
|
||||
* `item` — Primary key(s) of the item(s) being modified [Learn More](#)
|
||||
* `action` — Action that is performed [Learn More](#)
|
||||
* `payload` — Payload of the request [Learn More](#)
|
||||
* `event` — Full event string
|
||||
* `accountability` — Information about the current user
|
||||
* `collection` — Collection that is being modified
|
||||
* `item` — Primary key(s) of the item(s) being modified
|
||||
* `action` — Action that is performed
|
||||
* `payload` — Payload of the request
|
||||
|
||||
## 5. Restart the API
|
||||
|
||||
@@ -147,7 +134,9 @@ module.exports = function registerHook({ services, exceptions }) {
|
||||
if (accountability.admin !== true) throw new ForbiddenException();
|
||||
},
|
||||
// Sync with external recipes service, cancel creation on failure
|
||||
'items.recipes.create.before': async function(input) {
|
||||
'items.create.before': async function(input, { collection }) {
|
||||
if (collection !== 'recipes') return input;
|
||||
|
||||
try {
|
||||
await axios.post('https://example.com/recipes', input);
|
||||
} catch (error) {
|
||||
|
||||
@@ -27,7 +27,9 @@ All project configuration is handled by the `.env` file within the `/api` direct
|
||||
|
||||
## Upgrading a Project
|
||||
|
||||
@TODO
|
||||
1. Backup your project
|
||||
2. Run `npm update`
|
||||
<!-- 3. Run `directus migrate:latest` to update the DB ——— @TODO finalize when CLI is finalized -->
|
||||
|
||||
## Backing-up a Project
|
||||
|
||||
|
||||
@@ -10,19 +10,19 @@
|
||||
4. Enabling **App Access** allows logging in to the App
|
||||
5. Enabling **Admin Access** gives full permission to project data and Settings
|
||||
|
||||
## Deleting a Role
|
||||
## Configuring a Role
|
||||
|
||||
1. Navigate to **Settings > Roles & Permissions > [Role Name]**
|
||||
2. Click the red **Delete Role** action button in the header
|
||||
3. Confirm this decision by clicking **Delete** in the dialog
|
||||
|
||||
:::warning Users in a Deleted Role
|
||||
If you delete a role that still has users in it, those users will be given a `NULL` role, which denies their App access and limits them to the [Public](#) permissions. They can then be reassigned to a new role by an admin.
|
||||
:::
|
||||
|
||||
:::warning Last Admin
|
||||
You must maintain at least one role/user with Admin Access so that you can still properly manage the project.
|
||||
:::
|
||||
* **Permissions** — Defines the role's access permissions, see [Configuring Role Permissions](#) and [Configuring System Permissions](#)
|
||||
* **Role Name** — This is the name of the role
|
||||
* **Role Icon** — The icon used throughout the App when referencing this role
|
||||
* **Description** — A helpful note that explains the role's purpose
|
||||
* **App Access** — Allows logging in to the App
|
||||
* **Admin Access** — Gives full permission to project data and Settings
|
||||
* **IP Access** — An allow-list of IP addresses from which the platform can be accessed, empty allows all
|
||||
* **Require 2FA** — Forces all users within this role to use two-factor authentication
|
||||
* **Users in Role** — A list of all users within this role
|
||||
* **Module Navigation** — Overrides the visible modules, see [Customizing the Module Navigation](#)
|
||||
* **Collection Navigation** — Overrides the collection module's navigation, see [Customizing the Collection Navigation](#)
|
||||
|
||||
### Customizing the Module Navigation
|
||||
|
||||
@@ -39,18 +39,28 @@ The options in the [Module Bar](#) can be overridden with custom options per rol
|
||||
If you are looking to replicate the default modules, paste the following configuration into the Module Navigation field using the [Raw Value](#) field label option.
|
||||
|
||||
```json
|
||||
Collections
|
||||
box
|
||||
/collections
|
||||
User Directory
|
||||
people_alt
|
||||
/users
|
||||
File Library
|
||||
folder
|
||||
/files
|
||||
Documentation
|
||||
info
|
||||
/docs
|
||||
[
|
||||
{
|
||||
"icon": "box",
|
||||
"name": "Collections",
|
||||
"link": "/collections"
|
||||
},
|
||||
{
|
||||
"icon": "people_alt",
|
||||
"name": "User Directory",
|
||||
"link": "/users"
|
||||
},
|
||||
{
|
||||
"icon": "folder",
|
||||
"name": "File Library",
|
||||
"link": "/files"
|
||||
},
|
||||
{
|
||||
"icon": "info",
|
||||
"name": "Documentation",
|
||||
"link": "/docs"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
:::warning Settings Module
|
||||
@@ -77,13 +87,122 @@ The collections in the [Navigation Bar](#) can be overridden with custom options
|
||||
7. Choose a **Collection** from the dropdown
|
||||
8. Use the drag handles to **drag-and-drop** the groups/collections into the desired order
|
||||
|
||||
## Configuring Permissions
|
||||
## Configuring Role Permissions
|
||||
|
||||
Directus possesses an extremely granular, yet easy to configure, permissions system. When creating a new role, permissions are disabled for all project collections by default — allowing you to give explicit access to only what is required. Individual permissions are applied to the role, and each is scoped to a specific collection and CRUD action (create, read, update, delete).
|
||||
|
||||
:::warning Saves Automatically
|
||||
Every change made to the permissions of a role is saved automatically and instantly.
|
||||
:::
|
||||
|
||||
:::warning Admin Roles
|
||||
If a role is set to **Admin Access** then it is granted complete access to the platoform, and therefore the permission configuration field is disabled.
|
||||
:::
|
||||
|
||||
1. Navigate to **Settings > Roles & Permissions > [Role Name]**
|
||||
2. Scroll to the **Permissions** section
|
||||
3. **Click the icon** for the collection (row) and action (column) you want to set
|
||||
4. Choose the desired permission level: **All Access**, **No Access**, or **Use Custom**
|
||||
|
||||
If you selected _All Access_ or _No Access_ then setup is complete. If you chose to customize the permissions, then a modal will open with additional configuration options. Continue with the appropriate guide below based on the _action_ of the permission.
|
||||
|
||||
#### Create (Custom)
|
||||
|
||||
5. **Field Permissions** control which fields accept a value on create. Fields are individually toggled.
|
||||
6. **Field Validation** define the rules for field values on create, as defined by the [Filter Rules](#) entered.
|
||||
7. **Field Presets** control the field defaults when creating an item, as defined by the [Item Object](#) entered.
|
||||
|
||||
#### Read (Custom)
|
||||
|
||||
5. **Item Permissions** control which items can be read, as defined by the [Filter Rules](#) entered.
|
||||
6. **Field Permissions** control which fields can be read. Fields are individually toggled.
|
||||
|
||||
#### Update (Custom)
|
||||
|
||||
5. **Item Permissions** control which items can be updated, as defined by the [Filter Rules](#) entered.
|
||||
6. **Field Permissions** control which fields can be updated. Fields are individually toggled.
|
||||
7. **Field Validation** define the rules for field values on update, as defined by the [Filter Rules](#) entered.
|
||||
8. **Field Presets** control the field defaults when updating an item, as defined by the [Item Object](#) entered.
|
||||
|
||||
#### Delete (Custom)
|
||||
|
||||
5. **Item Permissions** control which items can be deleted, as defined by the [Filter Rules](#) entered.
|
||||
|
||||
## Configuring System Permissions
|
||||
|
||||
In addition to setting permissions for your project's collections, you can also tailor the permissions for system collections. It is important to note that when [App Access](#) is enabled for a role, Directus will automatically add permission for the neccesary system collections. To edit system permissions, simply click the "System Collections" toggle, and then edit permissions using the same steps as with project collections.
|
||||
|
||||
::: Resetting System Permissions
|
||||
To reset the role's system permissions for proper App access, expand the system collections and then click "Reset System Permissions" at the bottom of the listing.
|
||||
:::
|
||||
|
||||
## Deleting a Role
|
||||
|
||||
1. Navigate to **Settings > Roles & Permissions > [Role Name]**
|
||||
2. Click the red **Delete Role** action button in the header
|
||||
3. Confirm this decision by clicking **Delete** in the dialog
|
||||
|
||||
:::warning Users in a Deleted Role
|
||||
If you delete a role that still has users in it, those users will be given a `NULL` role, which denies their App access and limits them to the [Public](#) permissions. They can then be reassigned to a new role by an admin.
|
||||
:::
|
||||
|
||||
:::warning Last Admin
|
||||
You must maintain at least one role/user with Admin Access so that you can still properly manage the project.
|
||||
:::
|
||||
|
||||
:::warning Public Role
|
||||
You can not delete the Public role, as it is part of the core platform. To disable it completely, simply turn off all Public access permissions.
|
||||
:::
|
||||
|
||||
## Creating a User
|
||||
|
||||
* Draft
|
||||
* Invited
|
||||
* Active
|
||||
* Suspended
|
||||
* Archived
|
||||
1. Navigate to the **User Library**
|
||||
2. Click the **Create User** action button in the header
|
||||
3. Enter an **Email Address**
|
||||
4. Optional: Complete the **other user form fields** (see [Configuring a User](#))
|
||||
|
||||
## Inviting a User
|
||||
|
||||
1. Navigate to **Settings > Roles & Permissions > [Role Name]**
|
||||
2. Scroll to the **Users in Role** field
|
||||
3. Click the **Invite Users** button
|
||||
4. Enter **one or more email addresses**, separated by commas, in the modal
|
||||
5. Click **Invite**
|
||||
|
||||
## Configuring a User
|
||||
|
||||
1. Navigate to the **User Library**
|
||||
2. Click on the user you wish to manage
|
||||
3. Complete any of the [User Fields](/concepts/app-overview.md#user-detail)
|
||||
|
||||
::: User Preferences
|
||||
This section of the User Detail is only visible/editable by the current user, and admins.
|
||||
:::
|
||||
|
||||
### Status
|
||||
|
||||
* **Draft** — An incomplete user; no App/API access
|
||||
* **Invited** — Has a pending invite to the project; no App/API access until accepted
|
||||
* **Active** — The only status that has proper access to the App and API
|
||||
* **Suspended** — A user that has been temporarily disabled; no App/API access
|
||||
* **Archived** — A soft-deleted user; no App/API access
|
||||
|
||||
:::warning Admin Only
|
||||
Only admins can adjust this field's value.
|
||||
:::
|
||||
|
||||
### Role
|
||||
|
||||
Setting the user's role determines their access, permissions, and App presentation. You can adjust a user's role from the User Detail page, or from the _Users in Role_ field within **Settings > Roles & Permissions > [Role Name]**.
|
||||
|
||||
:::warning Admin Only
|
||||
Only admins can adjust this field's value.
|
||||
:::
|
||||
|
||||
### Token
|
||||
|
||||
A user's token is an alternate way to [authenticate into the API](#) using a static string. When NULL, the token is disabled. When enabled, ensure that a secure string is used.
|
||||
|
||||
:::warning Admin Only
|
||||
Only admins can adjust this field's value.
|
||||
:::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@directus/docs",
|
||||
"private": false,
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,25 +1,250 @@
|
||||
# Project Environment Variables
|
||||
# Environment Variables
|
||||
|
||||
> TK
|
||||
> Each Directus project supports a number of environment variables for configuration. These variables are added to the `/api/.env` file, with an example file at `/api/example.env` for easier boilerplate setup.
|
||||
|
||||
## General & Database
|
||||
|
||||
Set by installer, probably doesn't need to be changed
|
||||
## General
|
||||
|
||||
## Rate Limiter
|
||||
### `PORT`
|
||||
|
||||
### Memory
|
||||
What port to run the API under.<br>**Default: `8055`**
|
||||
|
||||
### Redis
|
||||
### `PUBLIC_URL`
|
||||
|
||||
## Caching
|
||||
URL where your API can be reached on the web.<br>**Default: `/`**
|
||||
|
||||
### `LOG_LEVEL`
|
||||
|
||||
What level of detail to log. One of `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`.<br>**Default: `info`**
|
||||
|
||||
### `LOG_STYLE`
|
||||
|
||||
Render the logs human readable (pretty) or as JSON. One of `pretty`, `raw`.<br>**Default: `pretty`**
|
||||
|
||||
|
||||
## Database
|
||||
|
||||
### `DB_CLIENT`
|
||||
|
||||
What database client to use. One of `pg`, `mysql`, `mysql2`, `oracledb`, `mssql`, or `sqlite3`. For all database clients except SQLite, you will also need to configure the following variables:
|
||||
|
||||
### `DB_HOST`
|
||||
|
||||
Database host. Required when using `pg`, `mysql`, `mysql2`, `oracledb`, or `mssql`.
|
||||
|
||||
### `DB_PORT`
|
||||
|
||||
Database port. Required when using `pg`, `mysql`, `mysql2`, `oracledb`, or `mssql`.
|
||||
|
||||
### `DB_DATABASE`
|
||||
|
||||
Database name. Required when using `pg`, `mysql`, `mysql2`, `oracledb`, or `mssql`.
|
||||
|
||||
### `DB_USER`
|
||||
|
||||
Database user. Required when using `pg`, `mysql`, `mysql2`, `oracledb`, or `mssql`.
|
||||
|
||||
### `DB_PASSWORD`
|
||||
|
||||
Database user's password. Required when using `pg`, `mysql`, `mysql2`, `oracledb`, or `mssql`.
|
||||
|
||||
### `DB_FILENAME` (SQLite Only)
|
||||
|
||||
Where to read/write the SQLite database. Required when using `sqlite3`.
|
||||
|
||||
::: Additional Database Variables
|
||||
All `DB_*` environment variables are passed to the `connection` configuration of a [`Knex` instance](http://knexjs.org).
|
||||
Based on your project's needs, you can extend the `DB_*` environment variables with any config you need to pass to the database instance.
|
||||
:::
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
### `KEY`
|
||||
|
||||
Unique identifier for the project.
|
||||
|
||||
### `SECRET`
|
||||
|
||||
Secret string for the project. Generated on installation.
|
||||
|
||||
### `ACCESS_TOKEN_TTL`
|
||||
|
||||
The duration that the access token is valid.<br>**Default: `15m`**
|
||||
|
||||
### `REFRESH_TOKEN_TTL`
|
||||
|
||||
The duration that the refresh token is valid, and also how long users stay logged-in to the App.<br>**Default: `7d`**
|
||||
|
||||
### `REFRESH_TOKEN_COOKIE_SECURE`
|
||||
|
||||
Whether or not to use a secure cookie for the refresh token in cookie mode.<br>**Default: `false`**
|
||||
|
||||
### `REFRESH_TOKEN_COOKIE_SAME_SITE`
|
||||
|
||||
Value for `sameSite` in the refresh token cookie when in cookie mode.<br>**Default: `lax`**
|
||||
|
||||
## File Storage
|
||||
|
||||
## CORS
|
||||
|
||||
## SSO
|
||||
### `CORS_ENABLED`
|
||||
|
||||
## Extensions path
|
||||
Whether or not to enable the CORS headers.<br>**Default: `true`**
|
||||
|
||||
## Email Server Setup
|
||||
### `CORS_METHODS`
|
||||
|
||||
Value for the `Access-Control-Allow-Methods` header.<br>**Default: `GET,POST,PATCH,DELETE`**
|
||||
|
||||
### `CORS_ALLOWED_HEADERS`
|
||||
|
||||
Value for the `Access-Control-Allow-Headers` header.<br>**Default: `Content-Type,Authorization`**
|
||||
|
||||
### `CORS_EXPOSED_HEADERS`
|
||||
|
||||
Value for the `Access-Control-Expose-Headers` header.<br>**Default: `Content-Range`**
|
||||
|
||||
### `CORS_CREDENTIALS`
|
||||
|
||||
Whether or not to send the `Access-Control-Allow-Credentials` header.<br>**Default: `true`**
|
||||
|
||||
### `CORS_MAX_AGE`
|
||||
|
||||
Value for the `Access-Control-Max-Age` header.<br>**Default: `18000`**
|
||||
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### `RATE_LIMITER_ENABLED`
|
||||
|
||||
Whether or not to enable rate limiting on the API.<br>**Default: `false`**
|
||||
|
||||
### `RATE_LIMITER_POINTS`
|
||||
|
||||
The amount of allowed hits per duration.<br>**Default: `50`**
|
||||
|
||||
### `RATE_LIMITER_DURATION`
|
||||
|
||||
The time window in seconds in which the points are counted.<br>**Default: `1`**
|
||||
|
||||
### `RATE_LIMITER_STORE`
|
||||
|
||||
Where to store the rate limiter counts. Either `memory`, `redis`, or `memcache`. Based on the rate limiter used, you must also provide the following configurations.<br>**Default: `memory`**
|
||||
|
||||
* **Memory**
|
||||
* No additional configuration required
|
||||
* **Redis**
|
||||
* **`RATE_LIMITER_REDIS`** — Redis connection string
|
||||
* eg: `redis://:authpassword@127.0.0.1:6380/4`
|
||||
* Alternatively, you can enter individual connection parameters:
|
||||
* **`RATE_LIMITER_REDIS_HOST`**
|
||||
* **`RATE_LIMITER_REDIS_PORT`**
|
||||
* **`RATE_LIMITER_REDIS_PASSWORD`**
|
||||
* **`RATE_LIMITER_REDIS_DB`**
|
||||
* **Memcache**
|
||||
* **`RATE_LIMITER_MEMCACHE`** — Location of your memcache instance
|
||||
|
||||
::: Additional Rate Limiter Variables
|
||||
All `RATE_LIMITER_*` variables are passed directly to a `rate-limiter-flexible` instance. Depending on your
|
||||
project's needs, you can extend the above environment variables to configure any of [the `rate-limiter-flexible` options](https://github.com/animir/node-rate-limiter-flexible/wiki/Options).
|
||||
:::
|
||||
|
||||
|
||||
## Cache
|
||||
|
||||
### `CACHE_ENABLED`
|
||||
|
||||
Whether or not caching is enabled.<br>**Default: `false`**
|
||||
|
||||
### `CACHE_TTL`
|
||||
|
||||
How long the cache is persisted.<br>**Default: `30m`**
|
||||
|
||||
:::warning Forced Flush
|
||||
Regardless of TTL, the cache is always flushed for every create, update, and delete action.
|
||||
:::
|
||||
|
||||
### `CACHE_NAMESPACE`
|
||||
|
||||
How to scope the cache data.<br>**Default: `directus-cache`**
|
||||
|
||||
### `CACHE_STORE`
|
||||
|
||||
Where to store the cache data. Either `memory`, `redis`, or `memcache`. Based on the cache used, you must also provide the following configurations.<br>**Default: `memory`**
|
||||
|
||||
* **Memory**
|
||||
* No additional configuration required
|
||||
* **Redis**
|
||||
* **`CACHE_REDIS`** — Redis connection string
|
||||
* eg: `redis://:authpassword@127.0.0.1:6380/4`
|
||||
* Alternatively, you can enter individual connection parameters:
|
||||
* **`CACHE_REDIS_HOST`**
|
||||
* **`CACHE_REDIS_PORT`**
|
||||
* **`CACHE_REDIS_PASSWORD`**
|
||||
* **`CACHE_REDIS_DB`**
|
||||
* **Memcache**
|
||||
* **`CACHE_MEMCACHE`** — Location of your memcache instance
|
||||
|
||||
|
||||
## File Storage
|
||||
|
||||
### `STORAGE_LOCATIONS`
|
||||
|
||||
A CSV of storage locations (eg: `local,digitalocean,amazon`) to use. You can use any names you'd like for these keys, but each must have a matching `<LOCATION>` configuration.<br>**Default: `local`**
|
||||
|
||||
For each of the storage locations listed, you must provide the following configuration:
|
||||
|
||||
* **`STORAGE_<LOCATION>_PUBLIC_URL`** — Location on the internet where the files are accessible
|
||||
* **`STORAGE_<LOCATION>_DRIVER`** — Which driver to use, either `local`, `s3`, or `gcl`
|
||||
|
||||
Based on your configured driver, you must also provide the following configurations.
|
||||
|
||||
* **Local**
|
||||
* `STORAGE_<LOCATION>_ROOT` — Where to store the files on disk
|
||||
* **S3**
|
||||
* **`STORAGE_<LOCATION>_KEY`** — User key
|
||||
* **`STORAGE_<LOCATION>_SECRET`** — User secret
|
||||
* **`STORAGE_<LOCATION>_ENDPOINT`** — S3 Endpoint
|
||||
* **`STORAGE_<LOCATION>_BUCKET`** — S3 Bucket
|
||||
* **`STORAGE_<LOCATION>_REGION`** — S3 Region
|
||||
* **Google Cloud**
|
||||
* **`STORAGE_<LOCATION>_KEY_FILENAME`** — Path to key file on disk
|
||||
* **`STORAGE_<LOCATION>_BUCKET`** — Google Cloud Storage bucket
|
||||
|
||||
|
||||
## oAuth
|
||||
|
||||
### `OAUTH_PROVIDERS`
|
||||
|
||||
CSV of oAuth providers you want to use. For each of the oAuth providers you list, you must also provide the following configurations.
|
||||
|
||||
* **`OAUTH_<PROVIDER>_KEY`** — oAuth key for the external service
|
||||
* **`OAUTH_<PROVIDER>_SECRET`** — oAuth secret for the external service.
|
||||
|
||||
|
||||
## Extensions
|
||||
|
||||
### `EXTENSIONS_PATH`
|
||||
|
||||
Path to your local extensions folder.<br>**Default: `./extensions`**
|
||||
|
||||
|
||||
## Email
|
||||
|
||||
### `EMAIL_FROM`
|
||||
|
||||
Email address from which emails are sent.<br>**Default: `no-reply@directus.io`**
|
||||
|
||||
### `EMAIL_TRANSPORT`
|
||||
|
||||
What to use to send emails. One of `sendmail`, `smtp`. Based on the transport used, you must also provide the following configurations.<br>**Default: `sendmail`**
|
||||
|
||||
* **Sendmail** (`sendgrid`)
|
||||
* **`EMAIL_SENDMAIL_NEW_LINE`** — What new line style to use in sendmail. **Default: `unix`**
|
||||
* **`EMAIL_SENDMAIL_PATH`** — Path to your sendmail executable. **Default: `/usr/sbin/sendmail`**
|
||||
* **SMTP** (`smtp`)
|
||||
* **`EMAIL_SMTP_HOST`** — SMTP Host
|
||||
* **`EMAIL_SMTP_PORT`** — SMTP Port
|
||||
* **`EMAIL_SMTP_USER`** — SMTP User
|
||||
* **`EMAIL_SMTP_PASSWORD`** — SMTP Password
|
||||
* **`EMAIL_SMTP_POOL`** — Use SMTP pooling
|
||||
* **`EMAIL_SMTP_SECURE`** — Enable TLS
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Error Codes
|
||||
|
||||
> TK
|
||||
| Error Code | Description |
|
||||
|-----------------------|------------------------------------------------|
|
||||
| `FAILED_VALIDATION` | Validation for this particular item failed |
|
||||
| `FORBIDDEN` | You are not allowed to do the current action |
|
||||
| `INVALID_CREDENTIALS` | Username / password or access token is wrong |
|
||||
| `INVALID_OTP` | Wrong OTP was provided |
|
||||
| `INVALID_PAYLOAD` | Provided payload is invalid |
|
||||
| `INVALID_QUERY` | The requested query parameters can not be used |
|
||||
| `REQUESTS_EXCEEDED` | Hit rate limit; Too many requests |
|
||||
| `ROUTE_NOT_FOUND` | Endpoint does not exist |
|
||||
| `SERVICE_UNAVAILABLE` | Could not use external service |
|
||||
|
||||
:::warning Security
|
||||
To prevent leaking which items exist, all actions for non-existing items will return a `FORBIDDEN` error.
|
||||
:::
|
||||
|
||||
94
docs/reference/filter-rules.md
Normal file
94
docs/reference/filter-rules.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Filter Rules
|
||||
|
||||
> Permissions, validation, and the API's `filter` parameter all rely on a specific JSON structure to define their rules. This page describes the syntax for creating flat, relational, or complex filter rules.
|
||||
|
||||
## Syntax
|
||||
|
||||
* **Field** — Any valid root field, [relational field](#), or [logical operator](#)
|
||||
* **Operator** — Any valid [API operator](#) prefaced with an underscore
|
||||
* **Value** — Any valid static value, or [dynamic variable](#)
|
||||
|
||||
```
|
||||
{
|
||||
<field>: {
|
||||
<operator>: <value>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"title": {
|
||||
"_contains": "Directus"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Relational
|
||||
|
||||
You can target related values by nesting field names. For example, if you have a relational [Many-to-One](#)
|
||||
`author` field, you can set a rule for the `author.name` field using the following syntax.
|
||||
|
||||
```json
|
||||
{
|
||||
"author": {
|
||||
"name": {
|
||||
"_eq": "Rijk van Zanten"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logical Operators
|
||||
|
||||
You can nest or group multiple rules using the `_and` or `_or` logical operators. Each operator holds an array of rules, allowing for more complex filtering.
|
||||
|
||||
```json
|
||||
{
|
||||
"_or": [
|
||||
{
|
||||
"_and": [
|
||||
{
|
||||
"owner": {
|
||||
"_eq": "$CURRENT_USER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": {
|
||||
"_in": [
|
||||
"published",
|
||||
"draft"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_and": [
|
||||
{
|
||||
"owner": {
|
||||
"_neq": "$CURRENT_USER"
|
||||
}
|
||||
},
|
||||
{
|
||||
"status": {
|
||||
"_in": [
|
||||
"published"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Variables
|
||||
|
||||
In addition to static values, you can also filter against _dynamic_ values using the following variables.
|
||||
|
||||
* `$CURRENT_USER` — The primary key of the currently authenticated user
|
||||
* `$CURRENT_ROLE` — The primary key of the role for the currently authenticated user
|
||||
* `$NOW` — The current timestamp
|
||||
43
docs/reference/item-objects.md
Normal file
43
docs/reference/item-objects.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Item Objects
|
||||
|
||||
> TK
|
||||
|
||||
## Syntax
|
||||
|
||||
* **Field** — Any valid root field or [relational field](#)
|
||||
* **Value** — Any valid static value, or [dynamic variable](#)
|
||||
|
||||
```
|
||||
{
|
||||
<operator>: <value>
|
||||
}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Directus"
|
||||
}
|
||||
```
|
||||
|
||||
## Relational
|
||||
|
||||
You can set related values by nesting field names. For example, if you have a relational [Many-to-One](#)
|
||||
`author` field, you can set a rule for the `author.name` field using the following syntax.
|
||||
|
||||
```json
|
||||
{
|
||||
"author": {
|
||||
"name": "Rijk van Zanten"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Variables
|
||||
|
||||
In addition to static values, you can also set _dynamic_ values using the following variables.
|
||||
|
||||
* `$CURRENT_USER` — The primary key of the currently authenticated user
|
||||
* `$CURRENT_ROLE` — The primary key of the role for the currently authenticated user
|
||||
* `$NOW` — The current timestamp
|
||||
@@ -5,7 +5,7 @@
|
||||
"docs",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"command": {
|
||||
"bootstrap": {
|
||||
"npmClientArgs": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "9.0.0-beta.8",
|
||||
"version": "9.0.0-beta.9",
|
||||
"description": "Specification of the Directus Api",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"dist",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"index.js"
|
||||
"index.js"
|
||||
],
|
||||
"gitHead": "4476da28dbbc2824e680137aa28b2b91b5afabec"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user