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:
Rijk van Zanten
2020-05-15 18:44:21 -04:00
committed by GitHub
parent 8a9daf554f
commit feaafe6440
104 changed files with 2081 additions and 832 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: () => {},

View File

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

View File

@@ -13,6 +13,7 @@
:width="file.width"
:height="file.height"
:title="file.title"
in-modal
@click="_active = false"
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -107,6 +107,7 @@ export default defineComponent({
postComment,
saving,
getFormattedTime,
refresh,
};
function getFormattedTime(datetime: string) {

View File

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

View File

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

View File

@@ -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() {

View File

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