Merge branch 'main' of https://github.com/directus/directus into main

This commit is contained in:
Ben Haynes
2021-01-25 10:52:51 -05:00
24 changed files with 236 additions and 63 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"license": "GPL-3.0-only",
"homepage": "https://github.com/directus/directus#readme",
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",

View File

@@ -7,7 +7,7 @@ import formatTitle from '@directus/format-title';
import env from '../env';
import axios from 'axios';
import Joi from 'joi';
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
import { InvalidPayloadException, ForbiddenException, FailedValidationException } from '../exceptions';
import url from 'url';
import path from 'path';
import useCollection from '../middleware/use-collection';
@@ -218,6 +218,60 @@ router.get(
respond
);
router.patch(
'/',
asyncHandler(async (req, res, next) => {
const service = new FilesService({
accountability: req.accountability,
schema: req.schema,
});
if (Array.isArray(req.body)) {
const primaryKeys = await service.update(req.body);
try {
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
res.locals.payload = { data: result || null };
} catch (error) {
if (error instanceof ForbiddenException) {
return next();
}
throw error;
}
return next();
}
const updateSchema = Joi.object({
keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())).required(),
data: Joi.object().required().unknown(),
});
const { error } = updateSchema.validate(req.body);
if (error) {
throw new FailedValidationException(error.details[0]);
}
const primaryKeys = await service.update(req.body.data, req.body.keys);
try {
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
res.locals.payload = { data: result || null };
} catch (error) {
if (error instanceof ForbiddenException) {
return next();
}
throw error;
}
return next();
}),
respond
);
router.patch(
'/:pk',
multipartHandler,

View File

@@ -151,7 +151,7 @@ export class ServerService {
checks[`${client}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3);
if (
checks[`${client}:responseTime`][0].observedValue! > 50 &&
checks[`${client}:responseTime`][0].observedValue! > 150 &&
checks[`${client}:responseTime`][0].status !== 'error'
) {
checks[`${client}:responseTime`][0].status = 'warn';
@@ -205,7 +205,7 @@ export class ServerService {
const endTime = performance.now();
checks['cache:responseTime'][0].observedValue = +(endTime - startTime).toFixed(3);
if (checks['cache:responseTime'][0].observedValue > 50 && checks['cache:responseTime'][0].status !== 'error') {
if (checks['cache:responseTime'][0].observedValue > 150 && checks['cache:responseTime'][0].status !== 'error') {
checks['cache:responseTime'][0].status = 'warn';
}
}
@@ -242,7 +242,7 @@ export class ServerService {
checks['rateLimiter:responseTime'][0].observedValue = +(endTime - startTime).toFixed(3);
if (
checks['rateLimiter:responseTime'][0].observedValue > 50 &&
checks['rateLimiter:responseTime'][0].observedValue > 150 &&
checks['rateLimiter:responseTime'][0].status !== 'error'
) {
checks['rateLimiter:responseTime'][0].status = 'warn';
@@ -281,7 +281,7 @@ export class ServerService {
checks[`storage:${location}:responseTime`][0].observedValue = +(endTime - startTime).toFixed(3);
if (
checks[`storage:${location}:responseTime`][0].observedValue! > 500 &&
checks[`storage:${location}:responseTime`][0].observedValue! > 750 &&
checks[`storage:${location}:responseTime`][0].status !== 'error'
) {
checks[`storage:${location}:responseTime`][0].status = 'warn';

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/app",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"private": false,
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
"author": "Rijk van Zanten <rijk@rngr.org>",

View File

@@ -5,7 +5,7 @@ import Vue from 'vue';
import { isEqual } from 'lodash';
import { Filter } from '@/types/';
import filtersToQuery from '@/utils/filters-to-query';
import { orderBy } from 'lodash';
import { orderBy, throttle } from 'lodash';
import moveInArray from '@/utils/move-in-array';
type Query = {
@@ -91,7 +91,7 @@ export function useItems(collection: Ref<string>, query: Query) {
}
});
watch([filters, limit, searchQuery], async (after, before) => {
watch([filters, limit], async (after, before) => {
if (!before || isEqual(after, before)) {
return;
}
@@ -102,6 +102,24 @@ export function useItems(collection: Ref<string>, query: Query) {
}
});
watch(
searchQuery,
throttle(
async (after, before) => {
if (!before || isEqual(after, before)) {
return;
}
page.value = 1;
await Vue.nextTick();
if (loading.value === false) {
getItems();
}
},
500,
{ trailing: true }
)
);
return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems };
async function getItems() {

View File

@@ -27,7 +27,6 @@ export function usePreset(collection: Ref<string>, bookmark: Ref<number | null>
const savePreset = async (preset?: Partial<Preset>) => {
busy.value = true;
const updatedValues = await presetsStore.savePreset(preset ? preset : localPreset.value);
initLocalPreset();
localPreset.value.id = updatedValues.id;
busy.value = false;
return updatedValues;
@@ -35,7 +34,6 @@ export function usePreset(collection: Ref<string>, bookmark: Ref<number | null>
const saveLocal = () => {
presetsStore.saveLocal(localPreset.value);
initLocalPreset();
};
const clearLocalSave = async () => {

View File

@@ -28,7 +28,10 @@
</div>
</template>
<template #append>
<v-icon class="deselect" name="close" v-if="file" @click.stop="$emit('input', null)" />
<template v-if="file">
<v-icon name="open_in_new" class="edit" v-tooltip="$t('edit')" @click.stop="editDrawerActive = true" />
<v-icon class="deselect" name="close" @click.stop="$emit('input', null)" v-tooltip="$t('deselect')" />
</template>
<v-icon v-else name="attach_file" />
</template>
</v-input>
@@ -67,6 +70,15 @@
</v-list>
</v-menu>
<drawer-item
v-if="!disabled && file"
:active.sync="editDrawerActive"
collection="directus_files"
:primary-key="file.id"
:edits="edits"
@input="stageEdits"
/>
<v-dialog :active="activeDialog === 'upload'" @esc="activeDialog = null" @toggle="activeDialog = null">
<v-card>
<v-card-title>{{ $t('upload_from_device') }}</v-card-title>
@@ -118,18 +130,19 @@ import readableMimeType from '@/utils/readable-mime-type';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { addTokenToURL } from '@/api';
import DrawerItem from '../../views/private/components/drawer-item';
type FileInfo = {
id: number;
id: string;
title: string;
type: string;
};
export default defineComponent({
components: { DrawerCollection },
components: { DrawerCollection, DrawerItem },
props: {
value: {
type: String,
type: [String, Object],
default: null,
},
disabled: {
@@ -148,7 +161,10 @@ export default defineComponent({
return readableMimeType(file.value.type, true);
});
const assetURL = computed(() => getRootPath() + `assets/${props.value}`);
const assetURL = computed(() => {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
return getRootPath() + `assets/${id}`;
});
const imageThumbnail = computed(() => {
if (file.value === null || props.value === null) return null;
@@ -158,8 +174,11 @@ export default defineComponent({
return addTokenToURL(url);
});
const { edits, stageEdits } = useEdits();
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
const editDrawerActive = ref(false);
return {
activeDialog,
setSelection,
@@ -173,6 +192,9 @@ export default defineComponent({
importFromURL,
isValidURL,
assetURL,
editDrawerActive,
edits,
stageEdits,
};
function useFile() {
@@ -191,13 +213,22 @@ export default defineComponent({
loading.value = true;
try {
const response = await api.get(`/files/${props.value}`, {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
const response = await api.get(`/files/${id}`, {
params: {
fields: ['title', 'type', 'filename_download'],
fields: ['id', 'title', 'type', 'filename_download'],
},
});
file.value = response.data.data;
if (props.value !== null && typeof props.value === 'object') {
file.value = {
...response.data.data,
...props.value,
};
} else {
file.value = response.data.data;
}
} catch (err) {
unexpectedError(err);
} finally {
@@ -255,6 +286,29 @@ export default defineComponent({
}
}
}
function useEdits() {
const edits = computed(() => {
// If the current value isn't a primitive, it means we've already staged some changes
// This ensures we continue on those changes instead of starting over
if (props.value && typeof props.value === 'object') {
return props.value;
}
return {};
});
return { edits, stageEdits };
function stageEdits(newEdits: Record<string, any>) {
if (!file.value) return;
emit('input', {
id: file.value.id,
...newEdits,
});
}
}
},
});
</script>
@@ -304,4 +358,12 @@ export default defineComponent({
.deselect:hover {
--v-icon-color: var(--danger);
}
.edit {
margin-right: 4px;
&:hover {
--v-icon-color: var(--foreground-normal);
}
}
</style>

View File

@@ -18,8 +18,8 @@
<v-button icon rounded :href="downloadSrc" :download="image.filename_download" v-tooltip="$t('download')">
<v-icon name="get_app" />
</v-button>
<v-button icon rounded @click="editorActive = true" v-tooltip="$t('edit')">
<v-icon name="crop_rotate" />
<v-button icon rounded @click="editDrawerActive = true" v-tooltip="$t('edit')">
<v-icon name="open_in_new" />
</v-button>
<v-button icon rounded @click="deselect" v-tooltip="$t('deselect')">
<v-icon name="close" />
@@ -31,12 +31,15 @@
<div class="meta">{{ meta }}</div>
</div>
<image-editor
v-if="image && image.type.startsWith('image')"
:id="image.id"
@refresh="changeCacheBuster"
v-model="editorActive"
<drawer-item
v-if="!disabled && image"
:active.sync="editDrawerActive"
collection="directus_files"
:primary-key="image.id"
:edits="edits"
@input="stageEdits"
/>
<file-lightbox v-model="lightboxActive" :id="image.id" />
</div>
<v-upload v-else @input="setImage" from-library from-url />
@@ -54,6 +57,7 @@ import { nanoid } from 'nanoid';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { addTokenToURL } from '@/api';
import DrawerItem from '../../views/private/components/drawer-item';
type Image = {
id: string; // uuid
@@ -65,10 +69,10 @@ type Image = {
};
export default defineComponent({
components: { FileLightbox, ImageEditor },
components: { FileLightbox, ImageEditor, DrawerItem },
props: {
value: {
type: String,
type: [String, Object],
default: null,
},
disabled: {
@@ -80,7 +84,7 @@ export default defineComponent({
const loading = ref(false);
const image = ref<Image | null>(null);
const lightboxActive = ref(false);
const editorActive = ref(false);
const editDrawerActive = ref(false);
const cacheBuster = ref(nanoid());
@@ -114,43 +118,56 @@ export default defineComponent({
watch(
() => props.value,
(newID, oldID) => {
if (newID === oldID) return;
(newValue, oldValue) => {
if (newValue === oldValue) return;
if (newID) {
if (newValue) {
fetchImage();
}
if (oldID && newID === null) {
if (oldValue && newValue === null) {
deselect();
}
}
);
const { edits, stageEdits } = useEdits();
return {
loading,
image,
src,
meta,
lightboxActive,
editorActive,
editDrawerActive,
changeCacheBuster,
setImage,
deselect,
downloadSrc,
edits,
stageEdits,
};
async function fetchImage() {
loading.value = true;
try {
const response = await api.get(`/files/${props.value}`, {
const id = typeof props.value === 'string' ? props.value : (props.value as Record<string, any>)?.id;
const response = await api.get(`/files/${id}`, {
params: {
fields: ['id', 'title', 'width', 'height', 'filesize', 'type', 'filename_download'],
},
});
image.value = response.data.data;
if (props.value !== null && typeof props.value === 'object') {
image.value = {
...response.data.data,
...props.value,
};
} else {
image.value = response.data.data;
}
} catch (err) {
unexpectedError(err);
} finally {
@@ -173,7 +190,30 @@ export default defineComponent({
loading.value = false;
image.value = null;
lightboxActive.value = false;
editorActive.value = false;
editDrawerActive.value = false;
}
function useEdits() {
const edits = computed(() => {
// If the current value isn't a primitive, it means we've already staged some changes
// This ensures we continue on those changes instead of starting over
if (props.value && typeof props.value === 'object') {
return props.value;
}
return {};
});
return { edits, stageEdits };
function stageEdits(newEdits: Record<string, any>) {
if (!image.value) return;
emit('input', {
id: image.value.id,
...newEdits,
});
}
}
},
});

View File

@@ -27,6 +27,7 @@ import { defineComponent, PropType, computed } from '@vue/composition-api';
import router from '@/router';
import { getRootPath } from '@/utils/get-root-path';
import { addTokenToURL } from '@/api';
import { readableMimeType } from '../../../utils/readable-mime-type';
type File = {
[key: string]: any;
@@ -82,7 +83,7 @@ export default defineComponent({
const type = computed(() => {
if (!props.file || !props.file.type) return null;
if (props.file.type.startsWith('image')) return null;
return props.file.type.split('/')[1];
return readableMimeType(props.file.type, true);
});
const imageSource = computed(() => {

View File

@@ -221,9 +221,7 @@ export default defineComponent({
const userStore = useUserStore();
const { layout, layoutOptions, layoutQuery, filters, searchQuery, resetPreset } = usePreset(
ref('directus_files')
);
const { layout, layoutOptions, layoutQuery, filters, searchQuery, resetPreset } = usePreset(ref('directus_files'));
const { confirmDelete, deleting, batchDelete, error: deleteError, batchEditActive } = useBatch();
@@ -412,9 +410,13 @@ export default defineComponent({
async function moveToFolder() {
moving.value = true;
try {
await api.patch(`/files/${selection.value}`, {
folder: selectedFolder.value,
await api.patch(`/files`, {
keys: selection.value,
data: {
folder: selectedFolder.value,
},
});
selection.value = [];

View File

@@ -4,7 +4,7 @@
"application/x-freearc": "arc",
"video/x-msvideo": "avi",
"application/vnd.amazon.ebook": "azw",
"application/octet-stream": "bin",
"application/octet-stream": "file",
"image/bmp": "bmp",
"application/x-bzip": "bz",
"application/x-bzip2": "bz2",
@@ -12,6 +12,7 @@
"text/css": "css",
"text/csv": "csv",
"application/msword": "doc",
"application/sql": "sql",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
"application/vnd.ms-fontobject": "eot",
"application/epub+zip": "epub",

View File

@@ -22,6 +22,7 @@
"application/x-shockwave-flash": "Adobe Flash",
"application/vnd.adobe.fxp": "Adobe Flex Project",
"application/pdf": "PDF",
"application/sql": "SQL",
"application/vnd.cups-ppd": "Adobe PostScript Printer Description File Format",
"application/x-director": "Adobe Shockwave Player",
"application/vnd.adobe.xdp+xml": "Adobe XML Data Package",

View File

@@ -106,9 +106,8 @@ export default defineComponent({
const showDivider = computed(() => {
return (
fieldsStore
.getFieldsForCollection(props.collection)
.filter((field: Field) => field.meta?.hidden !== true).length > 0
fieldsStore.getFieldsForCollection(props.collection).filter((field: Field) => field.meta?.hidden !== true)
.length > 0
);
});
@@ -243,9 +242,7 @@ export default defineComponent({
const relations = relationsStore.getRelationsForField(props.collection, props.junctionField);
const relationForField = relations.find((relation: Relation) => {
return (
relation.many_collection === props.collection && relation.many_field === props.junctionField
);
return relation.many_collection === props.collection && relation.many_field === props.junctionField;
});
if (relationForField.one_collection) return relationForField.one_collection;

View File

@@ -14,7 +14,6 @@
<script lang="ts">
import { defineComponent, ref, watch } from '@vue/composition-api';
import { debounce } from 'lodash';
export default defineComponent({
props: {
@@ -34,7 +33,7 @@ export default defineComponent({
}
});
const emitValue = debounce((event: InputEvent) => emit('input', (event.target as HTMLInputElement).value), 850);
const emitValue = (event: InputEvent) => emit('input', (event.target as HTMLInputElement).value);
return { active, disable, input, emitValue, emptyAndClose };

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/docs",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@directus/docs",
"private": false,
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -5,7 +5,7 @@
"docs",
"packages/*"
],
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"command": {
"bootstrap": {
"npmClientArgs": [

View File

@@ -1,6 +1,6 @@
{
"name": "create-directus-project",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"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",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/format-title",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"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",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/schema",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"description": "Utility for extracting information about existing DB schema",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -51,11 +51,11 @@ export default class MSSQL implements Schema {
c.DATA_TYPE as data_type,
pk.PK_SET as column_key
FROM
${this.knex.client.database()}.INFORMATION_SCHEMA.COLUMNS as c
[${this.knex.client.database()}].INFORMATION_SCHEMA.COLUMNS as c
LEFT JOIN (
SELECT
PK_SET = CASE WHEN CONSTRAINT_NAME LIKE '%pk%' THEN 'PRIMARY' ELSE NULL END
FROM ${this.knex.client.database()}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE
FROM [${this.knex.client.database()}].INFORMATION_SCHEMA.KEY_COLUMN_USAGE
) as pk
ON [c].[TABLE_NAME] = [pk].[CONSTRAINT_TABLE_NAME]
AND [c].[TABLE_CATALOG] = [pk].[CONSTRAINT_CATALOG]

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/sdk-js",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"description": "The official Directus SDK for use in JavaScript!",
"main": "dist/sdk-js.cjs.js",
"module": "dist/sdk-js.bundler.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/specs",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@directus/specs",
"version": "9.0.0-rc.30",
"version": "9.0.0-rc.31",
"description": "OpenAPI Specification of the Directus API",
"main": "index.js",
"scripts": {