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:
Rijk van Zanten
2020-05-01 18:08:37 -04:00
committed by GitHub
parent 4dabcc6a25
commit 31ef7a48c9
21 changed files with 181 additions and 41 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View File

@@ -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';

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -7,7 +7,7 @@
position: absolute;
top: 0;
left: 0;
z-index: 50;
z-index: 500;
display: none;
max-width: 260px;
padding: 4px 8px;

View File

@@ -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';
}
}

View File

@@ -0,0 +1,4 @@
import getDefaultInterfaceForType from './get-default-interface-for-type';
export { getDefaultInterfaceForType };
export default getDefaultInterfaceForType;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,

View File

@@ -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,

View File

@@ -0,0 +1,4 @@
import ValueNull from './value-null.vue';
export { ValueNull };
export default ValueNull;

View File

@@ -0,0 +1,9 @@
<template functional>
<span class="null">--</span>
</template>
<style lang="scss" scoped>
.null {
color: var(--foreground-subdued);
}
</style>

View File

@@ -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" />