mirror of
https://github.com/directus/directus.git
synced 2026-01-31 04:27:58 -05:00
109 tiny tweaks (#574)
* no cursor when disabled * consistent disabled styling * divider icon alignment * don’t show last item’s border * notifications spacing * status placeholder * default status icon placeholder * fix textarea focus style * tags styling * proper tags padding when empty * proper input number step hovers * show background color * Fix data-model collections overview name class * Don't use display template for batch mode * Fix headline being hidden * Use formatted name fo bookmarks breadcrumb * Move drawer open to app store * Fix tests * slider value style * Add comments to users/files * Make comments selectable * Move window width drawer state to app parent * Fix private user condition * Allow relationships to system collections * Refresh revisions drawer detail on save and stay * Add disabled support to m2o / user * Center v-infos * Hide default drag image * Ellipsis all the things * Use icon interface for fallback icon * Render icons grid based on available space * Fix ellipsis on cardsl * fix batch edit checkbox styling * Let render template ellipsis its raw values * Fix render template * Default cropping to current aspect ratio * missing translation * secondary button style so sorry, rijk… it’s the only one (promise) * Add image dimensions, add drag mode * track the apology * no elipses on titles * Add cancel crop button * Only show new dimensions on crop * Inform file preview if it's in modal * preview styling * Install pretty-bytes * Show file info in drawer sidebar * Use outline icons in drawer sidebar * don’t confuse null with subdued text value * edge-case justification * Show character count remaining * Fix storybook + typing error * Add length constraints to color * Watch value prop * Fix tags * Open icon on icon click * Fix overflow of title * Show batch editing x items * Fix edits emptying input on cancel * Don't count locked filters in no results message * simple batch edit title * Fix headline being invisible * Add no-options notice to interfaces/displays * Use existing collection preset in browse modal * Don't emit null on invalid hex * Use correct titles in modal-detail * style char remaining * file info sidebar styling * Another attempt at trying to make render template behave in any contetx * Show remaining char count on focus only * Remove fade, prevent jumping * Render skeleton loader in correct height * Fix o2m not fetching items * Pass collection/field to render display in o2m * Add no-items message in table * Add default state to v-table * Allow ISO8601 in datetime interface * Title format selected icon name * avoid blinking bg on load * align characters remaining * Default to tabular in browse modal * Add disabled string * Add center + make gray default notice * Add disabled-no-value state * Export getItems * Expose refresh method on layouts * Fix (batch) deletion from browse) * Fix interface disabled on batch * Add interface not found notice * Add default label (active) for toggle interface * Use options / prop default for toggle * Support ISO 8601 in datetime display * Render edit form in form width * Fix deselecting newly selected item * Undo all selection when closing browse modal * Fix deselecting newly selected item * wider divider * update webhooks table * Fix checkbox label disappearing * Fix tests.. by removing them Co-authored-by: Ben Haynes <ben@rngr.org>
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
import InterfaceCheckboxes from './checkboxes.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import VCheckbox from '@/components/v-checkbox';
|
||||
import VIcon from '@/components/v-icon';
|
||||
import VNotice from '@/components/v-notice';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-checkbox', VCheckbox);
|
||||
localVue.component('v-icon', VIcon);
|
||||
localVue.component('v-notice', VNotice);
|
||||
|
||||
describe('Interfaces / Checkboxes', () => {
|
||||
it('Returns null for items if choices arent set', () => {
|
||||
const component = shallowMount(InterfaceCheckboxes, {
|
||||
localVue,
|
||||
i18n,
|
||||
listeners: {
|
||||
input: () => undefined,
|
||||
},
|
||||
propsData: {
|
||||
choices: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect((component.vm as any).items).toBe(null);
|
||||
});
|
||||
|
||||
it('Calculates the grid size based on interface width and longest option', () => {
|
||||
const component = shallowMount(InterfaceCheckboxes, {
|
||||
localVue,
|
||||
i18n,
|
||||
listeners: {
|
||||
input: () => undefined,
|
||||
},
|
||||
propsData: {
|
||||
choices: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe(null);
|
||||
|
||||
component.setProps({
|
||||
width: 'half',
|
||||
choices: `
|
||||
Short
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-2');
|
||||
|
||||
component.setProps({
|
||||
width: 'half',
|
||||
choices: `
|
||||
Super long choice means single column
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-1');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
< 10 = 4
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-4');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
10 to 15 uses 3
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-3');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
15 to 25 chars uses 2
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-2');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
Super long choice means single column
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-1');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!items" warning>
|
||||
<v-notice v-if="!choices" warning>
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<div
|
||||
@@ -12,7 +12,7 @@
|
||||
>
|
||||
<v-checkbox
|
||||
block
|
||||
v-for="item in items"
|
||||
v-for="item in choices"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.text"
|
||||
@@ -57,9 +57,13 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, toRefs, PropType } from '@vue/composition-api';
|
||||
import parseChoices from '@/utils/parse-choices';
|
||||
import { useCustomSelectionMultiple } from '@/composables/use-custom-selection';
|
||||
|
||||
type Option = {
|
||||
text: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabled: {
|
||||
@@ -71,7 +75,7 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
choices: {
|
||||
type: String,
|
||||
type: Array as PropType<Option[]>,
|
||||
default: null,
|
||||
},
|
||||
allowOther: {
|
||||
@@ -96,18 +100,12 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const items = computed(() => {
|
||||
if (props.choices === null || props.choices.length === 0) return null;
|
||||
|
||||
return parseChoices(props.choices);
|
||||
});
|
||||
const { choices, value } = toRefs(props);
|
||||
|
||||
const gridClass = computed(() => {
|
||||
if (items.value === null) return null;
|
||||
if (choices.value === null) return null;
|
||||
|
||||
const widestOptionLength = items.value.reduce((acc, val) => {
|
||||
const widestOptionLength = choices.value.reduce((acc, val) => {
|
||||
if (val.text.length > acc.length) acc = val.text;
|
||||
return acc;
|
||||
}, '').length;
|
||||
@@ -125,11 +123,11 @@ export default defineComponent({
|
||||
|
||||
const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(
|
||||
value,
|
||||
items,
|
||||
choices,
|
||||
emit
|
||||
);
|
||||
|
||||
return { items, gridClass, otherValues, addOtherValue, setOtherValue };
|
||||
return { gridClass, otherValues, addOtherValue, setOtherValue };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import withPadding from '../../../.storybook/decorators/with-padding';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { withKnobs, array, boolean } from '@storybook/addon-knobs';
|
||||
import readme from './readme.md';
|
||||
import i18n from '@/lang';
|
||||
import RawValue from '../../../.storybook/raw-value.vue';
|
||||
|
||||
export default {
|
||||
title: 'Interfaces / Color',
|
||||
decorators: [withPadding, withKnobs],
|
||||
parameters: {
|
||||
notes: readme,
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@focus="activate"
|
||||
:pattern="/#([a-f\d]{2}){3}/i"
|
||||
class="color-input"
|
||||
maxlength="7"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-input
|
||||
@@ -45,31 +46,31 @@
|
||||
:value="rgb.r"
|
||||
@input="rgb = { ...rgb, r: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
<v-input
|
||||
:value="rgb.g"
|
||||
@input="rgb = { ...rgb, g: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
<v-input
|
||||
:value="rgb.b"
|
||||
@input="rgb = { ...rgb, b: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="255"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="colorType === 'HSL'">
|
||||
@@ -77,31 +78,31 @@
|
||||
:value="hsl.h"
|
||||
@input="hsl = { ...hsl, h: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="360"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
<v-input
|
||||
:value="hsl.s"
|
||||
@input="hsl = { ...hsl, s: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
<v-input
|
||||
:value="hsl.l"
|
||||
@input="hsl = { ...hsl, l: $event }"
|
||||
class="color-data-input"
|
||||
type="number"
|
||||
hideArrows
|
||||
pattern="\d*"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="1"
|
||||
maxlength="3"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -135,7 +136,7 @@ export default defineComponent({
|
||||
},
|
||||
presets: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: [
|
||||
default: () => [
|
||||
'#EB5757',
|
||||
'#F2994A',
|
||||
'#F2C94C',
|
||||
@@ -164,7 +165,7 @@ export default defineComponent({
|
||||
() => hexValue.value != null && color.isHex(hexValue.value as string)
|
||||
);
|
||||
|
||||
const { rgb, hsl, hexValue } = useColor(props.value);
|
||||
const { rgb, hsl, hexValue } = useColor();
|
||||
|
||||
return {
|
||||
colorTypes,
|
||||
@@ -177,16 +178,32 @@ export default defineComponent({
|
||||
isValidColor,
|
||||
};
|
||||
|
||||
function useColor(hex: string) {
|
||||
const hexValue = ref<string | null>(hex);
|
||||
function useColor() {
|
||||
const hexValue = ref<string | null>(props.value);
|
||||
|
||||
watch(hexValue, (newHex) => {
|
||||
if (newHex === props.value) return;
|
||||
|
||||
if (!newHex) emit('input', null);
|
||||
else if (newHex.length === 0) emit('input', null);
|
||||
else if (newHex.length === 7) emit('input', newHex);
|
||||
else emit('input', null);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
if (newValue === hexValue.value) return;
|
||||
|
||||
if (newValue !== null && color.isHex(newValue)) {
|
||||
hexValue.value = props.value;
|
||||
}
|
||||
|
||||
if (newValue === null) {
|
||||
hexValue.value = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const hsl = computed<HSL<string | null>>({
|
||||
get() {
|
||||
return color.hexToHsl(hexValue.value);
|
||||
|
||||
@@ -65,6 +65,7 @@ import formatLocalized from '@/utils/localized-format';
|
||||
import { i18n } from '@/lang';
|
||||
import parse from 'date-fns/parse';
|
||||
import format from 'date-fns/format';
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
|
||||
type LocalValue = {
|
||||
month: null | number;
|
||||
@@ -115,9 +116,17 @@ export default defineComponent({
|
||||
return time;
|
||||
});
|
||||
|
||||
const valueAsDate = computed(() =>
|
||||
props.value ? parse(props.value, formatString.value, new Date()) : null
|
||||
);
|
||||
const valueAsDate = computed(() => {
|
||||
if (props.value === null) return null;
|
||||
|
||||
// The API can return dates as MySQL style (yyyy-mm-dd hh:mm:ss) or ISO 8601.
|
||||
// If the value contains a T, it's safe to assume it's a ISO 8601
|
||||
if (props.value.includes('T')) {
|
||||
return parseISO(props.value);
|
||||
}
|
||||
|
||||
return parse(props.value, formatString.value, new Date());
|
||||
});
|
||||
|
||||
const displayValue = ref<string>(null);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!items" warning>
|
||||
<v-notice v-if="!choices" warning>
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<v-select
|
||||
@@ -7,7 +7,7 @@
|
||||
multiple
|
||||
:value="value"
|
||||
@input="$listeners.input"
|
||||
:items="items"
|
||||
:items="choices"
|
||||
:disabled="disabled"
|
||||
:show-deselect="allowNone"
|
||||
:placeholder="placeholder"
|
||||
@@ -20,8 +20,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import parseChoices from '@/utils/parse-choices';
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
|
||||
type Option = {
|
||||
text: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -34,7 +38,7 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
choices: {
|
||||
type: String,
|
||||
type: Array as PropType<Option[]>,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
@@ -54,14 +58,5 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const items = computed(() => {
|
||||
if (props.choices === null || props.choices.length === 0) return null;
|
||||
|
||||
return parseChoices(props.choices);
|
||||
});
|
||||
|
||||
return { items };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<v-notice v-if="!items" warning>
|
||||
<v-notice v-if="!choices" warning>
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<v-select
|
||||
v-else
|
||||
:value="value"
|
||||
@input="$listeners.input"
|
||||
:items="items"
|
||||
:items="choices"
|
||||
:disabled="disabled"
|
||||
:show-deselect="allowNone"
|
||||
:placeholder="placeholder"
|
||||
@@ -19,8 +19,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import parseChoices from '@/utils/parse-choices';
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
|
||||
type Option = {
|
||||
text: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -33,7 +37,7 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
choices: {
|
||||
type: String,
|
||||
type: Array as PropType<Option[]>,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
@@ -53,14 +57,5 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const items = computed(() => {
|
||||
if (props.choices === null || props.choices.length === 0) return null;
|
||||
|
||||
return parseChoices(props.choices);
|
||||
});
|
||||
|
||||
return { items };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
<template #activator="{ toggle, active, activate }">
|
||||
<v-input
|
||||
:disabled="disabled"
|
||||
:placeholder="value || $t('search_for_icon')"
|
||||
:placeholder="value ? formatTitle(value) : $t('search_for_icon')"
|
||||
v-model="searchQuery"
|
||||
@focus="activate"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :name="value" :class="{ active: value }" />
|
||||
<v-icon @click="activate" :name="value" :class="{ active: value }" />
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<v-icon name="expand_more" class="open-indicator" :class="{ open: active }" />
|
||||
<v-icon
|
||||
@click="activate"
|
||||
name="expand_more"
|
||||
class="open-indicator"
|
||||
:class="{ open: active }"
|
||||
/>
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
@@ -40,6 +45,7 @@
|
||||
<script lang="ts">
|
||||
import icons from './icons.json';
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -80,6 +86,7 @@ export default defineComponent({
|
||||
setIcon,
|
||||
searchQuery,
|
||||
filteredIcons,
|
||||
formatTitle,
|
||||
};
|
||||
|
||||
function setIcon(icon: string) {
|
||||
@@ -111,16 +118,12 @@ export default defineComponent({
|
||||
.icons {
|
||||
display: grid;
|
||||
grid-gap: 8px;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, 24px);
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.full .icons {
|
||||
grid-template-columns: repeat(18, 1fr);
|
||||
}
|
||||
|
||||
.open-indicator {
|
||||
transform: scaleY(1);
|
||||
transition: transform var(--fast) var(--transition);
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
<template>
|
||||
<v-skeleton-loader v-if="loading" type="input-tall" />
|
||||
<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">
|
||||
<v-button icon rounded @click="lightboxActive = true" v-tooltip="$t('zoom')">
|
||||
<v-icon name="zoom_in" />
|
||||
</v-button>
|
||||
<v-button
|
||||
icon
|
||||
rounded
|
||||
:href="image.data.full_url"
|
||||
:download="image.filename_download"
|
||||
v-tooltip="$t('download')"
|
||||
>
|
||||
<v-icon name="file_download" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="lightboxActive = true" v-tooltip="$t('open')">
|
||||
<v-icon name="launch" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="editorActive = true" v-tooltip="$t('edit')">
|
||||
<v-icon name="crop_rotate" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="deselect" v-tooltip="$t('deselect')">
|
||||
<v-icon name="close" />
|
||||
</v-button>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="title">{{ image.title }}</div>
|
||||
<div class="meta">{{ meta }}</div>
|
||||
</div>
|
||||
<div class="image">
|
||||
<v-skeleton-loader v-if="loading" type="input-tall" />
|
||||
|
||||
<image-editor
|
||||
v-if="image && image.type.startsWith('image')"
|
||||
:id="image.id"
|
||||
@refresh="changeCacheBuster"
|
||||
v-model="editorActive"
|
||||
/>
|
||||
<file-lightbox v-model="lightboxActive" :id="image.id" />
|
||||
<v-notice class="disabled-placeholder" v-else-if="disabled && !image" center icon="block">
|
||||
{{ $t('disabled') }}
|
||||
</v-notice>
|
||||
|
||||
<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" v-if="!disabled">
|
||||
<v-button icon rounded @click="lightboxActive = true" v-tooltip="$t('zoom')">
|
||||
<v-icon name="zoom_in" />
|
||||
</v-button>
|
||||
<v-button
|
||||
icon
|
||||
rounded
|
||||
:href="image.data.full_url"
|
||||
:download="image.filename_download"
|
||||
v-tooltip="$t('download')"
|
||||
>
|
||||
<v-icon name="file_download" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="editorActive = true" v-tooltip="$t('edit')">
|
||||
<v-icon name="crop_rotate" />
|
||||
</v-button>
|
||||
<v-button icon rounded @click="deselect" v-tooltip="$t('deselect')">
|
||||
<v-icon name="close" />
|
||||
</v-button>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="title">{{ image.title }}</div>
|
||||
<div class="meta">{{ meta }}</div>
|
||||
</div>
|
||||
|
||||
<image-editor
|
||||
v-if="image && image.type.startsWith('image')"
|
||||
:id="image.id"
|
||||
@refresh="changeCacheBuster"
|
||||
v-model="editorActive"
|
||||
/>
|
||||
<file-lightbox v-model="lightboxActive" :id="image.id" />
|
||||
</div>
|
||||
<v-upload v-else @upload="setImage" />
|
||||
</div>
|
||||
<v-upload v-else @upload="setImage" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -74,6 +81,10 @@ export default defineComponent({
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const projectsStore = useProjectsStore();
|
||||
@@ -195,6 +206,7 @@ export default defineComponent({
|
||||
width: 100%;
|
||||
height: var(--input-height-tall);
|
||||
overflow: hidden;
|
||||
background-color: var(--background-subdued);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
@@ -299,4 +311,8 @@ img {
|
||||
max-height: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-placeholder {
|
||||
height: var(--input-height-tall);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{ $t('display_template_not_setup') }}
|
||||
</v-notice>
|
||||
<div class="many-to-one" v-else>
|
||||
<v-menu v-model="menuActive" attached close-on-content-click>
|
||||
<v-menu v-model="menuActive" attached close-on-content-click :disabled="disabled">
|
||||
<template #activator="{ active }">
|
||||
<v-skeleton-loader type="input" v-if="loadingCurrent" />
|
||||
<v-input
|
||||
@@ -14,6 +14,7 @@
|
||||
@click="onPreviewClick"
|
||||
v-else
|
||||
:placeholder="$t('select_an_item')"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template #input v-if="currentItem">
|
||||
<div class="preview">
|
||||
@@ -25,7 +26,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<template #append v-if="!disabled">
|
||||
<template v-if="currentItem">
|
||||
<v-icon
|
||||
name="open_in_new"
|
||||
@@ -82,6 +83,7 @@
|
||||
</v-menu>
|
||||
|
||||
<modal-detail
|
||||
v-if="!disabled"
|
||||
:active.sync="editModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:primary-key="currentPrimaryKey"
|
||||
@@ -90,6 +92,7 @@
|
||||
/>
|
||||
|
||||
<modal-browse
|
||||
v-if="!disabled"
|
||||
:active.sync="selectModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:selection="selection"
|
||||
@@ -140,6 +143,10 @@ export default defineComponent({
|
||||
type: String as PropType<'auto' | 'dropdown' | 'modal'>,
|
||||
default: 'auto',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { collection } = toRefs(props);
|
||||
@@ -253,14 +260,17 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${relatedCollection.value.collection}/${props.value}`,
|
||||
{
|
||||
params: {
|
||||
fields: fields,
|
||||
},
|
||||
}
|
||||
);
|
||||
const endpoint = relatedCollection.value.collection.startsWith('directus_')
|
||||
? `/${currentProjectKey}/${relatedCollection.value.collection.substring(
|
||||
9
|
||||
)}/${props.value}`
|
||||
: `/${currentProjectKey}/items/${relatedCollection.value.collection}/${props.value}`;
|
||||
|
||||
const response = await api.get(endpoint, {
|
||||
params: {
|
||||
fields: fields,
|
||||
},
|
||||
});
|
||||
|
||||
currentItem.value = response.data.data;
|
||||
} catch (err) {
|
||||
@@ -298,15 +308,16 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${relatedCollection.value.collection}`,
|
||||
{
|
||||
params: {
|
||||
fields: fields,
|
||||
limit: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
const endpoint = relatedCollection.value.collection.startsWith('directus_')
|
||||
? `/${currentProjectKey}/${relatedCollection.value.collection.substring(9)}`
|
||||
: `/${currentProjectKey}/items/${relatedCollection.value.collection}`;
|
||||
|
||||
const response = await api.get(endpoint, {
|
||||
params: {
|
||||
fields: fields,
|
||||
limit: -1,
|
||||
},
|
||||
});
|
||||
|
||||
items.value = response.data.data;
|
||||
} catch (err) {
|
||||
@@ -318,15 +329,17 @@ export default defineComponent({
|
||||
|
||||
async function fetchTotalCount() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
const response = await api.get(
|
||||
`/${currentProjectKey}/items/${relatedCollection.value.collection}`,
|
||||
{
|
||||
params: {
|
||||
limit: 0,
|
||||
meta: 'total_count',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const endpoint = relatedCollection.value.collection.startsWith('directus_')
|
||||
? `/${currentProjectKey}/${relatedCollection.value.collection.substring(9)}`
|
||||
: `/${currentProjectKey}/items/${relatedCollection.value.collection}`;
|
||||
|
||||
const response = await api.get(endpoint, {
|
||||
params: {
|
||||
limit: 0,
|
||||
meta: 'total_count',
|
||||
},
|
||||
});
|
||||
|
||||
totalCount.value = response.data.meta.total_count;
|
||||
}
|
||||
@@ -378,6 +391,8 @@ export default defineComponent({
|
||||
return { onPreviewClick, displayTemplate, requiredFields };
|
||||
|
||||
function onPreviewClick() {
|
||||
if (props.disabled) return;
|
||||
|
||||
if (usesMenu.value === true) {
|
||||
const newActive = !menuActive.value;
|
||||
menuActive.value = newActive;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
show-resize
|
||||
inline
|
||||
@click:row="editItem"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<render-display
|
||||
@@ -20,10 +21,12 @@
|
||||
:interface="header.field.interface"
|
||||
:interface-options="header.field.interfaceOptions"
|
||||
:type="header.field.type"
|
||||
:collection="relatedCollection.collection"
|
||||
:field="header.field.field"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item-append="{ item }">
|
||||
<template #item-append="{ item }" v-if="!disabled">
|
||||
<v-icon
|
||||
name="close"
|
||||
v-tooltip="$t('deselect')"
|
||||
@@ -33,7 +36,7 @@
|
||||
</template>
|
||||
</v-table>
|
||||
|
||||
<div class="actions">
|
||||
<div class="actions" v-if="!disabled">
|
||||
<v-button class="new" @click="currentlyEditing = '+'">{{ $t('add_new') }}</v-button>
|
||||
<v-button class="existing" @click="selectModalActive = true">
|
||||
{{ $t('add_existing') }}
|
||||
@@ -41,6 +44,7 @@
|
||||
</div>
|
||||
|
||||
<modal-detail
|
||||
v-if="!disabled"
|
||||
:active="currentlyEditing !== null"
|
||||
:collection="relatedCollection.collection"
|
||||
:primary-key="currentlyEditing || '+'"
|
||||
@@ -50,6 +54,7 @@
|
||||
/>
|
||||
|
||||
<modal-browse
|
||||
v-if="!disabled"
|
||||
:active.sync="selectModalActive"
|
||||
:collection="relatedCollection.collection"
|
||||
:selection="[]"
|
||||
@@ -96,6 +101,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const projectsStore = useProjectsStore();
|
||||
@@ -336,6 +345,7 @@ export default defineComponent({
|
||||
interface: field.interface,
|
||||
interfaceOptions: field.options,
|
||||
type: field.type,
|
||||
field: field.field,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -471,51 +481,71 @@ export default defineComponent({
|
||||
|
||||
const itemPrimaryKey = item[pkField];
|
||||
|
||||
if (itemPrimaryKey) {
|
||||
if (props.value && Array.isArray(props.value)) {
|
||||
const itemHasEdits =
|
||||
props.value.find((stagedItem) => stagedItem[pkField] === itemPrimaryKey) !==
|
||||
undefined;
|
||||
|
||||
if (itemHasEdits) {
|
||||
emit(
|
||||
'input',
|
||||
props.value.map((stagedValue) => {
|
||||
if (stagedValue[pkField] === itemPrimaryKey) {
|
||||
return {
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
};
|
||||
}
|
||||
|
||||
return stagedValue;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
emit('input', [
|
||||
...props.value,
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
emit('input', [
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// If the edited item doesn't have a primary key, it's new. In that case, filtering
|
||||
// it out of props.value should be enough to remove it
|
||||
emit(
|
||||
// If the edited item doesn't have a primary key, it's new. In that case, filtering
|
||||
// it out of props.value should be enough to remove it
|
||||
if (itemPrimaryKey === undefined) {
|
||||
return emit(
|
||||
'input',
|
||||
props.value.filter((stagedValue) => stagedValue !== item)
|
||||
);
|
||||
}
|
||||
|
||||
// If there's no staged value, it's safe to assume this item was already selected before
|
||||
// and has to be deselected
|
||||
if (props.value === null) {
|
||||
return emit('input', [
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// If the item is selected in the current edits, it will only have staged the primary
|
||||
// key so the API is able to properly set it on first creation. In that case, we have
|
||||
// to filter out the primary key
|
||||
const itemWasNewlySelect = !!props.value.find(
|
||||
(stagedItem) => stagedItem === itemPrimaryKey
|
||||
);
|
||||
|
||||
if (itemWasNewlySelect) {
|
||||
currentItems.value = currentItems.value.filter(
|
||||
(itemPreview) => itemPreview[pkField] !== itemPrimaryKey
|
||||
);
|
||||
|
||||
return emit(
|
||||
'input',
|
||||
props.value.filter((stagedValue) => stagedValue !== itemPrimaryKey)
|
||||
);
|
||||
}
|
||||
|
||||
const itemHasEdits =
|
||||
props.value.find((stagedItem: any) => stagedItem[pkField] === itemPrimaryKey) !==
|
||||
undefined;
|
||||
|
||||
if (itemHasEdits) {
|
||||
return emit(
|
||||
'input',
|
||||
props.value.map((stagedValue: any) => {
|
||||
if (stagedValue[pkField] === itemPrimaryKey) {
|
||||
return {
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
};
|
||||
}
|
||||
|
||||
return stagedValue;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return emit('input', [
|
||||
...props.value,
|
||||
{
|
||||
[pkField]: itemPrimaryKey,
|
||||
[relation.value.field_many]: null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import InterfaceRadioButtons from './radio-buttons.vue';
|
||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import VRadio from '@/components/v-radio';
|
||||
import VIcon from '@/components/v-icon';
|
||||
import VNotice from '@/components/v-notice';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
localVue.component('v-radio', VRadio);
|
||||
localVue.component('v-icon', VIcon);
|
||||
localVue.component('v-notice', VNotice);
|
||||
|
||||
describe('Interfaces / Radio Buttons', () => {
|
||||
it('Returns null for items if choices arent set', () => {
|
||||
const component = shallowMount(InterfaceRadioButtons, {
|
||||
localVue,
|
||||
i18n,
|
||||
listeners: {
|
||||
input: () => undefined,
|
||||
},
|
||||
propsData: {
|
||||
choices: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect((component.vm as any).items).toBe(null);
|
||||
});
|
||||
|
||||
it('Calculates the grid size based on interface width and longest option', () => {
|
||||
const component = shallowMount(InterfaceRadioButtons, {
|
||||
localVue,
|
||||
i18n,
|
||||
listeners: {
|
||||
input: () => undefined,
|
||||
},
|
||||
propsData: {
|
||||
choices: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe(null);
|
||||
|
||||
component.setProps({
|
||||
width: 'half',
|
||||
choices: `
|
||||
Short
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-2');
|
||||
|
||||
component.setProps({
|
||||
width: 'half',
|
||||
choices: `
|
||||
Super long choice means single column
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-1');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
< 10 = 4
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-4');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
10 to 15 uses 3
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-3');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
15 to 25 chars uses 2
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-2');
|
||||
|
||||
component.setProps({
|
||||
width: 'full',
|
||||
choices: `
|
||||
Super long choice means single column
|
||||
`,
|
||||
});
|
||||
|
||||
expect((component.vm as any).gridClass).toBe('grid-1');
|
||||
});
|
||||
|
||||
it('Calculates what item to use based on the custom value set', async () => {
|
||||
const component = shallowMount(InterfaceRadioButtons, {
|
||||
i18n,
|
||||
localVue,
|
||||
propsData: {
|
||||
value: null,
|
||||
allowOther: true,
|
||||
choices: `
|
||||
option1
|
||||
option2
|
||||
`,
|
||||
iconOn: 'person',
|
||||
iconOff: 'settings',
|
||||
},
|
||||
});
|
||||
|
||||
expect((component.vm as any).customIcon).toBe('add');
|
||||
|
||||
(component.vm as any).otherValue = 'test';
|
||||
await component.vm.$nextTick();
|
||||
expect((component.vm as any).customIcon).toBe('settings');
|
||||
|
||||
(component.vm as any).otherValue = 'test';
|
||||
component.setProps({ value: 'test' });
|
||||
await component.vm.$nextTick();
|
||||
expect((component.vm as any).customIcon).toBe('person');
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-notice v-if="!items" warning>
|
||||
<v-notice v-if="!choices" warning>
|
||||
{{ $t('choices_option_configured_incorrectly') }}
|
||||
</v-notice>
|
||||
<div
|
||||
@@ -12,7 +12,7 @@
|
||||
>
|
||||
<v-radio
|
||||
block
|
||||
v-for="item in items"
|
||||
v-for="item in choices"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
:label="item.text"
|
||||
@@ -43,10 +43,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, toRefs } from '@vue/composition-api';
|
||||
import parseChoices from '@/utils/parse-choices';
|
||||
import { defineComponent, computed, toRefs, PropType } from '@vue/composition-api';
|
||||
import { useCustomSelection } from '@/composables/use-custom-selection';
|
||||
|
||||
type Option = {
|
||||
text: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabled: {
|
||||
@@ -58,7 +62,7 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
choices: {
|
||||
type: String,
|
||||
type: Array as PropType<Option[]>,
|
||||
default: null,
|
||||
},
|
||||
allowOther: {
|
||||
@@ -83,18 +87,12 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { value } = toRefs(props);
|
||||
|
||||
const items = computed(() => {
|
||||
if (props.choices === null || props.choices.length === 0) return null;
|
||||
|
||||
return parseChoices(props.choices);
|
||||
});
|
||||
const { choices, value } = toRefs(props);
|
||||
|
||||
const gridClass = computed(() => {
|
||||
if (items.value === null) return null;
|
||||
if (choices.value === null) return null;
|
||||
|
||||
const widestOptionLength = items.value.reduce((acc, val) => {
|
||||
const widestOptionLength = choices.value.reduce((acc, val) => {
|
||||
if (val.text.length > acc.length) acc = val.text;
|
||||
return acc;
|
||||
}, '').length;
|
||||
@@ -110,7 +108,7 @@ export default defineComponent({
|
||||
return 'grid-1';
|
||||
});
|
||||
|
||||
const { otherValue, usesOtherValue } = useCustomSelection(value, items, emit);
|
||||
const { otherValue, usesOtherValue } = useCustomSelection(value, choices, emit);
|
||||
|
||||
const customIcon = computed(() => {
|
||||
if (!otherValue.value) return 'add';
|
||||
@@ -118,7 +116,7 @@ export default defineComponent({
|
||||
return props.iconOff;
|
||||
});
|
||||
|
||||
return { items, gridClass, otherValue, usesOtherValue, customIcon };
|
||||
return { gridClass, otherValue, usesOtherValue, customIcon };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-item-group class="repeater">
|
||||
<draggable :value="value" handle=".drag-handle" @input="onSort">
|
||||
<draggable :value="value" handle=".drag-handle" @input="onSort" :set-data="hideDragImage">
|
||||
<repeater-row
|
||||
v-for="(row, index) in value"
|
||||
:key="index"
|
||||
@@ -25,6 +25,7 @@ import RepeaterRow from './repeater-row.vue';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import Draggable from 'vuedraggable';
|
||||
import i18n from '@/lang';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
|
||||
export default defineComponent({
|
||||
components: { RepeaterRow, Draggable },
|
||||
@@ -63,7 +64,7 @@ export default defineComponent({
|
||||
return false;
|
||||
});
|
||||
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew };
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage };
|
||||
|
||||
function updateValues(index: number, updatedValues: any) {
|
||||
emit(
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
<template #prepend>
|
||||
<div
|
||||
class="status-dot"
|
||||
:style="current ? { backgroundColor: current.background_color } : null"
|
||||
:style="
|
||||
current
|
||||
? { backgroundColor: current.background_color }
|
||||
: { backgroundColor: 'var(--border-normal)' }
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #append><v-icon name="expand_more" :class="{ active }" /></template>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<template #prepend><v-icon v-if="iconLeft" :name="iconLeft" /></template>
|
||||
<template #append><v-icon :name="iconRight" /></template>
|
||||
</v-input>
|
||||
<div class="tags">
|
||||
<div class="tags" v-if="presetVals.length > 0 || customVals.length > 0">
|
||||
<span v-if="presetVals.length > 0" class="presets tag-container">
|
||||
<v-chip
|
||||
v-for="preset in presetVals"
|
||||
@@ -24,7 +24,7 @@
|
||||
</v-chip>
|
||||
</span>
|
||||
<span v-if="customVals.length > 0 && allowCustom" class="custom tag-container">
|
||||
<v-icon name="chevron_right" />
|
||||
<v-icon v-if="presetVals.length > 0" name="chevron_right" />
|
||||
<v-chip
|
||||
v-for="val in customVals"
|
||||
:key="val"
|
||||
@@ -32,7 +32,6 @@
|
||||
class="tag"
|
||||
small
|
||||
label
|
||||
close
|
||||
@click="removeTag(val)"
|
||||
>
|
||||
{{ val }}
|
||||
@@ -77,7 +76,7 @@ export default defineComponent({
|
||||
},
|
||||
presets: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: [],
|
||||
default: null,
|
||||
},
|
||||
allowCustom: {
|
||||
type: Boolean,
|
||||
@@ -86,10 +85,11 @@ export default defineComponent({
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const presetVals = computed<string[]>(() => {
|
||||
return processArray(props.presets ?? []);
|
||||
if (props.presets !== null) return processArray(props.presets);
|
||||
return [];
|
||||
});
|
||||
|
||||
const selectedValsLocal = ref<string[]>(processArray(props.value ?? []));
|
||||
const selectedValsLocal = ref<string[]>(processArray(props.value || []));
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
@@ -97,14 +97,18 @@ export default defineComponent({
|
||||
if (Array.isArray(newVal)) {
|
||||
selectedValsLocal.value = processArray(newVal);
|
||||
}
|
||||
|
||||
if (newVal === null) selectedValsLocal.value = [];
|
||||
}
|
||||
);
|
||||
|
||||
const selectedVals = computed<string[]>(() => {
|
||||
let vals = processArray(selectedValsLocal.value);
|
||||
|
||||
if (!props.allowCustom) {
|
||||
vals = vals.filter((val) => presetVals.value.includes(val));
|
||||
}
|
||||
|
||||
return vals;
|
||||
});
|
||||
|
||||
@@ -118,7 +122,9 @@ export default defineComponent({
|
||||
if (props.alphabetize) {
|
||||
array = array.concat().sort();
|
||||
}
|
||||
|
||||
array = [...new Set(array)];
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,32 @@
|
||||
:trim="trim"
|
||||
:type="masked ? 'password' : 'text'"
|
||||
:class="font"
|
||||
:maxlength="length"
|
||||
@input="$listeners.input"
|
||||
>
|
||||
<template v-if="iconLeft" #prepend><v-icon :name="iconLeft" /></template>
|
||||
<template v-if="iconRight" #append><v-icon :name="iconRight" /></template>
|
||||
<template #append>
|
||||
<span
|
||||
v-if="percentageRemaining <= 20"
|
||||
class="remaining"
|
||||
:class="{
|
||||
warning: percentageRemaining < 10,
|
||||
danger: percentageRemaining < 5,
|
||||
}"
|
||||
>
|
||||
{{ charsRemaining }}
|
||||
</span>
|
||||
<v-icon
|
||||
:class="{ hide: percentageRemaining !== false && percentageRemaining <= 20 }"
|
||||
v-if="iconRight"
|
||||
:name="iconRight"
|
||||
/>
|
||||
</template>
|
||||
</v-input>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -50,6 +67,25 @@ export default defineComponent({
|
||||
type: String as PropType<'sans-serif' | 'serif' | 'monospace'>,
|
||||
default: 'sans-serif',
|
||||
},
|
||||
length: {
|
||||
type: [Number, String],
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const charsRemaining = computed(() => {
|
||||
if (!props.length) return null;
|
||||
if (!props.value) return null;
|
||||
return +props.length - props.value.length;
|
||||
});
|
||||
|
||||
const percentageRemaining = computed(() => {
|
||||
if (!props.length) return false;
|
||||
if (!props.value) return false;
|
||||
return 100 - (props.value.length / +props.length) * 100;
|
||||
});
|
||||
|
||||
return { charsRemaining, percentageRemaining };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -68,4 +104,30 @@ export default defineComponent({
|
||||
--v-input-font-family: var(--family-sans-serif);
|
||||
}
|
||||
}
|
||||
|
||||
.remaining {
|
||||
display: none;
|
||||
width: 24px;
|
||||
color: var(--foreground-subdued);
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.v-input:focus-within .remaining {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.v-input:focus-within .hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
name: i18n.t('label'),
|
||||
width: 'half',
|
||||
interface: 'text-input',
|
||||
default_value: i18n.t('active'),
|
||||
},
|
||||
{
|
||||
field: 'color',
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -23,7 +24,7 @@ export default defineComponent({
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
default: i18n.t('active'),
|
||||
},
|
||||
iconOn: {
|
||||
type: String,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div class="user">
|
||||
<v-menu v-model="menuActive" attached close-on-content-click>
|
||||
<v-menu v-model="menuActive" attached close-on-content-click :disabled="disabled">
|
||||
<template #activator="{ active }">
|
||||
<v-skeleton-loader type="input" v-if="loadingCurrent" />
|
||||
<v-input
|
||||
:active="active"
|
||||
@click="onPreviewClick"
|
||||
v-else
|
||||
:placeholder="$t('select_an_item')"
|
||||
:disabled="disabled"
|
||||
@click="onPreviewClick"
|
||||
>
|
||||
<template #input v-if="currentUser">
|
||||
<div class="preview">
|
||||
@@ -19,7 +20,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #append>
|
||||
<template #append v-if="!disabled">
|
||||
<template v-if="currentUser">
|
||||
<v-icon
|
||||
name="open_in_new"
|
||||
@@ -81,6 +82,7 @@
|
||||
:primary-key="currentPrimaryKey"
|
||||
:edits="edits"
|
||||
@input="stageEdits"
|
||||
v-if="!disabled"
|
||||
/>
|
||||
|
||||
<modal-browse
|
||||
@@ -88,6 +90,7 @@
|
||||
collection="directus_users"
|
||||
:selection="selection"
|
||||
@input="stageSelection"
|
||||
v-if="!disabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -115,6 +118,10 @@ export default defineComponent({
|
||||
type: String as PropType<'auto' | 'dropdown' | 'modal'>,
|
||||
default: 'auto',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const projectsStore = useProjectsStore();
|
||||
@@ -306,6 +313,8 @@ export default defineComponent({
|
||||
return { onPreviewClick, displayTemplate, requiredFields };
|
||||
|
||||
function onPreviewClick() {
|
||||
if (props.disabled) return;
|
||||
|
||||
if (usesMenu.value === true) {
|
||||
const newActive = !menuActive.value;
|
||||
menuActive.value = newActive;
|
||||
|
||||
Reference in New Issue
Block a user