mirror of
https://github.com/directus/directus.git
synced 2026-01-28 14:28:02 -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:
@@ -33,6 +33,7 @@
|
||||
"nanoid": "^3.1.7",
|
||||
"pinia": "0.0.5",
|
||||
"portal-vue": "^2.1.7",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"resize-observer": "^1.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"stylelint-config-prettier": "^8.0.1",
|
||||
|
||||
16
src/app.vue
16
src/app.vue
@@ -16,6 +16,7 @@ import { defineComponent, toRefs, watch, computed } from '@vue/composition-api';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useProjectsStore } from '@/stores/projects';
|
||||
import useWindowSize from '@/composables/use-window-size';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -23,7 +24,7 @@ export default defineComponent({
|
||||
const userStore = useUserStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { hydrating } = toRefs(appStore.state);
|
||||
const { hydrating, drawerOpen } = toRefs(appStore.state);
|
||||
|
||||
const brandStyle = computed(() => {
|
||||
return {
|
||||
@@ -31,6 +32,19 @@ export default defineComponent({
|
||||
};
|
||||
});
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
watch(width, (newWidth, oldWidth) => {
|
||||
if (newWidth === null || newWidth === 0) return;
|
||||
if (newWidth === oldWidth) return;
|
||||
|
||||
if (newWidth >= 1424) {
|
||||
if (drawerOpen.value === false) drawerOpen.value = true;
|
||||
} else {
|
||||
if (drawerOpen.value === true) drawerOpen.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => userStore.state.currentUser,
|
||||
(newUser) => {
|
||||
|
||||
@@ -153,7 +153,7 @@ body {
|
||||
--v-button-background-color-activated: var(--primary);
|
||||
--v-button-background-color-disabled: var(--background-normal);
|
||||
--v-button-font-size: 16px;
|
||||
--v-button-font-weight: 500;
|
||||
--v-button-font-weight: 600;
|
||||
--v-button-line-height: 22px;
|
||||
--v-button-min-width: 140px;
|
||||
}
|
||||
@@ -168,7 +168,7 @@ body {
|
||||
--v-button-color: var(--foreground-normal);
|
||||
--v-button-color-hover: var(--foreground-normal);
|
||||
--v-button-color-activated: var(--foreground-normal);
|
||||
--v-button-background-color: var(--background-normal-alt);
|
||||
--v-button-background-color: var(--border-subdued); // I'm so sorry! 🥺
|
||||
--v-button-background-color-hover: var(--background-normal-alt);
|
||||
--v-button-background-color-activated: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
@@ -179,13 +179,17 @@ body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-subdued);
|
||||
border-radius: var(--border-radius);
|
||||
content: '';
|
||||
}
|
||||
|
||||
> * {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.indeterminate).checked {
|
||||
|
||||
@@ -56,7 +56,13 @@ body {
|
||||
}
|
||||
|
||||
span.wrapper {
|
||||
display: flex;
|
||||
margin-right: 16px;
|
||||
color: var(--v-divider-label-color);
|
||||
|
||||
.v-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.type-text {
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
}"
|
||||
>
|
||||
<v-skeleton-loader v-if="loading && field.hideLoader !== true" />
|
||||
|
||||
<component
|
||||
v-if="interfaceExists"
|
||||
:is="`interface-${field.interface}`"
|
||||
v-bind="field.options"
|
||||
:disabled="disabled"
|
||||
@@ -16,14 +18,20 @@
|
||||
:collection="field.collection"
|
||||
:field="field.field"
|
||||
:primary-key="primaryKey"
|
||||
:length="field.length"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
|
||||
<v-notice v-else warning>
|
||||
{{ $t('interface_not_found', { interface: field.interface }) }}
|
||||
</v-notice>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import interfaces from '@/interfaces';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -56,6 +64,13 @@ export default defineComponent({
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const interfaceExists = computed(() => {
|
||||
return !!interfaces.find((inter) => inter.id === props.field.interface);
|
||||
});
|
||||
|
||||
return { interfaceExists };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -61,7 +61,9 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.v-checkbox {
|
||||
height: 18px; // Don't push down label with normal icon height (24px)
|
||||
margin-right: 4px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.required {
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
:batch-mode="batchMode"
|
||||
:batch-active="batchActive"
|
||||
:disabled="isDisabled"
|
||||
:primary-key="primaryKey"
|
||||
@input="$emit('input', $event)"
|
||||
/>
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ export default defineComponent({
|
||||
const gridClass = computed<string | null>(() => {
|
||||
if (el.value === null) return null;
|
||||
|
||||
if (width.value > 612 && width.value <= 700) {
|
||||
if (width.value > 612 && width.value <= 792) {
|
||||
return 'grid';
|
||||
} else {
|
||||
return 'grid with-fill';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="v-info" :class="type">
|
||||
<div class="v-info" :class="[type, { center }]">
|
||||
<div class="icon">
|
||||
<v-icon large :name="icon" />
|
||||
</div>
|
||||
@@ -26,6 +26,10 @@ export default defineComponent({
|
||||
type: String as PropType<'info' | 'success' | 'warning' | 'danger'>,
|
||||
default: 'info',
|
||||
},
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -80,4 +84,11 @@ export default defineComponent({
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
class="v-input"
|
||||
@click="$emit('click', $event)"
|
||||
:class="{ 'full-width': fullWidth, 'has-click': hasClick }"
|
||||
:class="{ 'full-width': fullWidth, 'has-click': hasClick, disabled: disabled }"
|
||||
>
|
||||
<div v-if="$slots['prepend-outer']" class="prepend-outer">
|
||||
<slot name="prepend-outer" :value="value" :disabled="disabled" />
|
||||
@@ -273,6 +273,10 @@ body {
|
||||
|
||||
display: block;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
--arrow-color: var(--primary);
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
@@ -294,7 +298,7 @@ body {
|
||||
|
||||
&:focus-within,
|
||||
&.active {
|
||||
--arrow-color: var(--primary);
|
||||
--arrow-color: var(--border-normal-alt);
|
||||
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-page);
|
||||
@@ -354,6 +358,11 @@ body {
|
||||
|
||||
&.has-click {
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
pointer-events: none;
|
||||
.prefix,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<slot name="activator" v-bind="{ on }" />
|
||||
</template>
|
||||
|
||||
<article class="v-modal">
|
||||
<article class="v-modal" :class="{ 'form-width': formWidth }">
|
||||
<header class="header">
|
||||
<v-icon class="menu-toggle" name="menu" @click="sidebarActive = !sidebarActive" />
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
@@ -27,7 +27,7 @@
|
||||
>
|
||||
<slot name="sidebar" />
|
||||
</nav>
|
||||
<main class="main">
|
||||
<main ref="mainEl" class="main">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed } from '@vue/composition-api';
|
||||
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
@@ -67,11 +67,21 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
formWidth: {
|
||||
// If the modal is used to just render a form, it needs to be a little smaller to
|
||||
// allow the form to be rendered in it's correct full size
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const sidebarActive = ref(false);
|
||||
const localActive = ref(false);
|
||||
|
||||
const mainEl = ref<Element>();
|
||||
|
||||
provide('main-element', mainEl);
|
||||
|
||||
const _active = computed({
|
||||
get() {
|
||||
return props.active === undefined ? localActive.value : props.active;
|
||||
@@ -82,11 +92,17 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
|
||||
return { sidebarActive, _active };
|
||||
return { sidebarActive, _active, mainEl };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
--v-modal-max-width: 916px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/breakpoint';
|
||||
|
||||
@@ -94,7 +110,7 @@ export default defineComponent({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: calc(100% - 16px);
|
||||
max-width: 916px;
|
||||
max-width: var(--v-modal-max-width);
|
||||
height: calc(100% - 16px);
|
||||
max-height: 760px;
|
||||
background-color: var(--background-page);
|
||||
@@ -209,4 +225,8 @@ export default defineComponent({
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
}
|
||||
|
||||
.form-width {
|
||||
--v-modal-max-width: 856px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
| `warning` | Shows the warning notice | `false` |
|
||||
| `danger` | Shows the danger notice | `false` |
|
||||
| `icon` | Custom icon name, or false if you want to hide the icon completely | `null` |
|
||||
| `center` | Render notice content centered | `false` |
|
||||
|
||||
## Slots
|
||||
| Slot | Description | Data |
|
||||
@@ -21,8 +22,8 @@
|
||||
n/a
|
||||
|
||||
## CSS Variables
|
||||
| Variable | Default |
|
||||
|-------------------------------|----------------------------|
|
||||
| `--v-notice-color` | `var(--foreground-normal);` |
|
||||
| `--v-notice-background-color` | `var(--primary-alt);` |
|
||||
| `--v-notice-icon-color` | `var(--primary);` |
|
||||
| Variable | Default |
|
||||
|-------------------------------|-----------------------------|
|
||||
| `--v-notice-color` | `var(--foreground-subdued)` |
|
||||
| `--v-notice-background-color` | `var(--background-subdued)` |
|
||||
| `--v-notice-icon-color` | `var(--foreground-subdued)` |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="v-notice" :class="className">
|
||||
<div class="v-notice" :class="[className, { center }]">
|
||||
<v-icon v-if="icon !== false" :name="iconName" left />
|
||||
<slot />
|
||||
</div>
|
||||
@@ -10,6 +10,10 @@ import { defineComponent, computed } from '@vue/composition-api';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
info: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
success: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -26,6 +30,10 @@ export default defineComponent({
|
||||
type: [String, Boolean],
|
||||
default: null,
|
||||
},
|
||||
center: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const iconName = computed(() => {
|
||||
@@ -33,7 +41,9 @@ export default defineComponent({
|
||||
return props.icon;
|
||||
}
|
||||
|
||||
if (props.success) {
|
||||
if (props.info) {
|
||||
return 'info';
|
||||
} else if (props.success) {
|
||||
return 'check_circle';
|
||||
} else if (props.warning) {
|
||||
return 'warning';
|
||||
@@ -45,7 +55,9 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const className = computed<string | null>(() => {
|
||||
if (props.success) {
|
||||
if (props.info) {
|
||||
return 'info';
|
||||
} else if (props.success) {
|
||||
return 'success';
|
||||
} else if (props.warning) {
|
||||
return 'warning';
|
||||
@@ -63,9 +75,9 @@ export default defineComponent({
|
||||
|
||||
<style>
|
||||
body {
|
||||
--v-notice-color: var(--primary);
|
||||
--v-notice-background-color: var(--primary-alt);
|
||||
--v-notice-icon-color: var(--primary);
|
||||
--v-notice-color: var(--foreground-subdued);
|
||||
--v-notice-background-color: var(--background-subdued);
|
||||
--v-notice-icon-color: var(--foreground-subdued);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,6 +97,12 @@ body {
|
||||
--v-icon-color: var(--v-notice-icon-color);
|
||||
}
|
||||
|
||||
&.info {
|
||||
--v-notice-icon-color: var(--primary);
|
||||
--v-notice-background-color: var(--primary-alt);
|
||||
--v-notice-color: var(--primary);
|
||||
}
|
||||
|
||||
&.success {
|
||||
--v-notice-icon-color: var(--success);
|
||||
--v-notice-background-color: var(--success-alt);
|
||||
@@ -102,5 +120,11 @@ body {
|
||||
--v-notice-background-color: var(--danger-alt);
|
||||
--v-notice-color: var(--danger);
|
||||
}
|
||||
|
||||
&.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -123,13 +123,16 @@ body {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--background-subdued);
|
||||
border-radius: var(--border-radius);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.label {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:disabled):hover {
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div :class="type" class="v-skeleton-loader">
|
||||
<template v-if="type === 'list-item-icon'">
|
||||
<div class="icon" />
|
||||
<div class="text" />
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
<div :class="type" class="v-skeleton-loader">
|
||||
<template v-if="type === 'list-item-icon'">
|
||||
<div class="icon" />
|
||||
<div class="text" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -214,8 +214,9 @@ body {
|
||||
left: calc(var(--_v-slider-percentage) * 1%);
|
||||
width: max-content;
|
||||
padding: 4px 8px;
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-normal);
|
||||
color: var(--foreground-inverted);
|
||||
font-weight: 600;
|
||||
background-color: var(--primary);
|
||||
border-radius: var(--border-radius);
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
@@ -227,8 +228,8 @@ body {
|
||||
left: calc(50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--primary);
|
||||
border-radius: 2px;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
content: '';
|
||||
}
|
||||
|
||||
@@ -123,10 +123,12 @@ export default defineComponent({
|
||||
| `selection` | What items are selected. Can be used with `v-model` as well | `[]` |
|
||||
| `fixed-header` | Make the header fixed | `false` |
|
||||
| `loading` | Show progress indicator | `false` |
|
||||
| `loadingText` | What text to show when table is loading with no items | `Loading...` |
|
||||
| `loading-text` | What text to show when table is loading with no items | `Loading...` |
|
||||
| `no-items-text` | What text to show when table doesn't contain any rows | `No items` |
|
||||
| `server-sort` | Handle sorting on the parent level. | `false` |
|
||||
| `row-height` | Height of the individual rows in px | `48` |
|
||||
| `must-sort` | Requires the sort to be on a particular column | `false` |
|
||||
| `disabled` | Disable edits to items in the form (drag/select) | `false` |
|
||||
|
||||
## Events
|
||||
| Event | Description | Value |
|
||||
|
||||
@@ -239,7 +239,7 @@ export default defineComponent({
|
||||
padding: 0 12px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
background-color: var(--background-page);
|
||||
background-color: var(--v-table-background-color);
|
||||
border-bottom: 2px solid var(--border-subdued);
|
||||
|
||||
&.select,
|
||||
|
||||
@@ -113,7 +113,7 @@ export default defineComponent({
|
||||
line-height: var(--table-row-line-height);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
background-color: var(--background-page);
|
||||
background-color: var(--v-table-background-color);
|
||||
border-bottom: 2px solid var(--border-subdued);
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="v-table" :class="{ loading, inline }">
|
||||
<div class="v-table" :class="{ loading, inline, disabled }">
|
||||
<table
|
||||
:summary="_headers.map((header) => header.text).join(', ')"
|
||||
:style="{
|
||||
@@ -25,13 +25,18 @@
|
||||
</template>
|
||||
</table-header>
|
||||
<thead v-if="loading" class="loading-indicator" :class="{ sticky: fixedHeader }">
|
||||
<th scope="colgroup" :style="{ gridColumn: loadingColSpan }">
|
||||
<th scope="colgroup" :style="{ gridColumn: fullColSpan }">
|
||||
<v-progress-linear indeterminate v-if="loading" />
|
||||
</th>
|
||||
</thead>
|
||||
<tbody v-if="loading && items.length === 0">
|
||||
<tr class="loading-text">
|
||||
<td :style="{ gridColumn: loadingColSpan }">{{ loadingText }}</td>
|
||||
<td :style="{ gridColumn: fullColSpan }">{{ loadingText }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-if="!loading && items.length === 0">
|
||||
<tr class="no-items-text">
|
||||
<td :style="{ gridColumn: fullColSpan }">{{ noItemsText }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<draggable
|
||||
@@ -39,20 +44,21 @@
|
||||
v-model="_items"
|
||||
tag="tbody"
|
||||
handle=".drag-handle"
|
||||
:disabled="_sort.by !== manualSortKey"
|
||||
:disabled="disabled || _sort.by !== manualSortKey"
|
||||
@change="onSortChange"
|
||||
:set-data="hideDragImage"
|
||||
>
|
||||
<table-row
|
||||
v-for="item in _items"
|
||||
:key="item[itemKey]"
|
||||
:headers="_headers"
|
||||
:item="item"
|
||||
:show-select="showSelect"
|
||||
:show-manual-sort="showManualSort"
|
||||
:show-select="!disabled && showSelect"
|
||||
:show-manual-sort="!disabled && showManualSort"
|
||||
:is-selected="getSelectedState(item)"
|
||||
:subdued="loading"
|
||||
:sorted-manually="_sort.by === manualSortKey"
|
||||
:has-click-listener="hasRowClick"
|
||||
:has-click-listener="!disabled && hasRowClick"
|
||||
:height="rowHeight"
|
||||
@click="hasRowClick ? $emit('click:row', item) : null"
|
||||
@item-selected="onItemSelected"
|
||||
@@ -79,6 +85,7 @@ import TableRow from './table-row/';
|
||||
import { sortBy, clone, forEach, pick } from 'lodash';
|
||||
import { i18n } from '@/lang/';
|
||||
import draggable from 'vuedraggable';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
|
||||
const HeaderDefaults: Header = {
|
||||
text: '',
|
||||
@@ -151,6 +158,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: i18n.t('loading'),
|
||||
},
|
||||
noItemsText: {
|
||||
type: String,
|
||||
default: i18n.t('no_items'),
|
||||
},
|
||||
serverSort: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -167,6 +178,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, { emit, listeners, slots }) {
|
||||
const _headers = computed({
|
||||
@@ -223,7 +238,7 @@ export default defineComponent({
|
||||
|
||||
const hasItemAppendSlot = computed(() => slots['item-append'] !== undefined);
|
||||
|
||||
const loadingColSpan = computed<string>(() => {
|
||||
const fullColSpan = computed<string>(() => {
|
||||
let length = _headers.value.length + 1; // +1 account for spacer
|
||||
if (props.showSelect) length++;
|
||||
if (props.showManualSort) length++;
|
||||
@@ -287,12 +302,15 @@ export default defineComponent({
|
||||
someItemsSelected,
|
||||
onSortChange,
|
||||
hasRowClick,
|
||||
loadingColSpan,
|
||||
fullColSpan,
|
||||
columnStyle,
|
||||
hasItemAppendSlot,
|
||||
hideDragImage,
|
||||
};
|
||||
|
||||
function onItemSelected(event: ItemSelectEvent) {
|
||||
if (props.disabled) return;
|
||||
|
||||
emit('item-selected', event);
|
||||
|
||||
let selection = clone(props.selection) as any[];
|
||||
@@ -324,6 +342,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function onToggleSelectAll(value: boolean) {
|
||||
if (props.disabled) return;
|
||||
|
||||
if (value === true) {
|
||||
if (props.selectionUseKeys) {
|
||||
emit(
|
||||
@@ -348,6 +368,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function onSortChange(event: VueDraggableChangeEvent) {
|
||||
if (props.disabled) return;
|
||||
|
||||
if (event.moved) {
|
||||
emit('manual-sort', {
|
||||
item: event.moved.element,
|
||||
@@ -364,6 +386,8 @@ export default defineComponent({
|
||||
body {
|
||||
--v-table-height: auto;
|
||||
--v-table-sticky-offset-top: 0;
|
||||
--v-table-color: var(--foreground-normal);
|
||||
--v-table-background-color: var(--background-page);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -399,7 +423,7 @@ body {
|
||||
|
||||
td,
|
||||
th {
|
||||
color: var(--foreground-normal);
|
||||
color: var(--v-table-color);
|
||||
|
||||
&.align-left {
|
||||
text-align: left;
|
||||
@@ -452,20 +476,30 @@ body {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
text-align: center;
|
||||
.loading-text,
|
||||
.no-items-text {
|
||||
text-align: center;
|
||||
|
||||
td {
|
||||
padding: 16px;
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
td {
|
||||
padding: 16px;
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
}
|
||||
|
||||
&.inline {
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
table ::v-deep .table-row:last-of-type .cell {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inline {
|
||||
border: 2px solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
.disabled {
|
||||
--v-table-color: var(--foreground-subdued);
|
||||
--v-table-background-color: var(--background-subdued);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -128,23 +128,22 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
&:focus:not(.disabled),
|
||||
&:focus-within:not(.disabled) {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--foreground-subdued);
|
||||
background-color: var(--background-subdued);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
||||
@@ -7,7 +7,7 @@ type Items = Readonly<
|
||||
Ref<
|
||||
| readonly {
|
||||
text: string;
|
||||
value: string;
|
||||
value: string | boolean | number;
|
||||
}[]
|
||||
| null
|
||||
>
|
||||
|
||||
@@ -107,7 +107,7 @@ export function useItems(collection: Ref<string>, query: Query) {
|
||||
}
|
||||
});
|
||||
|
||||
return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort };
|
||||
return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems };
|
||||
|
||||
async function getItems() {
|
||||
loading.value = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<span>{{ displayValue }}</span>
|
||||
<span class="datetime">{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -7,6 +7,7 @@ import { defineComponent, ref, watch } from '@vue/composition-api';
|
||||
import formatLocalized from '@/utils/localized-format';
|
||||
import i18n from '@/lang';
|
||||
import parse from 'date-fns/parse';
|
||||
import parseISO from 'date-fns/parseISO';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -30,15 +31,19 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
let format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
let date: Date;
|
||||
|
||||
if (newValue.includes('T')) {
|
||||
date = parseISO(props.value);
|
||||
} else {
|
||||
date = parse(props.value, 'yyyy-MM-dd HH:mm:ss', new Date());
|
||||
}
|
||||
|
||||
let format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
|
||||
displayValue.value = await formatLocalized(
|
||||
parse(props.value, 'yyyy-MM-dd HH:mm:ss', new Date()),
|
||||
format
|
||||
);
|
||||
displayValue.value = await formatLocalized(date, format);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -46,3 +51,11 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.datetime {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,6 +40,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.dot {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 4px;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,9 +130,22 @@
|
||||
"december": "December"
|
||||
},
|
||||
|
||||
"drag_mode": "Drag Mode",
|
||||
"cancel_crop": "Cancel Crop",
|
||||
|
||||
"many_to_many": "Many to Many (M2M)",
|
||||
"one_to_many": "One to Many (O2M)",
|
||||
"many_to_one": "Many to One (M2O)",
|
||||
"original": "Original",
|
||||
|
||||
"file_details": "File Details",
|
||||
"dimensions": "Dimensions",
|
||||
"size": "Size",
|
||||
"created": "Created",
|
||||
"modified": "Modified",
|
||||
"checksum": "Checksum",
|
||||
"owner": "Owner",
|
||||
"folder": "Folder",
|
||||
|
||||
"set_to_now": "Set to Now",
|
||||
|
||||
@@ -170,6 +183,12 @@
|
||||
|
||||
"select_item": "Select Item",
|
||||
|
||||
"no_items": "No items",
|
||||
|
||||
"disabled": "Disabled",
|
||||
|
||||
"interface_not_found": "Interface \"{interface}\" not found",
|
||||
|
||||
"item_count": "No Items | One Item | {count} Items",
|
||||
"no_items_copy": "It looks like you don’t have any items in this collection. You can click the button below to add an item.",
|
||||
"all_items": "All Items",
|
||||
@@ -185,7 +204,9 @@
|
||||
"relationship_not_setup": "The relationship hasn't been configured correctly",
|
||||
"display_template_not_setup": "The display template is misconfigured",
|
||||
|
||||
"choose_status": "Choose Status...",
|
||||
"active": "Active",
|
||||
|
||||
"choose_status": "Choose a status...",
|
||||
|
||||
"users": "Users",
|
||||
"files": "Files",
|
||||
@@ -265,7 +286,7 @@
|
||||
|
||||
"back": "Back",
|
||||
|
||||
"editing_image": "Editing Image...",
|
||||
"editing_image": "Editing Image",
|
||||
"square": "Square",
|
||||
"free": "Free",
|
||||
"flip_horizontal": "Flip Horizontal",
|
||||
@@ -507,6 +528,9 @@
|
||||
|
||||
"adding_in": "Adding New Item in {collection}",
|
||||
"editing_in": "Editing Item in {collection}",
|
||||
"editing_in_batch": "Batch Editing {count} Items",
|
||||
|
||||
"no_options_available": "No options available",
|
||||
|
||||
"settings_data_model": "Data Model",
|
||||
"settings_collections_fields": "Collections & Fields",
|
||||
@@ -709,6 +733,7 @@
|
||||
"extensions_missing": "No Extensions Found",
|
||||
"extensions_missing_copy": "Make sure you have the system extensions installed.",
|
||||
"extra_spacing": "Extra Spacing",
|
||||
"fallback_icon": "Fallback Icon",
|
||||
"fetching_data": "Fetching Data",
|
||||
"field": "Field | Fields",
|
||||
"field_already_exists": "Field Already Exists: {field}",
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
</div>
|
||||
|
||||
<div class="layout-option">
|
||||
<div class="option-label">{{ $t('layouts.cards.fallback_icon') }}</div>
|
||||
<v-input v-model="icon" />
|
||||
<div class="option-label">{{ $t('fallback_icon') }}</div>
|
||||
<interface-icon v-model="icon" />
|
||||
</div>
|
||||
</v-detail>
|
||||
</portal>
|
||||
@@ -70,12 +70,13 @@
|
||||
|
||||
<div class="grid" :class="{ 'single-row': isSingleRow }">
|
||||
<template v-if="loading">
|
||||
<card v-for="n in limit" :key="`loader-${n}`" loading />
|
||||
<card v-for="n in limit" :key="`loader-${n}`" item-key="loading" loading />
|
||||
</template>
|
||||
|
||||
<card
|
||||
v-else
|
||||
v-for="item in items"
|
||||
:item-key="primaryKeyField.field"
|
||||
:key="item[primaryKeyField.field]"
|
||||
:crop="imageFit === 'crop'"
|
||||
:icon="icon"
|
||||
@@ -123,7 +124,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-info v-else-if="itemCount === 0" :title="$t('no_results')" icon="search">
|
||||
<v-info
|
||||
v-else-if="itemCount === 0 && activeFilterCount > 0"
|
||||
:title="$t('no_results')"
|
||||
icon="search"
|
||||
center
|
||||
>
|
||||
{{ $t('no_results_copy') }}
|
||||
|
||||
<template #append>
|
||||
@@ -131,7 +137,7 @@
|
||||
</template>
|
||||
</v-info>
|
||||
|
||||
<v-info v-else :title="$tc('item_count', 0)" :icon="info.icon">
|
||||
<v-info v-else :title="$tc('item_count', 0)" :icon="info.icon" center>
|
||||
{{ $t('no_items_copy') }}
|
||||
|
||||
<template #append>
|
||||
@@ -242,7 +248,7 @@ export default defineComponent({
|
||||
const { size, icon, imageSource, title, subtitle, imageFit } = useViewOptions();
|
||||
const { sort, limit, page, fields } = useViewQuery();
|
||||
|
||||
const { items, loading, error, totalPages, itemCount } = useItems(collection, {
|
||||
const { items, loading, error, totalPages, itemCount, getItems } = useItems(collection, {
|
||||
sort,
|
||||
limit,
|
||||
page,
|
||||
@@ -276,6 +282,10 @@ export default defineComponent({
|
||||
return cardsWidth <= width.value;
|
||||
});
|
||||
|
||||
const activeFilterCount = computed(() => {
|
||||
return _filters.value.filter((filter) => !filter.locked);
|
||||
});
|
||||
|
||||
return {
|
||||
_selection,
|
||||
items,
|
||||
@@ -306,8 +316,14 @@ export default defineComponent({
|
||||
isSingleRow,
|
||||
width,
|
||||
layoutElement,
|
||||
activeFilterCount,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
getItems();
|
||||
}
|
||||
|
||||
function toPage(newPage: number) {
|
||||
page.value = newPage;
|
||||
mainElement.value?.scrollTo({
|
||||
@@ -438,10 +454,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="card" :class="{ loading, readonly }" @click="handleClick">
|
||||
<div class="header" :class="{ selected: value.includes(item) }">
|
||||
<div class="header" :class="{ selected: item && value.includes(item[itemKey]) }">
|
||||
<div class="selection-indicator" :class="{ 'select-mode': selectMode }">
|
||||
<v-icon class="selector" :name="selectionIcon" @click.stop="toggleSelection" />
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export default defineComponent({
|
||||
default: null,
|
||||
},
|
||||
value: {
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
type: Array as PropType<(string | number)[]>,
|
||||
default: () => [],
|
||||
},
|
||||
selectMode: {
|
||||
@@ -89,6 +89,10 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
itemKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const type = computed(() => {
|
||||
@@ -123,20 +127,26 @@ export default defineComponent({
|
||||
return props.file.data.full_url;
|
||||
});
|
||||
|
||||
const selectionIcon = computed(() =>
|
||||
props.value.includes(props.item) ? 'check_circle' : 'radio_button_unchecked'
|
||||
);
|
||||
const selectionIcon = computed(() => {
|
||||
if (!props.item) return 'radio_button_unchecked';
|
||||
|
||||
return props.value.includes(props.item[props.itemKey])
|
||||
? 'check_circle'
|
||||
: 'radio_button_unchecked';
|
||||
});
|
||||
|
||||
return { imageSource, svgSource, type, selectionIcon, toggleSelection, handleClick };
|
||||
|
||||
function toggleSelection() {
|
||||
if (props.value.includes(props.item)) {
|
||||
if (!props.item) return null;
|
||||
|
||||
if (props.value.includes(props.item[props.itemKey])) {
|
||||
emit(
|
||||
'input',
|
||||
props.value.filter((item) => item !== props.item)
|
||||
props.value.filter((key) => key !== props.item[props.itemKey])
|
||||
);
|
||||
} else {
|
||||
emit('input', [...props.value, props.item]);
|
||||
emit('input', [...props.value, props.item[props.itemKey]]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,20 +244,6 @@ export default defineComponent({
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(
|
||||
-180deg,
|
||||
rgba(38, 50, 56, 0.2) 10%,
|
||||
rgba(38, 50, 56, 0)
|
||||
);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@@ -275,24 +271,17 @@ export default defineComponent({
|
||||
.title,
|
||||
.subtitle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
overflow: hidden;
|
||||
line-height: 1.3em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--background-page-rgb), 0) 0%,
|
||||
rgba(var(--background-page-rgb), 1) 100%
|
||||
);
|
||||
content: '';
|
||||
|
||||
> .render-template ::v-deep > *:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div class="layout-option">
|
||||
<div class="option-label">{{ $t('layouts.tabular.fields') }}</div>
|
||||
<draggable v-model="activeFields" handle=".drag-handle">
|
||||
<draggable v-model="activeFields" handle=".drag-handle" :set-data="hideDragImage">
|
||||
<v-checkbox
|
||||
v-for="field in activeFields"
|
||||
v-model="fields"
|
||||
@@ -86,11 +86,7 @@
|
||||
@manual-sort="changeManualSort"
|
||||
>
|
||||
<template v-for="header in tableHeaders" v-slot:[`item.${header.value}`]="{ item }">
|
||||
<span :key="header.value" v-if="!header.field.display">
|
||||
{{ item[header.value] }}
|
||||
</span>
|
||||
<render-display
|
||||
v-else
|
||||
:key="header.value"
|
||||
:value="item[header.value]"
|
||||
:display="header.field.display"
|
||||
@@ -130,9 +126,10 @@
|
||||
</v-table>
|
||||
|
||||
<v-info
|
||||
v-else-if="itemCount === 0 && _filters.length > 0"
|
||||
v-else-if="itemCount === 0 && activeFilterCount > 0"
|
||||
:title="$t('no_results')"
|
||||
icon="search"
|
||||
center
|
||||
>
|
||||
{{ $t('no_results_copy') }}
|
||||
|
||||
@@ -141,7 +138,7 @@
|
||||
</template>
|
||||
</v-info>
|
||||
|
||||
<v-info v-else :title="$tc('item_count', 0)" :icon="info.icon">
|
||||
<v-info v-else :title="$tc('item_count', 0)" :icon="info.icon" center>
|
||||
{{ $t('no_items_copy') }}
|
||||
|
||||
<template #append>
|
||||
@@ -175,6 +172,7 @@ import { render } from 'micromustache';
|
||||
import { Filter } from '@/stores/collection-presets/types';
|
||||
import i18n from '@/lang';
|
||||
import adjustFieldsForDisplays from '@/utils/adjust-fields-for-displays';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
|
||||
type ViewOptions = {
|
||||
widths?: {
|
||||
@@ -252,17 +250,22 @@ export default defineComponent({
|
||||
|
||||
const { sort, limit, page, fields, fieldsWithRelational } = useItemOptions();
|
||||
|
||||
const { items, loading, error, totalPages, itemCount, changeManualSort } = useItems(
|
||||
collection,
|
||||
{
|
||||
sort,
|
||||
limit,
|
||||
page,
|
||||
fields: fieldsWithRelational,
|
||||
filters: _filters,
|
||||
searchQuery,
|
||||
}
|
||||
);
|
||||
const {
|
||||
items,
|
||||
loading,
|
||||
error,
|
||||
totalPages,
|
||||
itemCount,
|
||||
changeManualSort,
|
||||
getItems,
|
||||
} = useItems(collection, {
|
||||
sort,
|
||||
limit,
|
||||
page,
|
||||
fields: fieldsWithRelational,
|
||||
filters: _filters,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
const {
|
||||
tableSort,
|
||||
@@ -291,6 +294,10 @@ export default defineComponent({
|
||||
});
|
||||
});
|
||||
|
||||
const activeFilterCount = computed(() => {
|
||||
return _filters.value.filter((filter) => !filter.locked);
|
||||
});
|
||||
|
||||
return {
|
||||
_selection,
|
||||
table,
|
||||
@@ -319,8 +326,15 @@ export default defineComponent({
|
||||
showingCount,
|
||||
sortField,
|
||||
changeManualSort,
|
||||
hideDragImage,
|
||||
activeFilterCount,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
getItems();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
_filters.value = [];
|
||||
_searchQuery.value = null;
|
||||
@@ -580,10 +594,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
|
||||
.v-checkbox {
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
v-if="bookmark && bookmarkExists === false"
|
||||
:title="$t('bookmark_doesnt_exist')"
|
||||
icon="bookmark"
|
||||
center
|
||||
>
|
||||
{{ $t('bookmark_doesnt_exist_copy') }}
|
||||
|
||||
@@ -194,7 +195,7 @@ export default defineComponent({
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const { selection } = useSelection();
|
||||
const { info: currentCollection, primaryKeyField } = useCollection(collection);
|
||||
const { info: currentCollection } = useCollection(collection);
|
||||
const { addNewLink, batchLink, collectionsLink, currentCollectionLink } = useLinks();
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
const {
|
||||
@@ -254,7 +255,7 @@ export default defineComponent({
|
||||
function useBreadcrumb() {
|
||||
const breadcrumb = computed(() => [
|
||||
{
|
||||
name: props.collection,
|
||||
name: currentCollection.value?.name,
|
||||
to: `/${projectsStore.state.currentProjectKey}/collections/${props.collection}`,
|
||||
},
|
||||
]);
|
||||
@@ -287,20 +288,22 @@ export default defineComponent({
|
||||
|
||||
confirmDelete.value = false;
|
||||
|
||||
const batchPrimaryKeys = selection.value
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.map((item) => item[primaryKeyField.value!.field])
|
||||
.join();
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
await api.delete(
|
||||
`/${currentProjectKey}/items/${props.collection}/${batchPrimaryKeys}`
|
||||
);
|
||||
try {
|
||||
await api.delete(
|
||||
`/${currentProjectKey}/items/${props.collection}/${batchPrimaryKeys}`
|
||||
);
|
||||
|
||||
await layout.value?.refresh();
|
||||
await layout.value?.refresh?.();
|
||||
|
||||
selection.value = [];
|
||||
deleting.value = false;
|
||||
confirmDelete.value = false;
|
||||
selection.value = [];
|
||||
confirmDelete.value = false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,10 +400,6 @@ export default defineComponent({
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
|
||||
.bookmark-add .toggle,
|
||||
.bookmark-edit .toggle {
|
||||
margin-left: 8px;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<template>
|
||||
<collections-not-found v-if="error && error.code === 404" />
|
||||
<private-view
|
||||
v-else
|
||||
:title="
|
||||
isNew
|
||||
? $t('adding_in', { collection: collectionInfo.name })
|
||||
: $t('editing_in', { collection: collectionInfo.name })
|
||||
"
|
||||
>
|
||||
<template #title v-if="isNew === false && collectionInfo.display_template">
|
||||
<private-view v-else :title="title">
|
||||
<template
|
||||
#title
|
||||
v-if="isNew === false && isBatch === false && collectionInfo.display_template"
|
||||
>
|
||||
<v-skeleton-loader class="title-loader" type="text" v-if="loading" />
|
||||
<h1 class="type-title" v-else>
|
||||
<render-template
|
||||
@@ -151,6 +147,7 @@
|
||||
v-if="isBatch === false && isNew === false"
|
||||
:collection="collection"
|
||||
:primary-key="primaryKey"
|
||||
ref="revisionsDrawerDetail"
|
||||
/>
|
||||
<comments-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
@@ -172,6 +169,7 @@ import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-d
|
||||
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -202,6 +200,8 @@ export default defineComponent({
|
||||
const { collection, primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue>(null);
|
||||
|
||||
const { info: collectionInfo, softDeleteStatus, primaryKeyField } = useCollection(
|
||||
collection
|
||||
);
|
||||
@@ -237,6 +237,17 @@ export default defineComponent({
|
||||
};
|
||||
});
|
||||
|
||||
const title = computed(() => {
|
||||
if (isBatch.value) {
|
||||
const itemCount = props.primaryKey.split(',').length;
|
||||
return i18n.t('editing_in_batch', { count: itemCount });
|
||||
}
|
||||
|
||||
return isNew.value
|
||||
? i18n.t('adding_in', { collection: collectionInfo.value?.name })
|
||||
: i18n.t('editing_in', { collection: collectionInfo.value?.name });
|
||||
});
|
||||
|
||||
return {
|
||||
item,
|
||||
loading,
|
||||
@@ -260,6 +271,8 @@ export default defineComponent({
|
||||
softDeleteStatus,
|
||||
templateValues,
|
||||
breadcrumb,
|
||||
title,
|
||||
revisionsDrawerDetail,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
@@ -281,6 +294,8 @@ export default defineComponent({
|
||||
async function saveAndStay() {
|
||||
const savedItem: Record<string, any> = await save();
|
||||
|
||||
revisionsDrawerDetail.value?.$data?.refresh?.();
|
||||
|
||||
if (props.primaryKey === '+') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const newPrimaryKey = savedItem[primaryKeyField.value!.field];
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
</v-table>
|
||||
|
||||
<v-info icon="box" :title="$t('no_collections')" v-else>
|
||||
<v-info icon="box" :title="$t('no_collections')" v-else center>
|
||||
<template v-if="isAdmin">
|
||||
{{ $t('no_collections_copy_admin') }}
|
||||
</template>
|
||||
@@ -106,8 +106,4 @@ export default defineComponent({
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default defineComponent({
|
||||
|
||||
confirmDelete.value = false;
|
||||
|
||||
const batchPrimaryKeys = selection.value.map((item) => item.id).join();
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
await api.delete(`/${currentProjectKey}/files/${batchPrimaryKeys}`);
|
||||
|
||||
@@ -199,7 +199,7 @@ export default defineComponent({
|
||||
|
||||
const batchLink = computed<string>(() => {
|
||||
const currentProjectKey = projectsStore.state.currentProjectKey;
|
||||
const batchPrimaryKeys = selection.value.map((item) => item.id).join();
|
||||
const batchPrimaryKeys = selection.value;
|
||||
return `/${currentProjectKey}/files/${batchPrimaryKeys}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<drawer-detail icon="info_outline" :title="$t('file_details')">
|
||||
<dl>
|
||||
<div v-if="type">
|
||||
<dt>{{ $t('type') }}</dt>
|
||||
<dd>{{ readableMimeType(type) || type }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="width && height">
|
||||
<dt>{{ $t('dimensions') }}</dt>
|
||||
<dd>{{ $n(width) }} × {{ $n(height) }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="filesize">
|
||||
<dt>{{ $t('size') }}</dt>
|
||||
<dd>{{ size }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="creationDate">
|
||||
<dt>{{ $t('created') }}</dt>
|
||||
<dd>{{ creationDate }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="checksum" class="checksum">
|
||||
<dt>{{ $t('checksum') }}</dt>
|
||||
<dd>{{ checksum }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</drawer-detail>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import readableMimeType from '@/utils/readable-mime-type';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import i18n from '@/lang';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
filesize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
uploaded_on: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
checksum: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const size = computed(() => {
|
||||
if (!props.filesize) return null;
|
||||
|
||||
return prettyBytes(props.filesize, { locale: i18n.locale.split('-')[0] });
|
||||
});
|
||||
|
||||
const creationDate = ref<string>(null);
|
||||
|
||||
localizedFormat(new Date(props.uploaded_on), String(i18n.t('date-fns_datetime'))).then(
|
||||
(result) => {
|
||||
creationDate.value = result;
|
||||
}
|
||||
);
|
||||
|
||||
return { readableMimeType, size, creationDate };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
dl > div {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin-right: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
dd {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
color: var(--foreground-subdued);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.checksum {
|
||||
dd {
|
||||
font-family: var(--family-monospace);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="loading ? $t('loading') : item.title">
|
||||
<private-view :title="loading || !item ? $t('loading') : item.title">
|
||||
<template #title-outer:prepend>
|
||||
<v-button class="header-icon" rounded icon secondary exact :to="breadcrumb[0].to">
|
||||
<v-icon name="arrow_back" />
|
||||
@@ -83,7 +83,7 @@
|
||||
@click="previewActive = true"
|
||||
/>
|
||||
|
||||
<file-lightbox :id="item.id" v-model="previewActive" />
|
||||
<file-lightbox v-if="item" :id="item.id" v-model="previewActive" />
|
||||
|
||||
<image-editor
|
||||
v-if="item && item.type.startsWith('image')"
|
||||
@@ -93,9 +93,9 @@
|
||||
/>
|
||||
|
||||
<v-form
|
||||
:fields="formFields"
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
collection="directus_files"
|
||||
:batch-mode="isBatch"
|
||||
:primary-key="primaryKey"
|
||||
v-model="edits"
|
||||
@@ -103,10 +103,17 @@
|
||||
</div>
|
||||
|
||||
<template #drawer>
|
||||
<file-info-drawer-detail v-if="isNew === false" v-bind="item" />
|
||||
<revisions-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_files"
|
||||
:primary-key="primaryKey"
|
||||
ref="revisionsDrawerDetail"
|
||||
/>
|
||||
<comments-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_files"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
</template>
|
||||
</private-view>
|
||||
@@ -119,12 +126,16 @@ import FilesNavigation from '../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
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';
|
||||
import useFieldsStore from '@/stores/fields';
|
||||
import { Field } from '@/stores/fields/types';
|
||||
import FileInfoDrawerDetail from './components/file-info-drawer-detail.vue';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -135,10 +146,12 @@ export default defineComponent({
|
||||
components: {
|
||||
FilesNavigation,
|
||||
RevisionsDrawerDetail,
|
||||
CommentsDrawerDetail,
|
||||
SaveOptions,
|
||||
FilePreview,
|
||||
ImageEditor,
|
||||
FileLightbox,
|
||||
FileInfoDrawerDetail,
|
||||
},
|
||||
props: {
|
||||
primaryKey: {
|
||||
@@ -151,6 +164,9 @@ export default defineComponent({
|
||||
const { currentProjectKey } = toRefs(projectsStore.state);
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
const fieldsStore = useFieldsStore();
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue>(null);
|
||||
|
||||
const {
|
||||
isNew,
|
||||
@@ -167,15 +183,20 @@ export default defineComponent({
|
||||
} = useItem(ref('directus_files'), primaryKey);
|
||||
|
||||
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
|
||||
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
const cacheBuster = ref(nanoid());
|
||||
|
||||
const editActive = ref(false);
|
||||
|
||||
const previewActive = ref(false);
|
||||
|
||||
// These are the fields that will be prevented from showing up in the form
|
||||
const fieldsBlacklist: string[] = ['type', 'width', 'height', 'filesize', 'checksum'];
|
||||
|
||||
const formFields = computed(() => {
|
||||
return fieldsStore
|
||||
.getFieldsForCollection('directus_files')
|
||||
.filter((field: Field) => fieldsBlacklist.includes(field.field) === false);
|
||||
});
|
||||
|
||||
return {
|
||||
item,
|
||||
loading,
|
||||
@@ -197,6 +218,8 @@ export default defineComponent({
|
||||
cacheBuster,
|
||||
editActive,
|
||||
previewActive,
|
||||
revisionsDrawerDetail,
|
||||
formFields,
|
||||
};
|
||||
|
||||
function changeCacheBuster() {
|
||||
@@ -222,6 +245,8 @@ export default defineComponent({
|
||||
async function saveAndStay() {
|
||||
const savedItem: Record<string, any> = await save();
|
||||
|
||||
revisionsDrawerDetail.value?.$data?.refresh?.();
|
||||
|
||||
if (props.primaryKey === '+') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const newPrimaryKey = savedItem.id;
|
||||
|
||||
@@ -2,10 +2,10 @@ import { RouteConfig } from 'vue-router';
|
||||
import { replaceRoutes } from '@/router';
|
||||
import modules from './index';
|
||||
|
||||
const moduleRoutes: RouteConfig[] = modules
|
||||
const moduleRoutes = modules
|
||||
.map((module) => module.routes)
|
||||
.filter((r) => r)
|
||||
.flat();
|
||||
.flat() as RouteConfig[];
|
||||
|
||||
replaceRoutes((routes) => insertBeforeProjectWildcard(routes, moduleRoutes));
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
icon="box"
|
||||
:title="$t('no_collections')"
|
||||
v-if="items.length === 0"
|
||||
center
|
||||
>
|
||||
{{ $t('no_collections_copy_admin') }}
|
||||
|
||||
@@ -57,7 +58,7 @@
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #item.collection="{ item }">
|
||||
<template #item.name="{ item }">
|
||||
<span
|
||||
class="collection"
|
||||
:class="{
|
||||
@@ -249,10 +250,6 @@ export default defineComponent({
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
--v-button-color-disabled: var(--warning);
|
||||
--v-button-background-color-disabled: var(--warning-25);
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
:edits="value.options"
|
||||
@input="emitValue('options', $event)"
|
||||
/>
|
||||
|
||||
<v-notice v-else>
|
||||
{{ $t('no_options_available') }}
|
||||
</v-notice>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
:edits="value.options"
|
||||
@input="emitValue('options', $event)"
|
||||
/>
|
||||
|
||||
<v-notice v-else>
|
||||
{{ $t('no_options_available') }}
|
||||
</v-notice>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -216,6 +216,10 @@ export default defineComponent({
|
||||
if (field.value.id === null) {
|
||||
await fieldsStore.createField(props.collection, field.value);
|
||||
} else {
|
||||
if (field.value.hasOwnProperty('name')) {
|
||||
delete field.value.name;
|
||||
}
|
||||
|
||||
await fieldsStore.updateField(
|
||||
props.existingField.collection,
|
||||
props.existingField.field,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
handle=".drag-handle"
|
||||
group="fields"
|
||||
@change="($event) => handleChange($event, 'visible')"
|
||||
:set-data="hideDragImage"
|
||||
>
|
||||
<template #header>
|
||||
<div class="group-name">Visible Fields</div>
|
||||
@@ -33,6 +34,7 @@
|
||||
:value="sortedHiddenFields"
|
||||
handle=".drag-handle"
|
||||
group="fields"
|
||||
:set-data="hideDragImage"
|
||||
@change="($event) => handleChange($event, 'hidden')"
|
||||
>
|
||||
<template #header>
|
||||
@@ -74,6 +76,7 @@ import useFieldsStore from '@/stores/fields/';
|
||||
import FieldSelect from '../field-select/';
|
||||
import FieldSetup from '../field-setup/';
|
||||
import { sortBy } from 'lodash';
|
||||
import hideDragImage from '@/utils/hide-drag-image';
|
||||
|
||||
type DraggableEvent = {
|
||||
moved?: {
|
||||
@@ -125,6 +128,7 @@ export default defineComponent({
|
||||
editingField,
|
||||
openFieldSetup,
|
||||
closeFieldSetup,
|
||||
hideDragImage,
|
||||
};
|
||||
|
||||
function handleChange(event: DraggableEvent, location: 'visible' | 'hidden') {
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
|
||||
<div class="presets-browse">
|
||||
<v-info
|
||||
center
|
||||
type="warning"
|
||||
v-if="presets.length === 0"
|
||||
:title="$t('no_presets')"
|
||||
@@ -314,8 +315,4 @@ export default defineComponent({
|
||||
.default {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.v-info {
|
||||
margin: 20vh 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -466,7 +466,7 @@ export default defineComponent({
|
||||
field: 'divider',
|
||||
name: i18n.t('divider'),
|
||||
interface: 'divider',
|
||||
width: 'full',
|
||||
width: 'fill',
|
||||
options: {
|
||||
title: i18n.t('layout_preview'),
|
||||
},
|
||||
|
||||
@@ -84,11 +84,29 @@ export default defineComponent({
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const { viewOptions, viewQuery } = useCollectionPreset(ref('directus_webhooks'));
|
||||
const { viewType, viewOptions, viewQuery } = useCollectionPreset(ref('directus_webhooks'));
|
||||
const { addNewLink, batchLink } = useLinks();
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
if (viewType.value === null) {
|
||||
viewType.value = 'tabular';
|
||||
}
|
||||
|
||||
if (viewOptions.value === null && viewType.value === 'tabular') {
|
||||
viewOptions.value = {
|
||||
widths: {
|
||||
status: 50,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (viewQuery.value === null && viewType.value === 'tabular') {
|
||||
viewQuery.value = {
|
||||
fields: ['status', 'http_action', 'url'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
addNewLink,
|
||||
batchLink,
|
||||
|
||||
@@ -177,7 +177,7 @@ export default defineComponent({
|
||||
|
||||
confirmDelete.value = false;
|
||||
|
||||
const batchPrimaryKeys = selection.value.map((item) => item.id).join();
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
await api.delete(`/${currentProjectKey}/users/${batchPrimaryKeys}`);
|
||||
|
||||
@@ -197,7 +197,7 @@ export default defineComponent({
|
||||
|
||||
const batchLink = computed<string>(() => {
|
||||
const currentProjectKey = projectsStore.state.currentProjectKey;
|
||||
const batchPrimaryKeys = selection.value.map((item) => item.id).join();
|
||||
const batchPrimaryKeys = selection.value;
|
||||
return `/${currentProjectKey}/users/${batchPrimaryKeys}`;
|
||||
});
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_users"
|
||||
:primary-key="primaryKey"
|
||||
ref="revisionsDrawerDetail"
|
||||
/>
|
||||
<comments-drawer-detail
|
||||
v-if="isBatch === false && isNew === false"
|
||||
collection="directus_users"
|
||||
:primary-key="primaryKey"
|
||||
/>
|
||||
</template>
|
||||
</private-view>
|
||||
@@ -88,6 +94,7 @@ import UsersNavigation from '../../components/navigation/';
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import CommentsDrawerDetail from '@/views/private/components/comments-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import SaveOptions from '@/views/private/components/save-options';
|
||||
|
||||
@@ -97,7 +104,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-detail',
|
||||
components: { UsersNavigation, RevisionsDrawerDetail, SaveOptions },
|
||||
components: { UsersNavigation, RevisionsDrawerDetail, SaveOptions, CommentsDrawerDetail },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -110,6 +117,8 @@ export default defineComponent({
|
||||
const { primaryKey } = toRefs(props);
|
||||
const { breadcrumb } = useBreadcrumb();
|
||||
|
||||
const revisionsDrawerDetail = ref<Vue>(null);
|
||||
|
||||
const {
|
||||
isNew,
|
||||
edits,
|
||||
@@ -145,6 +154,7 @@ export default defineComponent({
|
||||
saveAndAddNew,
|
||||
saveAsCopyAndNavigate,
|
||||
isBatch,
|
||||
revisionsDrawerDetail,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
@@ -166,6 +176,8 @@ export default defineComponent({
|
||||
async function saveAndStay() {
|
||||
const savedItem: Record<string, any> = await save();
|
||||
|
||||
revisionsDrawerDetail.value?.$data?.refresh?.();
|
||||
|
||||
if (props.primaryKey === '+') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const newPrimaryKey = savedItem.id;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createStore } from 'pinia';
|
||||
export const useAppStore = createStore({
|
||||
id: 'appStore',
|
||||
state: () => ({
|
||||
drawerOpen: false,
|
||||
hydrated: false,
|
||||
hydrating: false,
|
||||
error: null,
|
||||
|
||||
@@ -55,6 +55,14 @@ textarea,
|
||||
[contenteditable],
|
||||
.selectable {
|
||||
user-select: text;
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* stylelint-enable no-descending-specificity */
|
||||
}
|
||||
|
||||
:invalid {
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
display: none;
|
||||
max-width: 260px;
|
||||
padding: 4px 8px;
|
||||
color: var(--foreground-inverted);
|
||||
background-color: var(--background-inverted);
|
||||
color: var(--tooltip-foreground-color);
|
||||
background-color: var(--tooltip-background-color);
|
||||
border-radius: 4px;
|
||||
transition: opacity 200ms;
|
||||
|
||||
|
||||
4
src/utils/hide-drag-image/hide-drag-image.ts
Normal file
4
src/utils/hide-drag-image/hide-drag-image.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default function hideDragImage(dataTransfer: DataTransfer) {
|
||||
const emptyImg = new Image();
|
||||
dataTransfer.setDragImage(emptyImg, 0, 0);
|
||||
}
|
||||
4
src/utils/hide-drag-image/index.ts
Normal file
4
src/utils/hide-drag-image/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import hideDragImage from './hide-drag-image';
|
||||
|
||||
export { hideDragImage };
|
||||
export default hideDragImage;
|
||||
@@ -1,4 +0,0 @@
|
||||
import parseChoices from './parse-choices';
|
||||
|
||||
export { parseChoices };
|
||||
export default parseChoices;
|
||||
@@ -1,42 +0,0 @@
|
||||
import parseChoices from './parse-choices';
|
||||
|
||||
describe('Utils / Parse Choices', () => {
|
||||
it('Filters out empty rows', () => {
|
||||
const choices = `
|
||||
test
|
||||
|
||||
above is gone
|
||||
`;
|
||||
|
||||
const result = parseChoices(choices);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]).toEqual({ text: 'test', value: 'test' });
|
||||
});
|
||||
|
||||
it('Filters out whitespace around options', () => {
|
||||
const choices = ' bunch of whitespace ';
|
||||
|
||||
const result = parseChoices(choices);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0]).toEqual({ text: 'bunch of whitespace', value: 'bunch of whitespace' });
|
||||
});
|
||||
|
||||
it('Separates on double colon to form key/value pairs', () => {
|
||||
const choices = `
|
||||
value::Text
|
||||
`;
|
||||
const result = parseChoices(choices);
|
||||
expect(result[0]).toEqual({ text: 'Text', value: 'value' });
|
||||
});
|
||||
|
||||
it('Trims whitespace around colons', () => {
|
||||
const choices = `
|
||||
works :: Yes!
|
||||
`;
|
||||
|
||||
const result = parseChoices(choices);
|
||||
expect(result[0]).toEqual({ text: 'Yes!', value: 'works' });
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
export default function parseChoices(choices: string) {
|
||||
return choices
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((r) => r.length !== 0)
|
||||
.map((row) => {
|
||||
const parts = row.split('::').map((part) => part.trim());
|
||||
|
||||
if (parts.length > 1) {
|
||||
return {
|
||||
value: parts[0],
|
||||
text: parts[1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: parts[0],
|
||||
text: parts[0],
|
||||
};
|
||||
});
|
||||
}
|
||||
4
src/utils/readable-mime-type/index.ts
Normal file
4
src/utils/readable-mime-type/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import readableMimeType from './readable-mime-type';
|
||||
|
||||
export { readableMimeType };
|
||||
export default readableMimeType;
|
||||
5
src/utils/readable-mime-type/readable-mime-type.ts
Normal file
5
src/utils/readable-mime-type/readable-mime-type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import types from './types.json';
|
||||
|
||||
export default function readableMimeType(type: string) {
|
||||
return (types as any)[type] || null;
|
||||
}
|
||||
693
src/utils/readable-mime-type/types.json
Normal file
693
src/utils/readable-mime-type/types.json
Normal file
@@ -0,0 +1,693 @@
|
||||
{
|
||||
"application/vnd.hzn-3d-crossword": "3D Crossword Plugin",
|
||||
"video/3gpp": "3GP",
|
||||
"video/3gpp2": "3GP2",
|
||||
"application/vnd.mseq": "3GPP MSEQ File",
|
||||
"application/vnd.3m.post-it-notes": "3M Post It Notes",
|
||||
"application/vnd.3gpp.pic-bw-large": "3rd Generation Partnership Project - Pic Large",
|
||||
"application/vnd.3gpp.pic-bw-small": "3rd Generation Partnership Project - Pic Small",
|
||||
"application/vnd.3gpp.pic-bw-var": "3rd Generation Partnership Project - Pic Var",
|
||||
"application/vnd.3gpp2.tcap": "3rd Generation Partnership Project - Transaction Capabilities Application Part",
|
||||
"application/x-7z-compressed": "7-Zip",
|
||||
"application/x-abiword": "AbiWord",
|
||||
"application/x-ace-compressed": "Ace Archive",
|
||||
"application/vnd.americandynamics.acc": "Active Content Compression",
|
||||
"application/vnd.acucobol": "ACU Cobol",
|
||||
"application/vnd.acucorp": "ACU Cobol",
|
||||
"audio/adpcm": "Adaptive differential pulse-code modulation",
|
||||
"application/x-authorware-bin": "Adobe (Macropedia) Authorware - Binary File",
|
||||
"application/x-authorware-map": "Adobe (Macropedia) Authorware - Map",
|
||||
"application/x-authorware-seg": "Adobe (Macropedia) Authorware - Segment File",
|
||||
"application/vnd.adobe.air-application-installer-package+zip": "Adobe AIR Application",
|
||||
"application/x-shockwave-flash": "Adobe Flash",
|
||||
"application/vnd.adobe.fxp": "Adobe Flex Project",
|
||||
"application/pdf": "Adobe Portable Document Format",
|
||||
"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",
|
||||
"application/vnd.adobe.xfdf": "Adobe XML Forms Data Format",
|
||||
"audio/x-aac": "Advanced Audio Coding (AAC)",
|
||||
"application/vnd.ahead.space": "Ahead AIR Application",
|
||||
"application/vnd.airzip.filesecure.azf": "AirZip FileSECURE",
|
||||
"application/vnd.airzip.filesecure.azs": "AirZip FileSECURE",
|
||||
"application/vnd.amazon.ebook": "Amazon Kindle eBook format",
|
||||
"application/vnd.amiga.ami": "AmigaDE",
|
||||
"application/andrew-inset": "Andrew Toolkit",
|
||||
"application/vnd.android.package-archive": "Android Package Archive",
|
||||
"application/vnd.anser-web-certificate-issue-initiation": "ANSER-WEB Terminal Client - Certificate Issue",
|
||||
"application/vnd.anser-web-funds-transfer-initiation": "ANSER-WEB Terminal Client - Web Funds Transfer",
|
||||
"application/vnd.antix.game-component": "Antix Game Player",
|
||||
"application/x-apple-diskimage": "Apple Disk Image",
|
||||
"application/vnd.apple.installer+xml": "Apple Installer Package",
|
||||
"application/applixware": "Applixware",
|
||||
"application/vnd.hhe.lesson-player": "Archipelago Lesson Player",
|
||||
"application/vnd.aristanetworks.swi": "Arista Networks Software Image",
|
||||
"text/x-asm": "Assembler Source File",
|
||||
"application/atomcat+xml": "Atom Publishing Protocol",
|
||||
"application/atomsvc+xml": "Atom Publishing Protocol Service Document",
|
||||
"application/atom+xml": "Atom Syndication Format",
|
||||
"application/pkix-attr-cert": "Attribute Certificate",
|
||||
"audio/x-aiff": "Audio Interchange File Format",
|
||||
"video/x-msvideo": "Audio Video Interleave (AVI)",
|
||||
"application/vnd.audiograph": "Audiograph",
|
||||
"image/vnd.dxf": "AutoCAD DXF",
|
||||
"model/vnd.dwf": "Autodesk Design Web Format (DWF)",
|
||||
"text/plain-bas": "BAS Partitur Format",
|
||||
"application/x-bcpio": "Binary CPIO Archive",
|
||||
"application/octet-stream": "Binary Data",
|
||||
"image/bmp": "Bitmap Image File",
|
||||
"application/x-bittorrent": "BitTorrent",
|
||||
"application/vnd.rim.cod": "Blackberry COD File",
|
||||
"application/vnd.blueice.multipass": "Blueice Research Multipass",
|
||||
"application/vnd.bmi": "BMI Drawing Data Interchange",
|
||||
"application/x-sh": "Bourne Shell Script",
|
||||
"image/prs.btif": "BTIF",
|
||||
"application/vnd.businessobjects": "BusinessObjects",
|
||||
"application/x-bzip": "Bzip Archive",
|
||||
"application/x-bzip2": "Bzip2 Archive",
|
||||
"application/x-csh": "C Shell Script",
|
||||
"text/x-c": "C Source File",
|
||||
"application/vnd.chemdraw+xml": "CambridgeSoft Chem Draw",
|
||||
"text/css": "Cascading Style Sheets (CSS)",
|
||||
"chemical/x-cdx": "ChemDraw eXchange file",
|
||||
"chemical/x-cml": "Chemical Markup Language",
|
||||
"chemical/x-csml": "Chemical Style Markup Language",
|
||||
"application/vnd.contact.cmsg": "CIM Database",
|
||||
"application/vnd.claymore": "Claymore Data Files",
|
||||
"application/vnd.clonk.c4group": "Clonk Game",
|
||||
"image/vnd.dvb.subtitle": "Close Captioning - Subtitle",
|
||||
"application/cdmi-capability": "Cloud Data Management Interface (CDMI) - Capability",
|
||||
"application/cdmi-container": "Cloud Data Management Interface (CDMI) - Contaimer",
|
||||
"application/cdmi-domain": "Cloud Data Management Interface (CDMI) - Domain",
|
||||
"application/cdmi-object": "Cloud Data Management Interface (CDMI) - Object",
|
||||
"application/cdmi-queue": "Cloud Data Management Interface (CDMI) - Queue",
|
||||
"application/vnd.cluetrust.cartomobile-config": "ClueTrust CartoMobile - Config",
|
||||
"application/vnd.cluetrust.cartomobile-config-pkg": "ClueTrust CartoMobile - Config Package",
|
||||
"image/x-cmu-raster": "CMU Image",
|
||||
"model/vnd.collada+xml": "COLLADA",
|
||||
"text/csv": "Comma-Seperated Values",
|
||||
"application/mac-compactpro": "Compact Pro",
|
||||
"application/vnd.wap.wmlc": "Compiled Wireless Markup Language (WMLC)",
|
||||
"image/cgm": "Computer Graphics Metafile",
|
||||
"x-conference/x-cooltalk": "CoolTalk",
|
||||
"image/x-cmx": "Corel Metafile Exchange (CMX)",
|
||||
"application/vnd.xara": "CorelXARA",
|
||||
"application/vnd.cosmocaller": "CosmoCaller",
|
||||
"application/x-cpio": "CPIO Archive",
|
||||
"application/vnd.crick.clicker": "CrickSoftware - Clicker",
|
||||
"application/vnd.crick.clicker.keyboard": "CrickSoftware - Clicker - Keyboard",
|
||||
"application/vnd.crick.clicker.palette": "CrickSoftware - Clicker - Palette",
|
||||
"application/vnd.crick.clicker.template": "CrickSoftware - Clicker - Template",
|
||||
"application/vnd.crick.clicker.wordbank": "CrickSoftware - Clicker - Wordbank",
|
||||
"application/vnd.criticaltools.wbs+xml": "Critical Tools - PERT Chart EXPERT",
|
||||
"application/vnd.rig.cryptonote": "CryptoNote",
|
||||
"chemical/x-cif": "Crystallographic Interchange Format",
|
||||
"chemical/x-cmdf": "CrystalMaker Data Format",
|
||||
"application/cu-seeme": "CU-SeeMe",
|
||||
"application/prs.cww": "CU-Writer",
|
||||
"text/vnd.curl": "Curl - Applet",
|
||||
"text/vnd.curl.dcurl": "Curl - Detached Applet",
|
||||
"text/vnd.curl.mcurl": "Curl - Manifest File",
|
||||
"text/vnd.curl.scurl": "Curl - Source Code",
|
||||
"application/vnd.curl.car": "CURL Applet",
|
||||
"application/vnd.curl.pcurl": "CURL Applet",
|
||||
"application/vnd.yellowriver-custom-menu": "CustomMenu",
|
||||
"application/dssc+der": "Data Structure for the Security Suitability of Cryptographic Algorithms",
|
||||
"application/dssc+xml": "Data Structure for the Security Suitability of Cryptographic Algorithms",
|
||||
"application/x-debian-package": "Debian Package",
|
||||
"audio/vnd.dece.audio": "DECE Audio",
|
||||
"image/vnd.dece.graphic": "DECE Graphic",
|
||||
"video/vnd.dece.hd": "DECE High Definition Video",
|
||||
"video/vnd.dece.mobile": "DECE Mobile Video",
|
||||
"video/vnd.uvvu.mp4": "DECE MP4",
|
||||
"video/vnd.dece.pd": "DECE PD Video",
|
||||
"video/vnd.dece.sd": "DECE SD Video",
|
||||
"video/vnd.dece.video": "DECE Video",
|
||||
"application/x-dvi": "Device Independent File Format (DVI)",
|
||||
"application/vnd.fdsn.seed": "Digital Siesmograph Networks - SEED Datafiles",
|
||||
"application/x-dtbook+xml": "Digital Talking Book",
|
||||
"application/x-dtbresource+xml": "Digital Talking Book - Resource File",
|
||||
"application/vnd.dvb.ait": "Digital Video Broadcasting",
|
||||
"application/vnd.dvb.service": "Digital Video Broadcasting",
|
||||
"audio/vnd.digital-winds": "Digital Winds Music",
|
||||
"image/vnd.djvu": "DjVu",
|
||||
"application/xml-dtd": "Document Type Definition",
|
||||
"application/vnd.dolby.mlp": "Dolby Meridian Lossless Packing",
|
||||
"application/x-doom": "Doom Video Game",
|
||||
"application/vnd.dpgraph": "DPGraph",
|
||||
"audio/vnd.dra": "DRA Audio",
|
||||
"application/vnd.dreamfactory": "DreamFactory",
|
||||
"audio/vnd.dts": "DTS Audio",
|
||||
"audio/vnd.dts.hd": "DTS High Definition Audio",
|
||||
"image/vnd.dwg": "DWG Drawing",
|
||||
"application/vnd.dynageo": "DynaGeo",
|
||||
"application/ecmascript": "ECMAScript",
|
||||
"application/vnd.ecowin.chart": "EcoWin Chart",
|
||||
"image/vnd.fujixerox.edmics-mmr": "EDMICS 2000",
|
||||
"image/vnd.fujixerox.edmics-rlc": "EDMICS 2000",
|
||||
"application/exi": "Efficient XML Interchange",
|
||||
"application/vnd.proteus.magazine": "EFI Proteus",
|
||||
"application/epub+zip": "Electronic Publication",
|
||||
"message/rfc822": "Email Message",
|
||||
"application/vnd.enliven": "Enliven Viewer",
|
||||
"application/vnd.is-xpr": "Express by Infoseek",
|
||||
"image/vnd.xiff": "eXtended Image File Format (XIFF)",
|
||||
"application/vnd.xfdl": "Extensible Forms Description Language",
|
||||
"application/emma+xml": "Extensible MultiModal Annotation",
|
||||
"application/vnd.ezpix-album": "EZPix Secure Photo Album",
|
||||
"application/vnd.ezpix-package": "EZPix Secure Photo Album",
|
||||
"image/vnd.fst": "FAST Search & Transfer ASA",
|
||||
"video/vnd.fvt": "FAST Search & Transfer ASA",
|
||||
"image/vnd.fastbidsheet": "FastBid Sheet",
|
||||
"application/vnd.denovo.fcselayout-link": "FCS Express Layout Link",
|
||||
"video/x-f4v": "Flash Video",
|
||||
"video/x-flv": "Flash Video",
|
||||
"image/vnd.fpx": "FlashPix",
|
||||
"image/vnd.net-fpx": "FlashPix",
|
||||
"text/vnd.fmi.flexstor": "FLEXSTOR",
|
||||
"video/x-fli": "FLI/FLC Animation Format",
|
||||
"application/vnd.fluxtime.clip": "FluxTime Clip",
|
||||
"application/vnd.fdf": "Forms Data Format",
|
||||
"text/x-fortran": "Fortran Source File",
|
||||
"application/vnd.mif": "FrameMaker Interchange Format",
|
||||
"application/vnd.framemaker": "FrameMaker Normal Format",
|
||||
"image/x-freehand": "FreeHand MX",
|
||||
"application/vnd.fsc.weblaunch": "Friendly Software Corporation",
|
||||
"application/vnd.frogans.fnc": "Frogans Player",
|
||||
"application/vnd.frogans.ltf": "Frogans Player",
|
||||
"application/vnd.fujixerox.ddd": "Fujitsu - Xerox 2D CAD Data",
|
||||
"application/vnd.fujixerox.docuworks": "Fujitsu - Xerox DocuWorks",
|
||||
"application/vnd.fujixerox.docuworks.binder": "Fujitsu - Xerox DocuWorks Binder",
|
||||
"application/vnd.fujitsu.oasys": "Fujitsu Oasys",
|
||||
"application/vnd.fujitsu.oasys2": "Fujitsu Oasys",
|
||||
"application/vnd.fujitsu.oasys3": "Fujitsu Oasys",
|
||||
"application/vnd.fujitsu.oasysgp": "Fujitsu Oasys",
|
||||
"application/vnd.fujitsu.oasysprs": "Fujitsu Oasys",
|
||||
"application/x-futuresplash": "FutureSplash Animator",
|
||||
"application/vnd.fuzzysheet": "FuzzySheet",
|
||||
"image/g3fax": "G3 Fax Image",
|
||||
"application/vnd.gmx": "GameMaker ActiveX",
|
||||
"model/vnd.gtw": "Gen-Trix Studio",
|
||||
"application/vnd.genomatix.tuxedo": "Genomatix Tuxedo Framework",
|
||||
"application/vnd.geogebra.file": "GeoGebra",
|
||||
"application/vnd.geogebra.tool": "GeoGebra",
|
||||
"model/vnd.gdl": "Geometric Description Language (GDL)",
|
||||
"application/vnd.geometry-explorer": "GeoMetry Explorer",
|
||||
"application/vnd.geonext": "GEONExT and JSXGraph",
|
||||
"application/vnd.geoplan": "GeoplanW",
|
||||
"application/vnd.geospace": "GeospacW",
|
||||
"application/x-font-ghostscript": "Ghostscript Font",
|
||||
"application/x-font-bdf": "Glyph Bitmap Distribution Format",
|
||||
"application/x-gtar": "GNU Tar Files",
|
||||
"application/x-texinfo": "GNU Texinfo Document",
|
||||
"application/x-gnumeric": "Gnumeric",
|
||||
"application/vnd.google-earth.kml+xml": "Google Earth - KML",
|
||||
"application/vnd.google-earth.kmz": "Google Earth - Zipped KML",
|
||||
"application/vnd.grafeq": "GrafEq",
|
||||
"image/gif": "Graphics Interchange Format",
|
||||
"text/vnd.graphviz": "Graphviz",
|
||||
"application/vnd.groove-account": "Groove - Account",
|
||||
"application/vnd.groove-help": "Groove - Help",
|
||||
"application/vnd.groove-identity-message": "Groove - Identity Message",
|
||||
"application/vnd.groove-injector": "Groove - Injector",
|
||||
"application/vnd.groove-tool-message": "Groove - Tool Message",
|
||||
"application/vnd.groove-tool-template": "Groove - Tool Template",
|
||||
"application/vnd.groove-vcard": "Groove - Vcard",
|
||||
"video/h261": "H.261",
|
||||
"video/h263": "H.263",
|
||||
"video/h264": "H.264",
|
||||
"application/vnd.hp-hpid": "Hewlett Packard Instant Delivery",
|
||||
"application/vnd.hp-hps": "Hewlett-Packard's WebPrintSmart",
|
||||
"application/x-hdf": "Hierarchical Data Format",
|
||||
"audio/vnd.rip": "Hit'n'Mix",
|
||||
"application/vnd.hbci": "Homebanking Computer Interface (HBCI)",
|
||||
"application/vnd.hp-jlyt": "HP Indigo Digital Press - Job Layout Languate",
|
||||
"application/vnd.hp-pcl": "HP Printer Command Language",
|
||||
"application/vnd.hp-hpgl": "HP-GL/2 and HP RTL",
|
||||
"application/vnd.yamaha.hv-script": "HV Script",
|
||||
"application/vnd.yamaha.hv-dic": "HV Voice Dictionary",
|
||||
"application/vnd.yamaha.hv-voice": "HV Voice Parameter",
|
||||
"application/vnd.hydrostatix.sof-data": "Hydrostatix Master Suite",
|
||||
"application/hyperstudio": "Hyperstudio",
|
||||
"application/vnd.hal+xml": "Hypertext Application Language",
|
||||
"text/html": "HyperText Markup Language (HTML)",
|
||||
"application/vnd.ibm.rights-management": "IBM DB2 Rights Manager",
|
||||
"application/vnd.ibm.secure-container": "IBM Electronic Media Management System - Secure Container",
|
||||
"text/calendar": "iCalendar",
|
||||
"application/vnd.iccprofile": "ICC profile",
|
||||
"image/x-icon": "Icon Image",
|
||||
"application/vnd.igloader": "igLoader",
|
||||
"image/ief": "Image Exchange Format",
|
||||
"application/vnd.immervision-ivp": "ImmerVision PURE Players",
|
||||
"application/vnd.immervision-ivu": "ImmerVision PURE Players",
|
||||
"application/reginfo+xml": "IMS Networks",
|
||||
"text/vnd.in3d.3dml": "In3D - 3DML",
|
||||
"text/vnd.in3d.spot": "In3D - 3DML",
|
||||
"model/iges": "Initial Graphics Exchange Specification (IGES)",
|
||||
"application/vnd.intergeo": "Interactive Geometry Software",
|
||||
"application/vnd.cinderella": "Interactive Geometry Software Cinderella",
|
||||
"application/vnd.intercon.formnet": "Intercon FormNet",
|
||||
"application/vnd.isac.fcs": "International Society for Advancement of Cytometry",
|
||||
"application/ipfix": "Internet Protocol Flow Information Export",
|
||||
"application/pkix-cert": "Internet Public Key Infrastructure - Certificate",
|
||||
"application/pkixcmp": "Internet Public Key Infrastructure - Certificate Management Protocole",
|
||||
"application/pkix-crl": "Internet Public Key Infrastructure - Certificate Revocation Lists",
|
||||
"application/pkix-pkipath": "Internet Public Key Infrastructure - Certification Path",
|
||||
"application/vnd.insors.igm": "IOCOM Visimeet",
|
||||
"application/vnd.ipunplugged.rcprofile": "IP Unplugged Roaming Client",
|
||||
"application/vnd.irepository.package+xml": "iRepository / Lucidoc Editor",
|
||||
"text/vnd.sun.j2me.app-descriptor": "J2ME App Descriptor",
|
||||
"application/java-archive": "Java Archive",
|
||||
"application/java-vm": "Java Bytecode File",
|
||||
"application/x-java-jnlp-file": "Java Network Launching Protocol",
|
||||
"application/java-serialized-object": "Java Serialized Object",
|
||||
"text/x-java-source,java": "Java Source File",
|
||||
"application/javascript": "JavaScript",
|
||||
"application/json": "JavaScript Object Notation (JSON)",
|
||||
"application/vnd.joost.joda-archive": "Joda Archive",
|
||||
"video/jpm": "JPEG 2000 Compound Image File Format",
|
||||
"image/jpeg": "JPEG Image",
|
||||
"image/x-citrix-jpeg": "JPEG Image (Citrix client)",
|
||||
"image/pjpeg": "JPEG Image (Progressive)",
|
||||
"video/jpeg": "JPGVideo",
|
||||
"application/vnd.kahootz": "Kahootz",
|
||||
"application/vnd.chipnuts.karaoke-mmd": "Karaoke on Chipnuts Chipsets",
|
||||
"application/vnd.kde.karbon": "KDE KOffice Office Suite - Karbon",
|
||||
"application/vnd.kde.kchart": "KDE KOffice Office Suite - KChart",
|
||||
"application/vnd.kde.kformula": "KDE KOffice Office Suite - Kformula",
|
||||
"application/vnd.kde.kivio": "KDE KOffice Office Suite - Kivio",
|
||||
"application/vnd.kde.kontour": "KDE KOffice Office Suite - Kontour",
|
||||
"application/vnd.kde.kpresenter": "KDE KOffice Office Suite - Kpresenter",
|
||||
"application/vnd.kde.kspread": "KDE KOffice Office Suite - Kspread",
|
||||
"application/vnd.kde.kword": "KDE KOffice Office Suite - Kword",
|
||||
"application/vnd.kenameaapp": "Kenamea App",
|
||||
"application/vnd.kidspiration": "Kidspiration",
|
||||
"application/vnd.kinar": "Kinar Applications",
|
||||
"application/vnd.kodak-descriptor": "Kodak Storyshare",
|
||||
"application/vnd.las.las+xml": "Laser App Enterprise",
|
||||
"application/x-latex": "LaTeX",
|
||||
"application/vnd.llamagraphics.life-balance.desktop": "Life Balance - Desktop Edition",
|
||||
"application/vnd.llamagraphics.life-balance.exchange+xml": "Life Balance - Exchange Format",
|
||||
"application/vnd.jam": "Lightspeed Audio Lab",
|
||||
"application/vnd.lotus-1-2-3": "Lotus 1-2-3",
|
||||
"application/vnd.lotus-approach": "Lotus Approach",
|
||||
"application/vnd.lotus-freelance": "Lotus Freelance",
|
||||
"application/vnd.lotus-notes": "Lotus Notes",
|
||||
"application/vnd.lotus-organizer": "Lotus Organizer",
|
||||
"application/vnd.lotus-screencam": "Lotus Screencam",
|
||||
"application/vnd.lotus-wordpro": "Lotus Wordpro",
|
||||
"audio/vnd.lucent.voice": "Lucent Voice",
|
||||
"audio/x-mpegurl": "M3U (Multimedia Playlist)",
|
||||
"video/x-m4v": "M4v",
|
||||
"application/mac-binhex40": "Macintosh BinHex 4.0",
|
||||
"application/vnd.macports.portpkg": "MacPorts Port System",
|
||||
"application/vnd.osgeo.mapguide.package": "MapGuide DBXML",
|
||||
"application/marc": "MARC Formats",
|
||||
"application/marcxml+xml": "MARC21 XML Schema",
|
||||
"application/mxf": "Material Exchange Format",
|
||||
"application/vnd.wolfram.player": "Mathematica Notebook Player",
|
||||
"application/mathematica": "Mathematica Notebooks",
|
||||
"application/mathml+xml": "Mathematical Markup Language",
|
||||
"application/mbox": "Mbox database files",
|
||||
"application/vnd.medcalcdata": "MedCalc",
|
||||
"application/mediaservercontrol+xml": "Media Server Control Markup Language",
|
||||
"application/vnd.mediastation.cdkey": "MediaRemote",
|
||||
"application/vnd.mfer": "Medical Waveform Encoding Format",
|
||||
"application/vnd.mfmp": "Melody Format for Mobile Platform",
|
||||
"model/mesh": "Mesh Data Type",
|
||||
"application/mads+xml": "Metadata Authority Description Schema",
|
||||
"application/mets+xml": "Metadata Encoding and Transmission Standard",
|
||||
"application/mods+xml": "Metadata Object Description Schema",
|
||||
"application/metalink4+xml": "Metalink",
|
||||
"application/vnd.mcd": "Micro CADAM Helix D&D",
|
||||
"application/vnd.micrografx.flo": "Micrografx",
|
||||
"application/vnd.micrografx.igx": "Micrografx iGrafx Professional",
|
||||
"application/vnd.eszigno3+xml": "MICROSEC e-Szign¢",
|
||||
"application/x-msaccess": "Microsoft Access",
|
||||
"video/x-ms-asf": "Microsoft Advanced Systems Format (ASF)",
|
||||
"application/x-msdownload": "Microsoft Application",
|
||||
"application/vnd.ms-artgalry": "Microsoft Artgalry",
|
||||
"application/vnd.ms-cab-compressed": "Microsoft Cabinet File",
|
||||
"application/vnd.ms-ims": "Microsoft Class Server",
|
||||
"application/x-ms-application": "Microsoft ClickOnce",
|
||||
"application/x-msclip": "Microsoft Clipboard Clip",
|
||||
"image/vnd.ms-modi": "Microsoft Document Imaging Format",
|
||||
"application/vnd.ms-fontobject": "Microsoft Embedded OpenType",
|
||||
"application/vnd.ms-excel": "Microsoft Excel",
|
||||
"application/vnd.ms-excel.addin.macroenabled.12": "Microsoft Excel - Add-In File",
|
||||
"application/vnd.ms-excel.sheet.binary.macroenabled.12": "Microsoft Excel - Binary Workbook",
|
||||
"application/vnd.ms-excel.template.macroenabled.12": "Microsoft Excel - Macro-Enabled Template File",
|
||||
"application/vnd.ms-excel.sheet.macroenabled.12": "Microsoft Excel - Macro-Enabled Workbook",
|
||||
"application/vnd.ms-htmlhelp": "Microsoft Html Help File",
|
||||
"application/x-mscardfile": "Microsoft Information Card",
|
||||
"application/vnd.ms-lrm": "Microsoft Learning Resource Module",
|
||||
"application/x-msmediaview": "Microsoft MediaView",
|
||||
"application/x-msmoney": "Microsoft Money",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "Microsoft Office - OOXML - Presentation",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slide": "Microsoft Office - OOXML - Presentation (Slide)",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": "Microsoft Office - OOXML - Presentation (Slideshow)",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.template": "Microsoft Office - OOXML - Presentation Template",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Microsoft Office - OOXML - Spreadsheet",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.template": "Microsoft Office - OOXML - Spreadsheet Template",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Microsoft Office - OOXML - Word Document",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.template": "Microsoft Office - OOXML - Word Document Template",
|
||||
"application/x-msbinder": "Microsoft Office Binder",
|
||||
"application/vnd.ms-officetheme": "Microsoft Office System Release Theme",
|
||||
"application/onenote": "Microsoft OneNote",
|
||||
"audio/vnd.ms-playready.media.pya": "Microsoft PlayReady Ecosystem",
|
||||
"video/vnd.ms-playready.media.pyv": "Microsoft PlayReady Ecosystem Video",
|
||||
"application/vnd.ms-powerpoint": "Microsoft PowerPoint",
|
||||
"application/vnd.ms-powerpoint.addin.macroenabled.12": "Microsoft PowerPoint - Add-in file",
|
||||
"application/vnd.ms-powerpoint.slide.macroenabled.12": "Microsoft PowerPoint - Macro-Enabled Open XML Slide",
|
||||
"application/vnd.ms-powerpoint.presentation.macroenabled.12": "Microsoft PowerPoint - Macro-Enabled Presentation File",
|
||||
"application/vnd.ms-powerpoint.slideshow.macroenabled.12": "Microsoft PowerPoint - Macro-Enabled Slide Show File",
|
||||
"application/vnd.ms-powerpoint.template.macroenabled.12": "Microsoft PowerPoint - Macro-Enabled Template File",
|
||||
"application/vnd.ms-project": "Microsoft Project",
|
||||
"application/x-mspublisher": "Microsoft Publisher",
|
||||
"application/x-msschedule": "Microsoft Schedule+",
|
||||
"application/x-silverlight-app": "Microsoft Silverlight",
|
||||
"application/vnd.ms-pki.stl": "Microsoft Trust UI Provider - Certificate Trust Link",
|
||||
"application/vnd.ms-pki.seccat": "Microsoft Trust UI Provider - Security Catalog",
|
||||
"application/vnd.visio": "Microsoft Visio",
|
||||
"application/vnd.visio2013": "Microsoft Visio 2013",
|
||||
"video/x-ms-wm": "Microsoft Windows Media",
|
||||
"audio/x-ms-wma": "Microsoft Windows Media Audio",
|
||||
"audio/x-ms-wax": "Microsoft Windows Media Audio Redirector",
|
||||
"video/x-ms-wmx": "Microsoft Windows Media Audio/Video Playlist",
|
||||
"application/x-ms-wmd": "Microsoft Windows Media Player Download Package",
|
||||
"application/vnd.ms-wpl": "Microsoft Windows Media Player Playlist",
|
||||
"application/x-ms-wmz": "Microsoft Windows Media Player Skin Package",
|
||||
"video/x-ms-wmv": "Microsoft Windows Media Video",
|
||||
"video/x-ms-wvx": "Microsoft Windows Media Video Playlist",
|
||||
"application/x-msmetafile": "Microsoft Windows Metafile",
|
||||
"application/x-msterminal": "Microsoft Windows Terminal Services",
|
||||
"application/msword": "Microsoft Word",
|
||||
"application/vnd.ms-word.document.macroenabled.12": "Microsoft Word - Macro-Enabled Document",
|
||||
"application/vnd.ms-word.template.macroenabled.12": "Microsoft Word - Macro-Enabled Template",
|
||||
"application/x-mswrite": "Microsoft Wordpad",
|
||||
"application/vnd.ms-works": "Microsoft Works",
|
||||
"application/x-ms-xbap": "Microsoft XAML Browser Application",
|
||||
"application/vnd.ms-xpsdocument": "Microsoft XML Paper Specification",
|
||||
"audio/midi": "MIDI - Musical Instrument Digital Interface",
|
||||
"application/vnd.ibm.minipay": "MiniPay",
|
||||
"application/vnd.ibm.modcap": "MO:DCA-P",
|
||||
"application/vnd.jcp.javame.midlet-rms": "Mobile Information Device Profile",
|
||||
"application/vnd.tmobile-livetv": "MobileTV",
|
||||
"application/x-mobipocket-ebook": "Mobipocket",
|
||||
"application/vnd.mobius.mbk": "Mobius Management Systems - Basket file",
|
||||
"application/vnd.mobius.dis": "Mobius Management Systems - Distribution Database",
|
||||
"application/vnd.mobius.plc": "Mobius Management Systems - Policy Definition Language File",
|
||||
"application/vnd.mobius.mqy": "Mobius Management Systems - Query File",
|
||||
"application/vnd.mobius.msl": "Mobius Management Systems - Script Language",
|
||||
"application/vnd.mobius.txf": "Mobius Management Systems - Topic Index File",
|
||||
"application/vnd.mobius.daf": "Mobius Management Systems - UniversalArchive",
|
||||
"text/vnd.fly": "mod_fly / fly.cgi",
|
||||
"application/vnd.mophun.certificate": "Mophun Certificate",
|
||||
"application/vnd.mophun.application": "Mophun VM",
|
||||
"video/mj2": "Motion JPEG 2000",
|
||||
"audio/mpeg": "MPEG Audio",
|
||||
"video/vnd.mpegurl": "MPEG Url",
|
||||
"video/mpeg": "MPEG Video",
|
||||
"application/mp21": "MPEG-21",
|
||||
"audio/mp4": "MPEG-4 Audio",
|
||||
"video/mp4": "MPEG-4 Video",
|
||||
"application/mp4": "MPEG4",
|
||||
"application/vnd.apple.mpegurl": "Multimedia Playlist Unicode",
|
||||
"application/vnd.musician": "MUsical Score Interpreted Code Invented for the ASCII designation of Notation",
|
||||
"application/vnd.muvee.style": "Muvee Automatic Video Editing",
|
||||
"application/xv+xml": "MXML",
|
||||
"application/vnd.nokia.n-gage.data": "N-Gage Game Data",
|
||||
"application/vnd.nokia.n-gage.symbian.install": "N-Gage Game Installer",
|
||||
"application/x-dtbncx+xml": "Navigation Control file for XML (for ePub)",
|
||||
"application/x-netcdf": "Network Common Data Form (NetCDF)",
|
||||
"application/vnd.neurolanguage.nlu": "neuroLanguage",
|
||||
"application/vnd.dna": "New Moon Liftoff/DNA",
|
||||
"application/vnd.noblenet-directory": "NobleNet Directory",
|
||||
"application/vnd.noblenet-sealer": "NobleNet Sealer",
|
||||
"application/vnd.noblenet-web": "NobleNet Web",
|
||||
"application/vnd.nokia.radio-preset": "Nokia Radio Application - Preset",
|
||||
"application/vnd.nokia.radio-presets": "Nokia Radio Application - Preset",
|
||||
"text/n3": "Notation3",
|
||||
"application/vnd.novadigm.edm": "Novadigm's RADIA and EDM products",
|
||||
"application/vnd.novadigm.edx": "Novadigm's RADIA and EDM products",
|
||||
"application/vnd.novadigm.ext": "Novadigm's RADIA and EDM products",
|
||||
"application/vnd.flographit": "NpGraphIt",
|
||||
"audio/vnd.nuera.ecelp4800": "Nuera ECELP 4800",
|
||||
"audio/vnd.nuera.ecelp7470": "Nuera ECELP 7470",
|
||||
"audio/vnd.nuera.ecelp9600": "Nuera ECELP 9600",
|
||||
"application/oda": "Office Document Architecture",
|
||||
"application/ogg": "Ogg",
|
||||
"audio/ogg": "Ogg Audio",
|
||||
"video/ogg": "Ogg Video",
|
||||
"application/vnd.oma.dd2+xml": "OMA Download Agents",
|
||||
"application/vnd.oasis.opendocument.text-web": "Open Document Text Web",
|
||||
"application/oebps-package+xml": "Open eBook Publication Structure",
|
||||
"application/vnd.intu.qbo": "Open Financial Exchange",
|
||||
"application/vnd.openofficeorg.extension": "Open Office Extension",
|
||||
"application/vnd.yamaha.openscoreformat": "Open Score Format",
|
||||
"audio/webm": "Open Web Media Project - Audio",
|
||||
"video/webm": "Open Web Media Project - Video",
|
||||
"application/vnd.oasis.opendocument.chart": "OpenDocument Chart",
|
||||
"application/vnd.oasis.opendocument.chart-template": "OpenDocument Chart Template",
|
||||
"application/vnd.oasis.opendocument.database": "OpenDocument Database",
|
||||
"application/vnd.oasis.opendocument.formula": "OpenDocument Formula",
|
||||
"application/vnd.oasis.opendocument.formula-template": "OpenDocument Formula Template",
|
||||
"application/vnd.oasis.opendocument.graphics": "OpenDocument Graphics",
|
||||
"application/vnd.oasis.opendocument.graphics-template": "OpenDocument Graphics Template",
|
||||
"application/vnd.oasis.opendocument.image": "OpenDocument Image",
|
||||
"application/vnd.oasis.opendocument.image-template": "OpenDocument Image Template",
|
||||
"application/vnd.oasis.opendocument.presentation": "OpenDocument Presentation",
|
||||
"application/vnd.oasis.opendocument.presentation-template": "OpenDocument Presentation Template",
|
||||
"application/vnd.oasis.opendocument.spreadsheet": "OpenDocument Spreadsheet",
|
||||
"application/vnd.oasis.opendocument.spreadsheet-template": "OpenDocument Spreadsheet Template",
|
||||
"application/vnd.oasis.opendocument.text": "OpenDocument Text",
|
||||
"application/vnd.oasis.opendocument.text-master": "OpenDocument Text Master",
|
||||
"application/vnd.oasis.opendocument.text-template": "OpenDocument Text Template",
|
||||
"image/ktx": "OpenGL Textures (KTX)",
|
||||
"application/vnd.sun.xml.calc": "OpenOffice - Calc (Spreadsheet)",
|
||||
"application/vnd.sun.xml.calc.template": "OpenOffice - Calc Template (Spreadsheet)",
|
||||
"application/vnd.sun.xml.draw": "OpenOffice - Draw (Graphics)",
|
||||
"application/vnd.sun.xml.draw.template": "OpenOffice - Draw Template (Graphics)",
|
||||
"application/vnd.sun.xml.impress": "OpenOffice - Impress (Presentation)",
|
||||
"application/vnd.sun.xml.impress.template": "OpenOffice - Impress Template (Presentation)",
|
||||
"application/vnd.sun.xml.math": "OpenOffice - Math (Formula)",
|
||||
"application/vnd.sun.xml.writer": "OpenOffice - Writer (Text - HTML)",
|
||||
"application/vnd.sun.xml.writer.global": "OpenOffice - Writer (Text - HTML)",
|
||||
"application/vnd.sun.xml.writer.template": "OpenOffice - Writer Template (Text - HTML)",
|
||||
"application/x-font-otf": "OpenType Font File",
|
||||
"application/vnd.yamaha.openscoreformat.osfpvg+xml": "OSFPVG",
|
||||
"application/vnd.osgi.dp": "OSGi Deployment Package",
|
||||
"application/vnd.palm": "PalmOS Data",
|
||||
"text/x-pascal": "Pascal Source File",
|
||||
"application/vnd.pawaafile": "PawaaFILE",
|
||||
"application/vnd.hp-pclxl": "PCL 6 Enhanced (Formely PCL XL)",
|
||||
"application/vnd.picsel": "Pcsel eFIF File",
|
||||
"image/x-pcx": "PCX Image",
|
||||
"image/vnd.adobe.photoshop": "Photoshop Document",
|
||||
"application/pics-rules": "PICSRules",
|
||||
"image/x-pict": "PICT Image",
|
||||
"application/x-chat": "pIRCh",
|
||||
"application/pkcs10": "PKCS #10 - Certification Request Standard",
|
||||
"application/x-pkcs12": "PKCS #12 - Personal Information Exchange Syntax Standard",
|
||||
"application/pkcs7-mime": "PKCS #7 - Cryptographic Message Syntax Standard",
|
||||
"application/pkcs7-signature": "PKCS #7 - Cryptographic Message Syntax Standard",
|
||||
"application/x-pkcs7-certreqresp": "PKCS #7 - Cryptographic Message Syntax Standard (Certificate Request Response)",
|
||||
"application/x-pkcs7-certificates": "PKCS #7 - Cryptographic Message Syntax Standard (Certificates)",
|
||||
"application/pkcs8": "PKCS #8 - Private-Key Information Syntax Standard",
|
||||
"application/vnd.pocketlearn": "PocketLearn Viewers",
|
||||
"image/x-portable-anymap": "Portable Anymap Image",
|
||||
"image/x-portable-bitmap": "Portable Bitmap Format",
|
||||
"application/x-font-pcf": "Portable Compiled Format",
|
||||
"application/font-tdpfr": "Portable Font Resource",
|
||||
"application/x-chess-pgn": "Portable Game Notation (Chess Games)",
|
||||
"image/x-portable-graymap": "Portable Graymap Format",
|
||||
"image/png": "Portable Network Graphics (PNG)",
|
||||
"image/x-citrix-png": "Portable Network Graphics (PNG) (Citrix client)",
|
||||
"image/x-png": "Portable Network Graphics (PNG) (x-token)",
|
||||
"image/x-portable-pixmap": "Portable Pixmap Format",
|
||||
"application/pskc+xml": "Portable Symmetric Key Container",
|
||||
"application/vnd.ctc-posml": "PosML",
|
||||
"application/postscript": "PostScript",
|
||||
"application/x-font-type1": "PostScript Fonts",
|
||||
"application/vnd.powerbuilder6": "PowerBuilder",
|
||||
"application/pgp-encrypted": "Pretty Good Privacy",
|
||||
"application/pgp-signature": "Pretty Good Privacy - Signature",
|
||||
"application/vnd.previewsystems.box": "Preview Systems ZipLock/VBox",
|
||||
"application/vnd.pvi.ptid1": "Princeton Video Image",
|
||||
"application/pls+xml": "Pronunciation Lexicon Specification",
|
||||
"application/vnd.pg.format": "Proprietary P&G Standard Reporting System",
|
||||
"application/vnd.pg.osasli": "Proprietary P&G Standard Reporting System",
|
||||
"text/prs.lines.tag": "PRS Lines Tag",
|
||||
"application/x-font-linux-psf": "PSF Fonts",
|
||||
"application/vnd.publishare-delta-tree": "PubliShare Objects",
|
||||
"application/vnd.pmi.widget": "Qualcomm's Plaza Mobile Internet",
|
||||
"application/vnd.quark.quarkxpress": "QuarkXpress",
|
||||
"application/vnd.epson.esf": "QUASS Stream Player",
|
||||
"application/vnd.epson.msf": "QUASS Stream Player",
|
||||
"application/vnd.epson.ssf": "QUASS Stream Player",
|
||||
"application/vnd.epson.quickanime": "QuickAnime Player",
|
||||
"application/vnd.intu.qfx": "Quicken",
|
||||
"video/quicktime": "Quicktime Video",
|
||||
"application/x-rar-compressed": "RAR Archive",
|
||||
"audio/x-pn-realaudio": "Real Audio Sound",
|
||||
"audio/x-pn-realaudio-plugin": "Real Audio Sound",
|
||||
"application/rsd+xml": "Really Simple Discovery",
|
||||
"application/vnd.rn-realmedia": "RealMedia",
|
||||
"application/vnd.realvnc.bed": "RealVNC",
|
||||
"application/vnd.recordare.musicxml": "Recordare Applications",
|
||||
"application/vnd.recordare.musicxml+xml": "Recordare Applications",
|
||||
"application/relax-ng-compact-syntax": "Relax NG Compact Syntax",
|
||||
"application/vnd.data-vision.rdz": "RemoteDocs R-Viewer",
|
||||
"application/rdf+xml": "Resource Description Framework",
|
||||
"application/vnd.cloanto.rp9": "RetroPlatform Player",
|
||||
"application/vnd.jisp": "RhymBox",
|
||||
"application/rtf": "Rich Text Format",
|
||||
"text/richtext": "Rich Text Format (RTF)",
|
||||
"application/vnd.route66.link66+xml": "ROUTE 66 Location Based Services",
|
||||
"application/rss+xml": "RSS - Really Simple Syndication",
|
||||
"application/shf+xml": "S Hexdump Format",
|
||||
"application/vnd.sailingtracker.track": "SailingTracker",
|
||||
"image/svg+xml": "Scalable Vector Graphics (SVG)",
|
||||
"application/vnd.sus-calendar": "ScheduleUs",
|
||||
"application/sru+xml": "Search/Retrieve via URL Response Format",
|
||||
"application/set-payment-initiation": "Secure Electronic Transaction - Payment",
|
||||
"application/set-registration-initiation": "Secure Electronic Transaction - Registration",
|
||||
"application/vnd.sema": "Secured eMail",
|
||||
"application/vnd.semd": "Secured eMail",
|
||||
"application/vnd.semf": "Secured eMail",
|
||||
"application/vnd.seemail": "SeeMail",
|
||||
"application/x-font-snf": "Server Normal Format",
|
||||
"application/scvp-vp-request": "Server-Based Certificate Validation Protocol - Validation Policies - Request",
|
||||
"application/scvp-vp-response": "Server-Based Certificate Validation Protocol - Validation Policies - Response",
|
||||
"application/scvp-cv-request": "Server-Based Certificate Validation Protocol - Validation Request",
|
||||
"application/scvp-cv-response": "Server-Based Certificate Validation Protocol - Validation Response",
|
||||
"application/sdp": "Session Description Protocol",
|
||||
"text/x-setext": "Setext",
|
||||
"video/x-sgi-movie": "SGI Movie",
|
||||
"application/vnd.shana.informed.formdata": "Shana Informed Filler",
|
||||
"application/vnd.shana.informed.formtemplate": "Shana Informed Filler",
|
||||
"application/vnd.shana.informed.interchange": "Shana Informed Filler",
|
||||
"application/vnd.shana.informed.package": "Shana Informed Filler",
|
||||
"application/thraud+xml": "Sharing Transaction Fraud Data",
|
||||
"application/x-shar": "Shell Archive",
|
||||
"image/x-rgb": "Silicon Graphics RGB Bitmap",
|
||||
"application/vnd.epson.salt": "SimpleAnimeLite Player",
|
||||
"application/vnd.accpac.simply.aso": "Simply Accounting",
|
||||
"application/vnd.accpac.simply.imp": "Simply Accounting - Data Import",
|
||||
"application/vnd.simtech-mindmapper": "SimTech MindMapper",
|
||||
"application/vnd.commonspace": "Sixth Floor Media - CommonSpace",
|
||||
"application/vnd.yamaha.smaf-audio": "SMAF Audio",
|
||||
"application/vnd.smaf": "SMAF File",
|
||||
"application/vnd.yamaha.smaf-phrase": "SMAF Phrase",
|
||||
"application/vnd.smart.teacher": "SMART Technologies Apps",
|
||||
"application/vnd.svd": "SourceView Document",
|
||||
"application/sparql-query": "SPARQL - Query",
|
||||
"application/sparql-results+xml": "SPARQL - Results",
|
||||
"application/srgs": "Speech Recognition Grammar Specification",
|
||||
"application/srgs+xml": "Speech Recognition Grammar Specification - XML",
|
||||
"application/ssml+xml": "Speech Synthesis Markup Language",
|
||||
"application/vnd.koan": "SSEYO Koan Play File",
|
||||
"text/sgml": "Standard Generalized Markup Language (SGML)",
|
||||
"application/vnd.stardivision.calc": "StarOffice - Calc",
|
||||
"application/vnd.stardivision.draw": "StarOffice - Draw",
|
||||
"application/vnd.stardivision.impress": "StarOffice - Impress",
|
||||
"application/vnd.stardivision.math": "StarOffice - Math",
|
||||
"application/vnd.stardivision.writer": "StarOffice - Writer",
|
||||
"application/vnd.stardivision.writer-global": "StarOffice - Writer (Global)",
|
||||
"application/vnd.stepmania.stepchart": "StepMania",
|
||||
"application/x-stuffit": "Stuffit Archive",
|
||||
"application/x-stuffitx": "Stuffit Archive",
|
||||
"application/vnd.solent.sdkm+xml": "SudokuMagic",
|
||||
"application/vnd.olpc-sugar": "Sugar Linux Application Bundle",
|
||||
"audio/basic": "Sun Audio - Au file format",
|
||||
"application/vnd.wqd": "SundaHus WQ",
|
||||
"application/vnd.symbian.install": "Symbian Install Package",
|
||||
"application/smil+xml": "Synchronized Multimedia Integration Language",
|
||||
"application/vnd.syncml+xml": "SyncML",
|
||||
"application/vnd.syncml.dm+wbxml": "SyncML - Device Management",
|
||||
"application/vnd.syncml.dm+xml": "SyncML - Device Management",
|
||||
"application/x-sv4cpio": "System V Release 4 CPIO Archive",
|
||||
"application/x-sv4crc": "System V Release 4 CPIO Checksum Data",
|
||||
"application/sbml+xml": "Systems Biology Markup Language",
|
||||
"text/tab-separated-values": "Tab Seperated Values",
|
||||
"image/tiff": "Tagged Image File Format",
|
||||
"application/vnd.tao.intent-module-archive": "Tao Intent",
|
||||
"application/x-tar": "Tar File (Tape Archive)",
|
||||
"application/x-tcl": "Tcl Script",
|
||||
"application/x-tex": "TeX",
|
||||
"application/x-tex-tfm": "TeX Font Metric",
|
||||
"application/tei+xml": "Text Encoding and Interchange",
|
||||
"text/plain": "Text File",
|
||||
"application/vnd.spotfire.dxp": "TIBCO Spotfire",
|
||||
"application/vnd.spotfire.sfs": "TIBCO Spotfire",
|
||||
"application/timestamped-data": "Time Stamped Data Envelope",
|
||||
"application/vnd.trid.tpt": "TRI Systems Config",
|
||||
"application/vnd.triscape.mxs": "Triscape Map Explorer",
|
||||
"text/troff": "troff",
|
||||
"application/vnd.trueapp": "True BASIC",
|
||||
"application/x-font-ttf": "TrueType Font",
|
||||
"text/turtle": "Turtle (Terse RDF Triple Language)",
|
||||
"application/vnd.umajin": "UMAJIN",
|
||||
"application/vnd.uoml+xml": "Unique Object Markup Language",
|
||||
"application/vnd.unity": "Unity 3d",
|
||||
"application/vnd.ufdl": "Universal Forms Description Language",
|
||||
"text/uri-list": "URI Resolution Services",
|
||||
"application/vnd.uiq.theme": "User Interface Quartz - Theme (Symbian)",
|
||||
"application/x-ustar": "Ustar (Uniform Standard Tape Archive)",
|
||||
"text/x-uuencode": "UUEncode",
|
||||
"text/x-vcalendar": "vCalendar",
|
||||
"text/x-vcard": "vCard",
|
||||
"application/x-cdlink": "Video CD",
|
||||
"application/vnd.vsf": "Viewport+",
|
||||
"model/vrml": "Virtual Reality Modeling Language",
|
||||
"application/vnd.vcx": "VirtualCatalog",
|
||||
"model/vnd.mts": "Virtue MTS",
|
||||
"model/vnd.vtu": "Virtue VTU",
|
||||
"application/vnd.visionary": "Visionary",
|
||||
"video/vnd.vivo": "Vivo",
|
||||
"application/ccxml+xml,": "Voice Browser Call Control",
|
||||
"application/voicexml+xml": "VoiceXML",
|
||||
"application/x-wais-source": "WAIS Source",
|
||||
"application/vnd.wap.wbxml": "WAP Binary XML (WBXML)",
|
||||
"image/vnd.wap.wbmp": "WAP Bitamp (WBMP)",
|
||||
"audio/x-wav": "Waveform Audio File Format (WAV)",
|
||||
"application/davmount+xml": "Web Distributed Authoring and Versioning",
|
||||
"application/x-font-woff": "Web Open Font Format",
|
||||
"application/wspolicy+xml": "Web Services Policy",
|
||||
"image/webp": "WebP Image",
|
||||
"application/vnd.webturbo": "WebTurbo",
|
||||
"application/widget": "Widget Packaging and XML Configuration",
|
||||
"application/winhlp": "WinHelp",
|
||||
"text/vnd.wap.wml": "Wireless Markup Language (WML)",
|
||||
"text/vnd.wap.wmlscript": "Wireless Markup Language Script (WMLScript)",
|
||||
"application/vnd.wap.wmlscriptc": "WMLScript",
|
||||
"application/vnd.wordperfect": "Wordperfect",
|
||||
"application/vnd.wt.stf": "Worldtalk",
|
||||
"application/wsdl+xml": "WSDL - Web Services Description Language",
|
||||
"image/x-xbitmap": "X BitMap",
|
||||
"image/x-xpixmap": "X PixMap",
|
||||
"image/x-xwindowdump": "X Window Dump",
|
||||
"application/x-x509-ca-cert": "X.509 Certificate",
|
||||
"application/x-xfig": "Xfig",
|
||||
"application/xhtml+xml": "XHTML - The Extensible HyperText Markup Language",
|
||||
"application/xml": "XML - Extensible Markup Language",
|
||||
"application/xcap-diff+xml": "XML Configuration Access Protocol - XCAP Diff",
|
||||
"application/xenc+xml": "XML Encryption Syntax and Processing",
|
||||
"application/patch-ops-error+xml": "XML Patch Framework",
|
||||
"application/resource-lists+xml": "XML Resource Lists",
|
||||
"application/rls-services+xml": "XML Resource Lists",
|
||||
"application/resource-lists-diff+xml": "XML Resource Lists Diff",
|
||||
"application/xslt+xml": "XML Transformations",
|
||||
"application/xop+xml": "XML-Binary Optimized Packaging",
|
||||
"application/x-xpinstall": "XPInstall - Mozilla",
|
||||
"application/xspf+xml": "XSPF - XML Shareable Playlist Format",
|
||||
"application/vnd.mozilla.xul+xml": "XUL - XML User Interface Language",
|
||||
"chemical/x-xyz": "XYZ File Format",
|
||||
"text/yaml": "YAML Ain't Markup Language / Yet Another Markup Language",
|
||||
"application/yang": "YANG Data Modeling Language",
|
||||
"application/yin+xml": "YIN (YANG - XML)",
|
||||
"application/vnd.zul": "Z.U.L. Geometry",
|
||||
"application/zip": "Zip Archive",
|
||||
"application/vnd.handheld-entertainment+xml": "ZVUE Media Manager",
|
||||
"application/vnd.zzazz.deck+xml": "Zzazz Deck"
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
{{ activity.action_by.first_name }} {{ activity.action_by.last_name }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="activity.action_by && action.action_by">
|
||||
<template v-else>
|
||||
{{ $t('private_user') }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -4,14 +4,25 @@
|
||||
|
||||
<v-textarea v-if="editing" v-model="edits">
|
||||
<template #append>
|
||||
<v-button :loading="savingEdits" class="post-comment" @click="saveEdits" x-small>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
<div class="buttons">
|
||||
<v-button class="cancel" @click="cancelEditing" secondary x-small>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
:loading="savingEdits"
|
||||
class="post-comment"
|
||||
@click="saveEdits"
|
||||
x-small
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
</div>
|
||||
</template>
|
||||
</v-textarea>
|
||||
|
||||
<div v-else class="content">
|
||||
<span v-html="htmlContent" />
|
||||
<span v-html="htmlContent" class="selectable" />
|
||||
|
||||
<!-- @TODO: Dynamically add element below if the comment overflows -->
|
||||
<!-- <div v-if="activity.id == 204" class="expand-text">
|
||||
@@ -48,9 +59,9 @@ export default defineComponent({
|
||||
props.activity.comment ? marked(props.activity.comment) : null
|
||||
);
|
||||
|
||||
const { edits, editing, savingEdits, saveEdits } = useEdits();
|
||||
const { edits, editing, savingEdits, saveEdits, cancelEditing } = useEdits();
|
||||
|
||||
return { htmlContent, edits, editing, savingEdits, saveEdits };
|
||||
return { htmlContent, edits, editing, savingEdits, saveEdits, cancelEditing };
|
||||
|
||||
function useEdits() {
|
||||
const edits = ref(props.activity.comment);
|
||||
@@ -62,7 +73,7 @@ export default defineComponent({
|
||||
() => (edits.value = props.activity.comment)
|
||||
);
|
||||
|
||||
return { edits, editing, savingEdits, saveEdits };
|
||||
return { edits, editing, savingEdits, saveEdits, cancelEditing };
|
||||
|
||||
async function saveEdits() {
|
||||
const { currentProjectKey } = projectsStore.state;
|
||||
@@ -80,6 +91,11 @@ export default defineComponent({
|
||||
editing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditing() {
|
||||
edits.value = props.activity.comment;
|
||||
editing.value = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -196,9 +212,13 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.post-comment {
|
||||
.buttons {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 8px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import markdown from './readme.md';
|
||||
import { defineComponent, provide, toRefs } from '@vue/composition-api';
|
||||
import { defineComponent, watch } from '@vue/composition-api';
|
||||
import { withKnobs, boolean } from '@storybook/addon-knobs';
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../../.storybook/decorators/with-alt-colors';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
import DrawerButton from './drawer-button.vue';
|
||||
|
||||
@@ -23,7 +24,16 @@ export const basic = () =>
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
provide('drawer-open', toRefs(props).drawerOpen);
|
||||
const appStore = useAppStore();
|
||||
|
||||
appStore.state.drawerOpen = props.drawerOpen;
|
||||
|
||||
watch(
|
||||
() => props.drawerOpen,
|
||||
(newOpen) => {
|
||||
appStore.state.drawerOpen = newOpen;
|
||||
}
|
||||
);
|
||||
},
|
||||
template: `
|
||||
<drawer-button icon="info">Close Drawer</drawer-button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import VueCompositionAPI from '@vue/composition-api';
|
||||
import VIcon from '@/components/v-icon/';
|
||||
import DrawerButton from './drawer-button.vue';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
@@ -9,11 +10,11 @@ localVue.component('v-icon', VIcon);
|
||||
|
||||
describe('Views / Private / Components / Drawer Button', () => {
|
||||
it('Does not render the title when the drawer is closed', () => {
|
||||
const appStore = useAppStore();
|
||||
|
||||
appStore.state.drawerOpen = false;
|
||||
const component = shallowMount(DrawerButton, {
|
||||
localVue,
|
||||
provide: {
|
||||
'drawer-open': false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.find('.title').exists()).toBe(false);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<div class="icon">
|
||||
<v-icon :name="icon" />
|
||||
<v-icon :name="icon" outline />
|
||||
</div>
|
||||
<div class="title" v-if="drawerOpen">
|
||||
<slot />
|
||||
@@ -15,7 +15,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, ref } from '@vue/composition-api';
|
||||
import { defineComponent, toRefs } from '@vue/composition-api';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -33,7 +34,8 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const drawerOpen = inject('drawer-open', ref(false));
|
||||
const appStore = useAppStore();
|
||||
const { drawerOpen } = toRefs(appStore.state);
|
||||
|
||||
return { drawerOpen };
|
||||
},
|
||||
|
||||
@@ -3,9 +3,10 @@ import markdown from './readme.md';
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../../.storybook/decorators/with-alt-colors';
|
||||
|
||||
import { defineComponent, provide } from '@vue/composition-api';
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import DrawerDetailGroup from './drawer-detail-group.vue';
|
||||
import DrawerDetail from '../drawer-detail';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Components / Drawer Detail Group',
|
||||
@@ -19,7 +20,8 @@ export const basic = () =>
|
||||
defineComponent({
|
||||
components: { DrawerDetailGroup, DrawerDetail },
|
||||
setup() {
|
||||
provide('drawer-open', true);
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.drawerOpen = false;
|
||||
},
|
||||
template: `
|
||||
<drawer-detail-group>
|
||||
|
||||
@@ -3,6 +3,7 @@ import markdown from './readme.md';
|
||||
import { defineComponent, provide, ref, watch } from '@vue/composition-api';
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import withAltColors from '../../../../../.storybook/decorators/with-alt-colors';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Components / Drawer Detail',
|
||||
@@ -27,7 +28,9 @@ export const basic = () =>
|
||||
},
|
||||
setup(props) {
|
||||
const open = ref(false);
|
||||
provide('drawer-open', open);
|
||||
const appStore = useAppStore();
|
||||
|
||||
appStore.state.drawerOpen = true;
|
||||
|
||||
watch(
|
||||
() => props.drawerOpen,
|
||||
|
||||
@@ -6,6 +6,7 @@ import VIcon from '@/components/v-icon';
|
||||
import VDivider from '@/components/v-divider';
|
||||
import TransitionExpand from '@/components/transition/expand';
|
||||
import VBadge from '@/components/v-badge/';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
@@ -18,6 +19,9 @@ describe('Drawer Detail', () => {
|
||||
it('Uses the useGroupable composition', () => {
|
||||
jest.spyOn(GroupableComposable, 'useGroupable');
|
||||
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.drawerOpen = false;
|
||||
|
||||
mount(DrawerDetail, {
|
||||
localVue,
|
||||
propsData: {
|
||||
@@ -25,7 +29,6 @@ describe('Drawer Detail', () => {
|
||||
title: 'Users',
|
||||
},
|
||||
provide: {
|
||||
'drawer-open': ref(false),
|
||||
'item-group': {
|
||||
register: () => {},
|
||||
unregister: () => {},
|
||||
@@ -39,6 +42,9 @@ describe('Drawer Detail', () => {
|
||||
it('Passes the title prop as selection value', () => {
|
||||
jest.spyOn(GroupableComposable, 'useGroupable');
|
||||
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.drawerOpen = false;
|
||||
|
||||
mount(DrawerDetail, {
|
||||
localVue,
|
||||
propsData: {
|
||||
@@ -46,7 +52,6 @@ describe('Drawer Detail', () => {
|
||||
title: 'Users',
|
||||
},
|
||||
provide: {
|
||||
'drawer-open': ref(false),
|
||||
'item-group': {
|
||||
register: () => {},
|
||||
unregister: () => {},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<button class="toggle" @click="toggle" :class="{ open: active }">
|
||||
<div class="icon">
|
||||
<v-badge bordered :value="badge" :disabled="!badge">
|
||||
<v-icon :name="icon" />
|
||||
<v-icon :name="icon" outline />
|
||||
</v-badge>
|
||||
</div>
|
||||
<div class="title" v-show="drawerOpen">
|
||||
@@ -21,7 +21,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, inject } from '@vue/composition-api';
|
||||
import { defineComponent, toRefs } from '@vue/composition-api';
|
||||
import useAppStore from '@/stores/app';
|
||||
import { useGroupable } from '@/composables/groupable';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -41,7 +42,8 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const { active, toggle } = useGroupable(props.title, 'drawer-detail');
|
||||
const drawerOpen = inject('drawer-open', ref(false));
|
||||
const appStore = useAppStore();
|
||||
const { drawerOpen } = toRefs(appStore.state);
|
||||
return { active, toggle, drawerOpen };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:width="file.width"
|
||||
:height="file.height"
|
||||
:title="file.title"
|
||||
in-modal
|
||||
@click="_active = false"
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="file-preview" v-if="type">
|
||||
<div v-if="type === 'image'" class="image" :class="{ svg: isSVG }" @click="$emit('click')">
|
||||
<img :src="src" :width="width" :height="height" :alt="title" />
|
||||
<v-icon name="fullscreen" />
|
||||
<v-icon v-if="inModal === false" name="fullscreen" />
|
||||
</div>
|
||||
|
||||
<video v-else-if="type === 'video'" controls :src="src" />
|
||||
@@ -36,6 +36,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inModal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const type = computed<'image' | 'video' | 'audio' | null>(() => {
|
||||
@@ -98,7 +102,7 @@ audio {
|
||||
bottom: 12px;
|
||||
z-index: 2;
|
||||
color: white;
|
||||
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);
|
||||
text-shadow: 0px 0px 8px rgba(0, 0, 0, 0.75);
|
||||
opacity: 0;
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
}
|
||||
|
||||
@@ -105,31 +105,21 @@ export default defineComponent({
|
||||
|
||||
.title-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 70%;
|
||||
height: 100%;
|
||||
margin-left: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&.full {
|
||||
margin-right: 20px;
|
||||
padding-right: 20px;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--background-page-rgb), 0) 0%,
|
||||
rgba(var(--background-page-rgb), 1) 100%
|
||||
);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.headline {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
transition: opacity var(--fast) var(--transition);
|
||||
@@ -139,9 +129,12 @@ export default defineComponent({
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
.type-title {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,25 +31,40 @@
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<v-icon name="rotate_90_degrees_ccw" @click="rotate" v-tooltip.top="$t('rotate')" />
|
||||
<div
|
||||
v-tooltip.bottom.inverted="$t('drag_mode')"
|
||||
class="drag-mode toolbar-button"
|
||||
@click="dragMode = dragMode === 'crop' ? 'move' : 'crop'"
|
||||
>
|
||||
<v-icon name="pan_tool" :class="{ active: dragMode === 'move' }" />
|
||||
<v-icon name="crop" :class="{ active: dragMode === 'crop' }" />
|
||||
</div>
|
||||
|
||||
<v-icon
|
||||
name="rotate_90_degrees_ccw"
|
||||
@click="rotate"
|
||||
v-tooltip.bottom.inverted="$t('rotate')"
|
||||
/>
|
||||
|
||||
<v-icon
|
||||
name="flip_horizontal"
|
||||
@click="flip('horizontal')"
|
||||
v-tooltip.top="$t('flip_horizontal')"
|
||||
v-tooltip.bottom.inverted="$t('flip_horizontal')"
|
||||
/>
|
||||
|
||||
<v-icon
|
||||
name="flip_vertical"
|
||||
@click="flip('vertical')"
|
||||
v-tooltip.top="$t('flip_vertical')"
|
||||
v-tooltip.bottom.inverted="$t('flip_vertical')"
|
||||
/>
|
||||
<v-menu
|
||||
placement="top"
|
||||
show-arrow
|
||||
close-on-content-click
|
||||
v-tooltip.top="$t('aspect_ratio')"
|
||||
>
|
||||
|
||||
<v-menu placement="top" show-arrow close-on-content-click>
|
||||
<template #activator="{ toggle }">
|
||||
<v-icon :name="aspectRatioIcon" @click="toggle" />
|
||||
<v-icon
|
||||
:name="aspectRatioIcon"
|
||||
@click="toggle"
|
||||
v-tooltip.bottom.inverted="$t('aspect_ratio')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
@@ -77,12 +92,41 @@
|
||||
<v-list-item-icon><v-icon name="crop_free" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('free') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-if="imageData"
|
||||
@click="aspectRatio = imageData.width / imageData.height"
|
||||
:active="aspectRatio === imageData.width / imageData.height"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="crop_original" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('original') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<button class="toolbar-button cancel" v-show="cropping" @click="cropping = false">
|
||||
{{ $t('cancel_crop') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<div class="dimensions" v-if="imageData">
|
||||
<v-icon name="info_outline" />
|
||||
{{ $n(imageData.width) }}x{{ $n(imageData.height) }}
|
||||
<template
|
||||
v-if="
|
||||
(imageData.width !== newDimensions.width ||
|
||||
imageData.height !== newDimensions.height)
|
||||
"
|
||||
>
|
||||
->
|
||||
{{ $n(newDimensions.width) }}x{{ $n(newDimensions.height) }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="spacer" />
|
||||
<v-button @click="close" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="save" :loading="saving">{{ $t('save') }}</v-button>
|
||||
</template>
|
||||
@@ -90,11 +134,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, computed } from '@vue/composition-api';
|
||||
import { defineComponent, ref, watch, computed, reactive } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import Cropper from 'cropperjs';
|
||||
import { nanoid } from 'nanoid';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
type Image = {
|
||||
type: string;
|
||||
@@ -103,6 +148,8 @@ type Image = {
|
||||
};
|
||||
filesize: number;
|
||||
filename_download: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
@@ -153,6 +200,9 @@ export default defineComponent({
|
||||
rotate,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
} = useCropper();
|
||||
|
||||
watch(_active, (isActive) => {
|
||||
@@ -187,6 +237,9 @@ export default defineComponent({
|
||||
aspectRatioIcon,
|
||||
saving,
|
||||
imageURL,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
};
|
||||
|
||||
function useImage() {
|
||||
@@ -215,7 +268,14 @@ export default defineComponent({
|
||||
loading.value = true;
|
||||
const response = await api.get(`/${currentProjectKey}/files/${props.id}`, {
|
||||
params: {
|
||||
fields: ['data', 'type', 'filesize', 'filename_download'],
|
||||
fields: [
|
||||
'data',
|
||||
'type',
|
||||
'filesize',
|
||||
'filename_download',
|
||||
'width',
|
||||
'height',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -266,6 +326,18 @@ export default defineComponent({
|
||||
|
||||
const localAspectRatio = ref(NaN);
|
||||
|
||||
const newDimensions = reactive({
|
||||
width: null as null | number,
|
||||
height: null as null | number,
|
||||
});
|
||||
|
||||
watch(imageData, () => {
|
||||
if (!imageData.value) return;
|
||||
localAspectRatio.value = imageData.value.width / imageData.value.height;
|
||||
newDimensions.width = imageData.value.width;
|
||||
newDimensions.height = imageData.value.height;
|
||||
});
|
||||
|
||||
const aspectRatio = computed<number>({
|
||||
get() {
|
||||
return localAspectRatio.value;
|
||||
@@ -277,6 +349,8 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const aspectRatioIcon = computed(() => {
|
||||
if (!imageData.value) return 'crop_original';
|
||||
|
||||
switch (aspectRatio.value) {
|
||||
case 16 / 9:
|
||||
return 'crop_16_9';
|
||||
@@ -288,13 +362,51 @@ export default defineComponent({
|
||||
return 'crop_7_5';
|
||||
case 1 / 1:
|
||||
return 'crop_square';
|
||||
case imageData.value.width / imageData.value.height:
|
||||
return 'crop_original';
|
||||
case NaN:
|
||||
default:
|
||||
return 'crop_free';
|
||||
}
|
||||
});
|
||||
|
||||
return { cropperInstance, initCropper, flip, rotate, aspectRatio, aspectRatioIcon };
|
||||
const localDragMode = ref<'move' | 'crop'>('move');
|
||||
|
||||
const dragMode = computed({
|
||||
get() {
|
||||
return localDragMode.value;
|
||||
},
|
||||
set(newMode: 'move' | 'crop') {
|
||||
cropperInstance.value?.setDragMode(newMode);
|
||||
localDragMode.value = newMode;
|
||||
},
|
||||
});
|
||||
|
||||
const localCropping = ref(false);
|
||||
const cropping = computed({
|
||||
get() {
|
||||
return localCropping.value;
|
||||
},
|
||||
set(newCropping: boolean) {
|
||||
if (newCropping === false) {
|
||||
cropperInstance.value?.clear();
|
||||
}
|
||||
|
||||
localCropping.value = newCropping;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
cropperInstance,
|
||||
initCropper,
|
||||
flip,
|
||||
rotate,
|
||||
aspectRatio,
|
||||
aspectRatioIcon,
|
||||
newDimensions,
|
||||
dragMode,
|
||||
cropping,
|
||||
};
|
||||
|
||||
function initCropper() {
|
||||
if (imageElement.value === null) return;
|
||||
@@ -303,7 +415,35 @@ export default defineComponent({
|
||||
cropperInstance.value.destroy();
|
||||
}
|
||||
|
||||
cropperInstance.value = new Cropper(imageElement.value, { autoCrop: false });
|
||||
if (!imageData.value) return;
|
||||
|
||||
cropperInstance.value = new Cropper(imageElement.value, {
|
||||
autoCrop: false,
|
||||
aspectRatio: imageData.value.width / imageData.value.height,
|
||||
toggleDragModeOnDblclick: false,
|
||||
dragMode: 'move',
|
||||
crop: throttle((event) => {
|
||||
if (!imageData.value) return;
|
||||
|
||||
if (
|
||||
cropping.value === false &&
|
||||
(event.detail.width || event.detail.height)
|
||||
) {
|
||||
cropping.value = true;
|
||||
}
|
||||
|
||||
const newWidth = event.detail.width || imageData.value.width;
|
||||
const newHeight = event.detail.height || imageData.value.height;
|
||||
|
||||
if (event.detail.rotate === 0 || event.detail.rotate === -180) {
|
||||
newDimensions.width = Math.round(newWidth);
|
||||
newDimensions.height = Math.round(newHeight);
|
||||
} else {
|
||||
newDimensions.height = Math.round(newWidth);
|
||||
newDimensions.width = Math.round(newHeight);
|
||||
}
|
||||
}, 50),
|
||||
});
|
||||
}
|
||||
|
||||
function flip(type: 'horizontal' | 'vertical') {
|
||||
@@ -367,19 +507,63 @@ export default defineComponent({
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
padding: 0 24px;
|
||||
color: var(--white);
|
||||
background-color: #263238;
|
||||
|
||||
> * {
|
||||
.v-icon {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dimensions {
|
||||
color: var(--foreground-subdued);
|
||||
font-feature-settings: 'tnum';
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
padding: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color var(--fast) var(--transition);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.drag-mode {
|
||||
margin-right: 16px;
|
||||
margin-left: -8px;
|
||||
|
||||
.v-icon {
|
||||
margin-right: 0;
|
||||
opacity: 0.5;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.v-icon:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<v-modal v-model="_active" :title="$t('select_item')" no-padding>
|
||||
<layout-tabular
|
||||
class="layout"
|
||||
<component
|
||||
:is="`layout-${layout}`"
|
||||
:collection="collection"
|
||||
:selection="_selection"
|
||||
:filters="filters"
|
||||
:view-query.sync="query"
|
||||
:view-options.sync="options"
|
||||
@update:selection="onSelect"
|
||||
select-mode
|
||||
/>
|
||||
@@ -17,8 +19,16 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
|
||||
import {
|
||||
defineComponent,
|
||||
PropType,
|
||||
ref,
|
||||
computed,
|
||||
toRefs,
|
||||
onUnmounted,
|
||||
} from '@vue/composition-api';
|
||||
import { Filter } from '@/stores/collection-presets/types';
|
||||
import useCollectionPreset from '@/composables/use-collection-preset';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -48,7 +58,17 @@ export default defineComponent({
|
||||
const { _active } = useActiveState();
|
||||
const { _selection, onSelect } = useSelection();
|
||||
|
||||
return { save, cancel, _active, _selection, onSelect };
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { viewType, viewOptions, viewQuery } = useCollectionPreset(collection);
|
||||
|
||||
// This is a local copy of the viewtype. This means that we can sync it the layout without
|
||||
// having use-collection-preset auto-save the values
|
||||
const layout = ref(viewType.value || 'tabular');
|
||||
const options = ref(viewOptions.value);
|
||||
const query = ref(viewQuery.value);
|
||||
|
||||
return { save, cancel, _active, _selection, onSelect, layout, options, query };
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
@@ -69,6 +89,10 @@ export default defineComponent({
|
||||
function useSelection() {
|
||||
const localSelection = ref<(string | number)[]>(null);
|
||||
|
||||
onUnmounted(() => {
|
||||
localSelection.value = null;
|
||||
});
|
||||
|
||||
const _selection = computed({
|
||||
get() {
|
||||
if (localSelection.value === null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-modal v-model="_active" :title="$t('editing_in', { collection })" persistent>
|
||||
<v-modal v-model="_active" :title="title" persistent form-width>
|
||||
<v-form
|
||||
:loading="loading"
|
||||
:initial-values="item"
|
||||
@@ -16,9 +16,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, PropType, watch } from '@vue/composition-api';
|
||||
import { defineComponent, ref, computed, PropType, watch, toRefs } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import useProjectsStore from '@/stores/projects';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
@@ -49,7 +51,19 @@ export default defineComponent({
|
||||
const { _edits, loading, error, item } = useItem();
|
||||
const { save, cancel } = useActions();
|
||||
|
||||
return { _active, _edits, loading, error, item, save, cancel };
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
|
||||
const title = computed(() => {
|
||||
if (props.primaryKey === '+') {
|
||||
return i18n.t('adding_in', { collection: collectionInfo.value?.name });
|
||||
}
|
||||
|
||||
return i18n.t('editing_in', { collection: collectionInfo.value?.name });
|
||||
});
|
||||
|
||||
return { _active, _edits, loading, error, item, save, cancel, title };
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
@@ -85,7 +85,7 @@ export default defineComponent({
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
padding: 12px;
|
||||
color: var(--white);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import readme from './readme.md';
|
||||
import { defineComponent, ref, provide } from '@vue/composition-api';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import NotificationsPreview from './notifications-preview.vue';
|
||||
import NotificationItem from '../notification-item/';
|
||||
import DrawerButton from '../drawer-button/';
|
||||
@@ -8,6 +8,7 @@ import { NotificationRaw } from '@/stores/notifications/types';
|
||||
import { i18n } from '@/lang';
|
||||
import withPadding from '../../../../../.storybook/decorators/with-padding';
|
||||
import VueRouter from 'vue-router';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
export default {
|
||||
title: 'Views / Private / Components / Notifications Preview',
|
||||
@@ -52,7 +53,8 @@ export const basic = () =>
|
||||
const notificationsStore = useNotificationsStore({});
|
||||
const active = ref(false);
|
||||
|
||||
provide('drawer-open', ref(true));
|
||||
const appStore = useAppStore({});
|
||||
appStore.state.drawerOpen = true;
|
||||
|
||||
return { add, active };
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
<transition-expand tag="div">
|
||||
<div v-if="active" class="inline">
|
||||
<div class="padding-box">
|
||||
<router-link class="link" :to="activityLink">
|
||||
<router-link
|
||||
class="link"
|
||||
:to="activityLink"
|
||||
:class="{ 'has-items': lastFour.length > 0 }"
|
||||
>
|
||||
{{ $t('show_all_activity') }}
|
||||
</router-link>
|
||||
<transition-group tag="div" name="notification" class="transition">
|
||||
@@ -19,7 +23,7 @@
|
||||
|
||||
<drawer-button
|
||||
:active="active"
|
||||
@click="$emit('toggle', !active)"
|
||||
@click="active = !active"
|
||||
v-tooltip.left="$t('notifications')"
|
||||
class="toggle"
|
||||
icon="notifications"
|
||||
@@ -30,7 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { defineComponent, computed, ref, watch } from '@vue/composition-api';
|
||||
import DrawerButton from '../drawer-button';
|
||||
import NotificationItem from '../notification-item';
|
||||
import useNotificationsStore from '@/stores/notifications';
|
||||
@@ -38,22 +42,28 @@ import useProjectsStore from '@/stores/projects';
|
||||
|
||||
export default defineComponent({
|
||||
components: { DrawerButton, NotificationItem },
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
drawerOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
setup(props) {
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const activityLink = computed(() => `/${projectsStore.state.currentProjectKey}/activity`);
|
||||
const active = ref(false);
|
||||
|
||||
return { lastFour: notificationsStore.lastFour, activityLink };
|
||||
watch(
|
||||
() => props.drawerOpen,
|
||||
(open: boolean) => {
|
||||
if (open === false) {
|
||||
active.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { lastFour: notificationsStore.lastFour, activityLink, active };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -72,6 +82,10 @@ export default defineComponent({
|
||||
&:hover {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
&.has-items {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.transition {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<value-null v-if="value === null || value === undefined" />
|
||||
<span v-else-if="displayInfo === null">{{ value }}</span>
|
||||
<span v-else-if="typeof displayInfo.handler === 'function'">
|
||||
<span v-else-if="displayInfo === null" class="no-wrap">{{ value }}</span>
|
||||
<span v-else-if="typeof displayInfo.handler === 'function'" class="no-wrap">
|
||||
{{ displayInfo.handler(value, options, { type }) }}
|
||||
</span>
|
||||
<component
|
||||
@@ -66,3 +66,12 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/no-wrap.scss';
|
||||
|
||||
.no-wrap {
|
||||
line-height: 22px;
|
||||
@include no-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:type="part.type"
|
||||
v-bind="part.options"
|
||||
/>
|
||||
<span v-else :key="index" class="raw">{{ part }}</span>
|
||||
<span :key="index" v-else>{{ part + ' ' }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -96,15 +96,22 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/no-wrap';
|
||||
|
||||
.render-template {
|
||||
display: contents;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
|
||||
& > * {
|
||||
margin-right: 6px;
|
||||
|
||||
@include no-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.subdued {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.raw:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -107,6 +107,7 @@ export default defineComponent({
|
||||
postComment,
|
||||
saving,
|
||||
getFormattedTime,
|
||||
refresh,
|
||||
};
|
||||
|
||||
function getFormattedTime(datetime: string) {
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.null {
|
||||
color: var(--foreground-subdued);
|
||||
color: var(--border-normal); // Don't confuse NULL with subdued value
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user