mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Various fixes (#515)
* Fix image deselection when newID isn't set * Fix svg support in image interface * Render correct title on file library detail * Default to interfaces based on type * Remove unused import * Don't show dropdown indicator on field label when readonly * Accept datetime_created datetime_updated in datetime interface * Add reused value-null component * Fix z-index of v-badge * Close status on content click * Move edit button to header on file detail * Increase tooltip z-index * Add warning notice to edit image modal * Fix extra spacing in divider interface * Use wide style for notifications when drawer is open * Click on image to open file preview
This commit is contained in:
@@ -78,7 +78,7 @@ body {
|
||||
position: absolute;
|
||||
top: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-y));
|
||||
right: calc(var(--v-badge-size) / -2 + var(--v-badge-offset-x));
|
||||
z-index: 10;
|
||||
z-index: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"
|
||||
>
|
||||
<template #activator="{ toggle, active }">
|
||||
<div class="label type-label">
|
||||
<div class="label type-label" :class="{ readonly: field.readonly }">
|
||||
<v-checkbox
|
||||
v-if="batchMode"
|
||||
@change="toggleBatchField(field)"
|
||||
@@ -23,7 +23,12 @@
|
||||
<span @click="toggle">
|
||||
{{ field.name }}
|
||||
<v-icon class="required" sup name="star" v-if="field.required" />
|
||||
<v-icon class="ctx-arrow" :class="{ active }" name="arrow_drop_down" />
|
||||
<v-icon
|
||||
v-if="!field.readonly"
|
||||
class="ctx-arrow"
|
||||
:class="{ active }"
|
||||
name="arrow_drop_down"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,6 +109,7 @@ import { clone } from 'lodash';
|
||||
import { FormField } from './types';
|
||||
import interfaces from '@/interfaces';
|
||||
import marked from 'marked';
|
||||
import getDefaultInterfaceForType from '@/utils/get-default-interface-for-type';
|
||||
|
||||
type FieldValues = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -212,18 +218,12 @@ export default defineComponent({
|
||||
return field;
|
||||
});
|
||||
|
||||
// Make sure all used interfaces actually exist, default to text-input if not
|
||||
formFields = formFields.map((field) => {
|
||||
const interfaceUsed = interfaces.find((int) => int.id === field.interface);
|
||||
const interfaceExists = interfaceUsed !== undefined;
|
||||
|
||||
if (interfaceExists === false) {
|
||||
/**
|
||||
* @NOTE
|
||||
* Can be optimized by making the default smarter based on type used for the
|
||||
* field
|
||||
*/
|
||||
field.interface = 'text-input';
|
||||
field.interface = getDefaultInterfaceForType(field.type);
|
||||
}
|
||||
|
||||
if (interfaceUsed?.hideLabel === true) {
|
||||
@@ -379,6 +379,10 @@ body {
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&.readonly {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.v-checkbox {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<p v-if="subtitle" class="subtitle">{{ subtitle }}</p>
|
||||
<div class="spacer" />
|
||||
<v-icon name="" />
|
||||
<slot name="header:append" />
|
||||
</header>
|
||||
<div class="content" :class="{ 'no-padding': noPadding }">
|
||||
<v-overlay
|
||||
|
||||
@@ -87,9 +87,12 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<'datetime' | 'time' | 'date'>,
|
||||
type: String as PropType<
|
||||
'datetime' | 'time' | 'date' | 'datetime_created' | 'datetime_updated'
|
||||
>,
|
||||
required: true,
|
||||
validator: (val: string) => ['datetime', 'date', 'time'].includes(val),
|
||||
validator: (val: string) =>
|
||||
['datetime', 'date', 'time', 'datetime_created', 'datetime_updated'].includes(val),
|
||||
},
|
||||
includeSeconds: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}"
|
||||
>
|
||||
<template v-if="icon" #icon><v-icon :name="icon" /></template>
|
||||
{{ title }}
|
||||
<template v-if="title" #default>{{ title }}</template>
|
||||
</v-divider>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-skeleton-loader v-if="loading" type="input-tall" />
|
||||
<div class="image-preview" v-else-if="image">
|
||||
<div class="image-preview" v-else-if="image" :class="{ isSVG: image.type.includes('svg') }">
|
||||
<img :src="src" alt="" role="presentation" />
|
||||
<div class="shadow" />
|
||||
<div class="actions">
|
||||
@@ -80,6 +80,10 @@ export default defineComponent({
|
||||
const src = computed(() => {
|
||||
if (!image.value) return null;
|
||||
|
||||
if (image.value.type.includes('svg')) {
|
||||
return image.value.data.full_url;
|
||||
}
|
||||
|
||||
const url = image.value.data.thumbnails.find(
|
||||
(thumb) => thumb.key === 'directus-large-crop'
|
||||
)?.url;
|
||||
@@ -107,7 +111,7 @@ export default defineComponent({
|
||||
fetchImage();
|
||||
}
|
||||
|
||||
if (newID === null) {
|
||||
if (oldID && newID === null) {
|
||||
deselect();
|
||||
}
|
||||
}
|
||||
@@ -192,6 +196,15 @@ img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.isSVG {
|
||||
padding: 32px;
|
||||
background-color: var(--background-subdued);
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<v-notice v-if="!statuses">
|
||||
{{ $t('statuses_not_configured') }}
|
||||
</v-notice>
|
||||
<v-menu v-else attached :disabled="disabled">
|
||||
<v-menu v-else attached :disabled="disabled" close-on-content-click>
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-input
|
||||
readonly
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed, watch } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
|
||||
|
||||
import 'tinymce/tinymce';
|
||||
import 'tinymce/themes/silver';
|
||||
|
||||
@@ -296,6 +296,8 @@
|
||||
"has": "Contains some of these keys"
|
||||
},
|
||||
|
||||
"loading": "Loading...",
|
||||
|
||||
"search_for_icon": "Search for icon...",
|
||||
|
||||
"drop_to_upload": "Drop to Upload",
|
||||
@@ -428,6 +430,9 @@
|
||||
"adding_in": "Adding New Item in {collection}",
|
||||
"editing_in": "Editing Item in {collection}",
|
||||
|
||||
"editing_file": "Editing File: {title}",
|
||||
"changes_are_immediate_and_permanent": "Changes are immediate and permanent",
|
||||
|
||||
"about_directus": "About Directus",
|
||||
"activity_log": "Activity Log",
|
||||
"add_field_filter": "Add a field filter",
|
||||
@@ -699,7 +704,6 @@
|
||||
"less_than": "Less than",
|
||||
"less_than_equal": "Less than or equal to",
|
||||
"limited": "Limited",
|
||||
"loading": "Loading...",
|
||||
"load_more": "Load more",
|
||||
"loading_more": "Loading More Items",
|
||||
"login": "Log in",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="$t('editing', { collection: $t('files') })">
|
||||
<private-view :title="loading ? $t('loading') : $t('editing_file', { title: item.title })">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
@@ -38,6 +38,16 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button
|
||||
v-if="item && item.type.includes('image')"
|
||||
rounded
|
||||
icon
|
||||
@click="editActive = true"
|
||||
class="edit"
|
||||
>
|
||||
<v-icon name="tune" />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
@@ -70,17 +80,17 @@
|
||||
:width="item.width"
|
||||
:height="item.height"
|
||||
:title="item.title"
|
||||
@click="previewActive = true"
|
||||
/>
|
||||
|
||||
<file-lightbox :id="item.id" v-model="previewActive" />
|
||||
|
||||
<image-editor
|
||||
v-if="item && item.type.startsWith('image')"
|
||||
:id="item.id"
|
||||
@refresh="changeCacheBuster"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-button @click="on">Edit</v-button>
|
||||
</template>
|
||||
</image-editor>
|
||||
v-model="editActive"
|
||||
/>
|
||||
|
||||
<v-form
|
||||
:loading="loading"
|
||||
@@ -113,6 +123,7 @@ import SaveOptions from '@/views/private/components/save-options';
|
||||
import FilePreview from '@/views/private/components/file-preview';
|
||||
import ImageEditor from '@/views/private/components/image-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import FileLightbox from '@/views/private/components/file-lightbox';
|
||||
|
||||
type Values = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -121,7 +132,14 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'files-detail',
|
||||
components: { FilesNavigation, ActivityDrawerDetail, SaveOptions, FilePreview, ImageEditor },
|
||||
components: {
|
||||
FilesNavigation,
|
||||
ActivityDrawerDetail,
|
||||
SaveOptions,
|
||||
FilePreview,
|
||||
ImageEditor,
|
||||
FileLightbox,
|
||||
},
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -154,6 +172,10 @@ export default defineComponent({
|
||||
|
||||
const cacheBuster = ref(nanoid());
|
||||
|
||||
const editActive = ref(false);
|
||||
|
||||
const previewActive = ref(false);
|
||||
|
||||
return {
|
||||
item,
|
||||
loading,
|
||||
@@ -173,6 +195,8 @@ export default defineComponent({
|
||||
isBatch,
|
||||
changeCacheBuster,
|
||||
cacheBuster,
|
||||
editActive,
|
||||
previewActive,
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
@@ -236,6 +260,13 @@ export default defineComponent({
|
||||
--v-button-background-color: var(--background-normal);
|
||||
}
|
||||
|
||||
.edit {
|
||||
--v-button-background-color: var(--primary-25);
|
||||
--v-button-color: var(--primary);
|
||||
--v-button-background-color-hover: var(--primary-50);
|
||||
--v-button-color-hover: var(--primary);
|
||||
}
|
||||
|
||||
.file-detail {
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
z-index: 500;
|
||||
display: none;
|
||||
max-width: 260px;
|
||||
padding: 4px 8px;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export default function getDefaultInterfaceForType(type: string | null | undefined) {
|
||||
switch (type) {
|
||||
case 'datetime':
|
||||
case 'date':
|
||||
case 'time':
|
||||
case 'datetime_created':
|
||||
case 'datetime_updated':
|
||||
return 'datetime';
|
||||
|
||||
case 'owner':
|
||||
case 'user_updated':
|
||||
case 'user':
|
||||
return 'user';
|
||||
|
||||
case 'file':
|
||||
return 'file';
|
||||
|
||||
case 'integer':
|
||||
case 'sort':
|
||||
case 'decimal':
|
||||
return 'numeric';
|
||||
|
||||
case 'status':
|
||||
return 'status';
|
||||
|
||||
case 'slug':
|
||||
return 'slug';
|
||||
|
||||
case 'm2o':
|
||||
return 'many-to-one';
|
||||
|
||||
case 'json':
|
||||
return 'code';
|
||||
|
||||
case 'array':
|
||||
return 'tags';
|
||||
|
||||
case 'hash':
|
||||
case 'group':
|
||||
case 'lang':
|
||||
case 'translation':
|
||||
case 'uuid':
|
||||
case 'string':
|
||||
case 'alias':
|
||||
case 'binary':
|
||||
case 'boolean':
|
||||
case 'o2m':
|
||||
default:
|
||||
return 'text-input';
|
||||
}
|
||||
}
|
||||
4
src/utils/get-default-interface-for-type/index.ts
Normal file
4
src/utils/get-default-interface-for-type/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import getDefaultInterfaceForType from './get-default-interface-for-type';
|
||||
|
||||
export { getDefaultInterfaceForType };
|
||||
export default getDefaultInterfaceForType;
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="file-preview" v-if="type">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG }">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG }" @click="$emit('click')">
|
||||
<img :src="src" :width="width" :height="height" :alt="title" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
<slot name="activator" v-bind="activatorBinding" />
|
||||
</template>
|
||||
|
||||
<template #header:append>
|
||||
<span class="warning">{{ $t('changes_are_immediate_and_permanent') }}</span>
|
||||
</template>
|
||||
|
||||
<div class="loader" v-if="loading">
|
||||
<v-progress-circular indeterminate />
|
||||
</div>
|
||||
@@ -335,13 +339,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: var(--background-subdued);
|
||||
|
||||
&,
|
||||
.editor {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -359,21 +366,21 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
color: var(--white);
|
||||
background-color: rgba(0 0 0 / 75%);
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: #263238;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:key="notification.id"
|
||||
v-bind="notification"
|
||||
:tail="index === queue.length - 1"
|
||||
dense
|
||||
:dense="dense"
|
||||
:show-close="notification.persist === true && notification.closeable !== false"
|
||||
/>
|
||||
</transition-group>
|
||||
@@ -19,6 +19,12 @@ import NotificationItem from '../notification-item';
|
||||
|
||||
export default defineComponent({
|
||||
components: { NotificationItem },
|
||||
props: {
|
||||
dense: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const queue = toRefs(notificationsStore.state).queue;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span v-if="value === null || value === undefined">--</span>
|
||||
<value-null v-if="value === null || value === undefined" />
|
||||
<span v-else-if="!displayInfo">{{ value }}</span>
|
||||
<span v-else-if="typeof displayInfo.handler === 'function'">
|
||||
{{ display.handler(value, options) }}
|
||||
@@ -17,8 +17,10 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import displays from '@/displays';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ValueNull },
|
||||
props: {
|
||||
display: {
|
||||
type: String,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="render-template">
|
||||
<template v-for="(part, index) in parts">
|
||||
<span :key="index" v-if="part === null || part.value === null" class="subdued">--</span>
|
||||
<value-null :key="index" v-if="part === null || part.value === null" />
|
||||
<component
|
||||
v-else-if="typeof part === 'object'"
|
||||
:is="`display-${part.component}`"
|
||||
:key="index"
|
||||
:value="part.value === null ? '--' : part.value"
|
||||
:value="part.value"
|
||||
:interface="part.interface"
|
||||
:interface-options="part.interfaceOptions"
|
||||
v-bind="part.options"
|
||||
@@ -22,8 +22,10 @@ import useFieldsStore from '@/stores/fields';
|
||||
import { get } from 'lodash';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import displays from '@/displays';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
|
||||
export default defineComponent({
|
||||
components: { ValueNull },
|
||||
props: {
|
||||
collection: {
|
||||
type: String,
|
||||
|
||||
4
src/views/private/components/value-null/index.ts
Normal file
4
src/views/private/components/value-null/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import ValueNull from './value-null.vue';
|
||||
|
||||
export { ValueNull };
|
||||
export default ValueNull;
|
||||
9
src/views/private/components/value-null/value-null.vue
Normal file
9
src/views/private/components/value-null/value-null.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template functional>
|
||||
<span class="null">--</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.null {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,7 @@
|
||||
<v-overlay class="nav-overlay" :active="navOpen" @click="navOpen = false" />
|
||||
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
|
||||
|
||||
<notifications-group v-if="navigationsInline === false" />
|
||||
<notifications-group v-if="navigationsInline === false" :dense="drawerOpen === false" />
|
||||
|
||||
<template v-if="showDropEffect">
|
||||
<div class="drop-border top" />
|
||||
|
||||
Reference in New Issue
Block a user