mirror of
https://github.com/directus/directus.git
synced 2026-02-19 10:14:33 -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:
@@ -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>
|
||||
|
||||
@@ -32,22 +32,4 @@ describe('Views / Private', () => {
|
||||
|
||||
expect(component.find('.navigation').classes()).toEqual(['navigation', 'is-open']);
|
||||
});
|
||||
|
||||
it('Adds the is-open class to the drawer', async () => {
|
||||
const component = shallowMount(PrivateView, {
|
||||
localVue,
|
||||
i18n,
|
||||
propsData: {
|
||||
title: 'Title',
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors']);
|
||||
|
||||
(component.vm as any).drawerOpen = true;
|
||||
|
||||
await component.vm.$nextTick();
|
||||
|
||||
expect(component.find('.drawer').classes()).toEqual(['drawer', 'alt-colors', 'is-open']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,13 +54,13 @@
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<notifications-preview v-model="navigationsInline" />
|
||||
<notifications-preview :drawer-open="drawerOpen" />
|
||||
</aside>
|
||||
|
||||
<v-overlay class="nav-overlay" :active="navOpen" @click="navOpen = false" />
|
||||
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
|
||||
|
||||
<notifications-group v-if="navigationsInline === false" :dense="drawerOpen === false" />
|
||||
<notifications-group v-if="drawerOpen === false" :dense="drawerOpen === false" />
|
||||
|
||||
<template v-if="showDropEffect">
|
||||
<div class="drop-border top" />
|
||||
@@ -72,7 +72,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, provide, watch, computed } from '@vue/composition-api';
|
||||
import { defineComponent, ref, provide, toRefs, computed } from '@vue/composition-api';
|
||||
import ModuleBar from './components/module-bar/';
|
||||
import DrawerDetailGroup from './components/drawer-detail-group/';
|
||||
import HeaderBar from './components/header-bar';
|
||||
@@ -86,6 +86,7 @@ import useNotificationsStore from '@/stores/notifications';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
import i18n from '@/lang';
|
||||
import useEventListener from '@/composables/use-event-listener';
|
||||
import useAppStore from '@/stores/app';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -106,23 +107,17 @@ export default defineComponent({
|
||||
},
|
||||
setup() {
|
||||
const navOpen = ref(false);
|
||||
const drawerOpen = ref(false);
|
||||
const contentEl = ref<Element>();
|
||||
const navigationsInline = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const { drawerOpen } = toRefs(appStore.state);
|
||||
|
||||
const theme = computed(() => {
|
||||
return userStore.state.currentUser?.theme || 'auto';
|
||||
});
|
||||
|
||||
watch(drawerOpen, (open: boolean) => {
|
||||
if (open === false) {
|
||||
navigationsInline.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
provide('drawer-open', drawerOpen);
|
||||
provide('main-element', contentEl);
|
||||
|
||||
const {
|
||||
@@ -141,15 +136,14 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
navOpen,
|
||||
drawerOpen,
|
||||
contentEl,
|
||||
navigationsInline,
|
||||
theme,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
showDropEffect,
|
||||
onDrop,
|
||||
dragging,
|
||||
drawerOpen,
|
||||
};
|
||||
|
||||
function useFileUpload() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ProjectWithKey } from '@/stores/projects/types';
|
||||
import ClickOutside from '@/directives/click-outside';
|
||||
import VMenu from '@/components/v-menu';
|
||||
import VList, { VListItem, VListItemIcon, VListItemContent } from '@/components/v-list';
|
||||
import PortalVue from 'portal-vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueCompositionAPI);
|
||||
@@ -17,6 +18,7 @@ localVue.component('v-list-item-icon', VListItemIcon);
|
||||
localVue.component('v-list-item-content', VListItemContent);
|
||||
localVue.component('v-menu', VMenu);
|
||||
localVue.directive('click-outside', ClickOutside);
|
||||
localVue.use(PortalVue);
|
||||
|
||||
import PublicView from './public-view.vue';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user