Merge branch 'main' into insights

This commit is contained in:
rijkvanzanten
2021-06-11 19:07:53 -04:00
545 changed files with 37116 additions and 32568 deletions

View File

@@ -52,9 +52,7 @@ export const onError = async (error: RequestError): Promise<RequestError> => {
// access, or that your session doesn't exist / has expired.
// In case of the second, we should force the app to logout completely and redirect to the login
// view.
/* istanbul ignore next */
const status = error.response?.status;
/* istanbul ignore next */
const code = error.response?.data?.errors?.[0]?.extensions?.code;
if (

View File

@@ -1,13 +1,13 @@
<template>
<div id="app" :style="brandStyle">
<div id="directus" :style="brandStyle">
<transition name="fade">
<div class="hydrating" v-if="hydrating">
<v-progress-circular indeterminate />
</div>
</transition>
<v-info v-if="error" type="danger" :title="$t('unexpected_error')" icon="error" center>
{{ $t('unexpected_error_copy') }}
<v-info v-if="error" type="danger" :title="t('unexpected_error')" icon="error" center>
{{ t('unexpected_error_copy') }}
<template #append>
<v-error :error="error" />
@@ -16,15 +16,13 @@
<router-view v-else-if="!hydrating" />
<portal-target name="dialog-outlet" transition="transition-dialog" multiple />
<portal-target name="menu-outlet" transition="transition-bounce" multiple />
<mounting-portal mount-to="#custom-css" target-tag="style">{{ customCSS }}</mounting-portal>
<teleport to="#custom-css">{{ customCSS }}</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs, watch, computed, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, toRefs, watch, computed, provide } from 'vue';
import * as stores from '@/stores';
import api, { addTokenToURL } from '@/api';
import axios from 'axios';
@@ -34,27 +32,26 @@ import setFavicon from '@/utils/set-favicon';
export default defineComponent({
setup() {
const { t } = useI18n();
const { useAppStore, useUserStore, useServerStore } = stores;
const appStore = useAppStore();
const userStore = useUserStore();
const serverStore = useServerStore();
const { hydrating, sidebarOpen } = toRefs(appStore.state);
const { hydrating, sidebarOpen } = toRefs(appStore);
const brandStyle = computed(() => {
return {
'--brand': serverStore.state.info?.project?.project_color || 'var(--primary)',
'--brand': serverStore.info?.project?.project_color || 'var(--primary)',
};
});
watch(
[() => serverStore.state.info?.project?.project_color, () => serverStore.state.info?.project?.project_logo],
() => {
const hasCustomLogo = !!serverStore.state.info?.project?.project_logo;
setFavicon(serverStore.state.info?.project?.project_color || '#00C897', hasCustomLogo);
}
);
watch([() => serverStore.info?.project?.project_color, () => serverStore.info?.project?.project_logo], () => {
const hasCustomLogo = !!serverStore.info?.project?.project_logo;
setFavicon(serverStore.info?.project?.project_color || '#00C897', hasCustomLogo);
});
const { width } = useWindowSize();
@@ -74,7 +71,7 @@ export default defineComponent({
);
watch(
() => userStore.state.currentUser,
() => userStore.currentUser,
(newUser) => {
document.body.classList.remove('dark');
document.body.classList.remove('light');
@@ -93,17 +90,17 @@ export default defineComponent({
);
watch(
() => serverStore.state.info?.project?.project_name,
() => serverStore.info?.project?.project_name,
(projectName) => {
document.title = projectName || 'Directus';
}
);
const customCSS = computed(() => {
return serverStore.state?.info?.project?.custom_css || '';
return serverStore.info?.project?.custom_css || '';
});
const error = computed(() => appStore.state.error);
const error = computed(() => appStore.error);
/**
* This allows custom extensions to use the apps internals
@@ -115,13 +112,17 @@ export default defineComponent({
addTokenToURL,
});
return { hydrating, brandStyle, error, customCSS };
return { t, hydrating, brandStyle, error, customCSS };
},
});
</script>
<style lang="scss" scoped>
#app {
:global(#app) {
height: 100%;
}
#directus {
height: 100%;
}
@@ -142,7 +143,7 @@ export default defineComponent({
transition: opacity var(--medium) var(--transition);
}
.fade-enter,
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@@ -1,8 +1,8 @@
import api from '@/api';
import { dehydrate, hydrate } from '@/hydrate';
import router from '@/router';
import { router } from '@/router';
import { useAppStore } from '@/stores';
import { RawLocation } from 'vue-router';
import { RouteLocationRaw } from 'vue-router';
export type LoginCredentials = {
email: string;
@@ -31,7 +31,7 @@ export async function login(credentials: LoginCredentials): Promise<void> {
setTimeout(() => refresh(), response.data.data.expires - 10000);
}
appStore.state.authenticated = true;
appStore.authenticated = true;
await hydrate();
}
@@ -58,7 +58,7 @@ export async function refresh({ navigate }: LogoutOptions = { navigate: true }):
if (response.data.data.expires <= 2100000000) {
refreshTimeout = setTimeout(() => refresh(), response.data.data.expires - 10000);
}
appStore.state.authenticated = true;
appStore.authenticated = true;
return accessToken;
} catch (error) {
@@ -96,12 +96,12 @@ export async function logout(optionsRaw: LogoutOptions = {}): Promise<void> {
await api.post(`/auth/logout`);
}
appStore.state.authenticated = false;
appStore.authenticated = false;
await dehydrate();
if (options.navigate === true) {
const location: RawLocation = {
const location: RouteLocationRaw = {
path: `/login`,
query: { reason: options.reason },
};

View File

@@ -1,17 +1,17 @@
import ExportSidebarDetail from '@/views/private/components/export-sidebar-detail';
import FilterSidebarDetail from '@/views/private/components/filter-sidebar-detail';
import RenderDisplay from '@/views/private/components/render-display';
import RenderTemplate from '@/views/private/components/render-template';
import SidebarDetail from '@/views/private/components/sidebar-detail/';
import UserPopover from '@/views/private/components/user-popover';
import ValueNull from '@/views/private/components/value-null';
import Vue from 'vue';
import { App } from 'vue';
import TransitionBounce from './transition/bounce';
import TransitionDialog from './transition/dialog';
import TransitionExpand from './transition/expand';
import VAvatar from './v-avatar/';
import VBadge from './v-badge/';
import VBreadcrumb from './v-breadcrumb';
import VButtonGroup from './v-button-group/';
import VButton from './v-button/';
import VCard, { VCardActions, VCardSubtitle, VCardText, VCardTitle } from './v-card';
import VCheckbox from './v-checkbox/';
@@ -49,67 +49,69 @@ import VTextOverflow from './v-text-overflow.vue';
import VTextarea from './v-textarea';
import VUpload from './v-upload';
Vue.component('v-avatar', VAvatar);
Vue.component('v-badge', VBadge);
Vue.component('v-breadcrumb', VBreadcrumb);
Vue.component('v-button', VButton);
Vue.component('v-button-group', VButtonGroup);
Vue.component('v-card-actions', VCardActions);
Vue.component('v-card-subtitle', VCardSubtitle);
Vue.component('v-card-text', VCardText);
Vue.component('v-card-title', VCardTitle);
Vue.component('v-card', VCard);
Vue.component('v-checkbox', VCheckbox);
Vue.component('v-chip', VChip);
Vue.component('v-detail', VDetail);
Vue.component('v-dialog', VDialog);
Vue.component('v-divider', VDivider);
Vue.component('v-error', VError);
Vue.component('v-fancy-select', VFancySelect);
Vue.component('v-field-template', VFieldTemplate);
Vue.component('v-field-select', VFieldSelect);
Vue.component('v-form', VForm);
Vue.component('v-hover', VHover);
Vue.component('v-icon', VIcon);
Vue.component('v-info', VInfo);
Vue.component('v-input', VInput);
Vue.component('v-item-group', VItemGroup);
Vue.component('v-item', VItem);
Vue.component('v-list-group', VListGroup);
Vue.component('v-list-item-content', VListItemContent);
Vue.component('v-list-item-hint', VListItemHint);
Vue.component('v-list-item-icon', VListItemIcon);
Vue.component('v-list-item', VListItem);
Vue.component('v-list', VList);
Vue.component('v-menu', VMenu);
Vue.component('v-drawer', VDrawer);
Vue.component('v-notice', VNotice);
Vue.component('v-overlay', VOverlay);
Vue.component('v-pagination', VPagination);
Vue.component('v-progress-circular', VProgressCircular);
Vue.component('v-progress-linear', VProgressLinear);
Vue.component('v-radio', VRadio);
Vue.component('v-select', VSelect);
Vue.component('v-sheet', VSheet);
Vue.component('v-skeleton-loader', VSkeletonLoader);
Vue.component('v-slider', VSlider);
Vue.component('v-switch', VSwitch);
Vue.component('v-tab-item', VTabItem);
Vue.component('v-tab', VTab);
Vue.component('v-table', VTable);
Vue.component('v-tabs-items', VTabsItems);
Vue.component('v-tabs', VTabs);
Vue.component('v-textarea', VTextarea);
Vue.component('v-text-overflow', VTextOverflow);
Vue.component('v-upload', VUpload);
export function registerComponents(app: App): void {
app.component('v-avatar', VAvatar);
app.component('v-badge', VBadge);
app.component('v-breadcrumb', VBreadcrumb);
app.component('v-button', VButton);
app.component('v-card-actions', VCardActions);
app.component('v-card-subtitle', VCardSubtitle);
app.component('v-card-text', VCardText);
app.component('v-card-title', VCardTitle);
app.component('v-card', VCard);
app.component('v-checkbox', VCheckbox);
app.component('v-chip', VChip);
app.component('v-detail', VDetail);
app.component('v-dialog', VDialog);
app.component('v-divider', VDivider);
app.component('v-error', VError);
app.component('v-fancy-select', VFancySelect);
app.component('v-field-template', VFieldTemplate);
app.component('v-field-select', VFieldSelect);
app.component('v-form', VForm);
app.component('v-hover', VHover);
app.component('v-icon', VIcon);
app.component('v-info', VInfo);
app.component('v-input', VInput);
app.component('v-item-group', VItemGroup);
app.component('v-item', VItem);
app.component('v-list-group', VListGroup);
app.component('v-list-item-content', VListItemContent);
app.component('v-list-item-hint', VListItemHint);
app.component('v-list-item-icon', VListItemIcon);
app.component('v-list-item', VListItem);
app.component('v-list', VList);
app.component('v-menu', VMenu);
app.component('v-drawer', VDrawer);
app.component('v-notice', VNotice);
app.component('v-overlay', VOverlay);
app.component('v-pagination', VPagination);
app.component('v-progress-circular', VProgressCircular);
app.component('v-progress-linear', VProgressLinear);
app.component('v-radio', VRadio);
app.component('v-select', VSelect);
app.component('v-sheet', VSheet);
app.component('v-skeleton-loader', VSkeletonLoader);
app.component('v-slider', VSlider);
app.component('v-switch', VSwitch);
app.component('v-tab-item', VTabItem);
app.component('v-tab', VTab);
app.component('v-table', VTable);
app.component('v-tabs-items', VTabsItems);
app.component('v-tabs', VTabs);
app.component('v-textarea', VTextarea);
app.component('v-text-overflow', VTextOverflow);
app.component('v-upload', VUpload);
Vue.component('transition-bounce', TransitionBounce);
Vue.component('transition-dialog', TransitionDialog);
Vue.component('transition-expand', TransitionExpand);
app.component('transition-bounce', TransitionBounce);
app.component('transition-dialog', TransitionDialog);
app.component('transition-expand', TransitionExpand);
Vue.component('render-display', RenderDisplay);
Vue.component('render-template', RenderTemplate);
Vue.component('filter-sidebar-detail', FilterSidebarDetail);
Vue.component('sidebar-detail', SidebarDetail);
Vue.component('user-popover', UserPopover);
Vue.component('value-null', ValueNull);
app.component('render-display', RenderDisplay);
app.component('render-template', RenderTemplate);
app.component('filter-sidebar-detail', FilterSidebarDetail);
app.component('export-sidebar-detail', ExportSidebarDetail);
app.component('sidebar-detail', SidebarDetail);
app.component('user-popover', UserPopover);
app.component('value-null', ValueNull);
}

View File

@@ -1,11 +1,12 @@
<template>
<transition-group name="bounce" tag="div">
<transition-group name="bounce" tag="div" v-bind="$attrs">
<slot />
</transition-group>
</template>
<style lang="scss">
/** @NOTE this is not scoped on purpose. The children are outsisde of the tree (portal) */
/** @NOTE this is not scoped on purpose. The children are outsisde of the tree (teleport) */
.bounce-enter-active,
.bounce-leave-active {
transition: opacity var(--fast) var(--transition);
@@ -15,7 +16,7 @@
}
}
.bounce-enter,
.bounce-enter-from,
.bounce-leave-to {
opacity: 0;

View File

@@ -1,11 +1,12 @@
<template>
<transition-group name="dialog">
<transition-group name="dialog" tag="span" v-bind="$attrs">
<slot />
</transition-group>
</template>
<style lang="scss">
/** @NOTE this is not scoped on purpose. The children are outside of the tree (portal) */
/** @NOTE this is not scoped on purpose. The children are outside of the tree (teleport) */
.dialog-enter-active,
.dialog-leave-active {
transition: opacity var(--slow) var(--transition);
@@ -21,7 +22,7 @@
}
}
.dialog-enter,
.dialog-enter-from,
.dialog-leave-to {
opacity: 0;

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import ExpandMethods from './transition-expand-methods';
export default defineComponent({

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
@@ -34,7 +34,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-avatar {
position: relative;
display: flex;
@@ -48,35 +48,33 @@ body {
text-overflow: ellipsis;
background-color: var(--v-avatar-color);
border-radius: var(--border-radius);
}
&.tile {
border-radius: 0;
}
.tile {
border-radius: 0;
}
&.x-small {
--v-avatar-size: 24px;
.x-small {
--v-avatar-size: 24px;
border-radius: 2px;
}
border-radius: 2px;
}
&.small {
--v-avatar-size: 36px;
}
.small {
--v-avatar-size: 36px;
}
&.large {
--v-avatar-size: 64px;
}
.large {
--v-avatar-size: 64px;
}
&.x-large {
--v-avatar-size: 80px;
}
.x-large {
--v-avatar-size: 80px;
}
::v-deep {
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
:slotted(img) {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {

View File

@@ -15,7 +15,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
interface Breadcrumb {
to: string;
@@ -46,8 +46,6 @@ body {
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-breadcrumb {
display: flex;
align-items: center;
@@ -93,7 +91,7 @@ body {
}
}
@include breakpoint(small) {
@media (min-width: 600px) {
font-size: inherit;
}
}

View File

@@ -1,4 +0,0 @@
import VButtonGroup from './v-button-group.vue';
export { VButtonGroup };
export default VButtonGroup;

View File

@@ -1,107 +0,0 @@
<template>
<div class="v-button-group" :class="{ rounded, tile }">
<v-item-group
:value="value"
:mandatory="mandatory"
:max="max"
:multiple="multiple"
scope="button-group"
@input="update"
>
<slot />
</v-item-group>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
export default defineComponent({
props: {
mandatory: {
type: Boolean,
default: false,
},
max: {
type: Number,
default: -1,
},
multiple: {
type: Boolean,
default: false,
},
value: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
rounded: {
type: Boolean,
default: false,
},
tile: {
type: Boolean,
default: false,
},
},
setup(props, { emit }) {
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
}
return { update };
},
});
</script>
<style>
body {
--v-button-group-background-color-active: var(--primary-alt);
}
</style>
<style lang="scss" scoped>
.v-button-group {
.v-item-group {
::v-deep .v-button {
--border-radius: 0px;
&:active {
transform: unset;
}
&.active {
--v-button-background-color: var(--v-button-group-background-color-active);
--v-button-background-color-hover: var(--v-button-group-background-color-active);
}
&:first-child {
--border-radius: var(--border-radius) 0px 0px var(--border-radius);
}
&:last-child {
--border-radius: 0px var(--border-radius) var(--border-radius) 0px;
}
}
}
&.tile .v-item-group ::v-deep .v-button {
&:first-child .button {
--border-radius: 0px;
}
&:last-child .button {
--border-radius: 0px;
}
}
&.rounded:not(.tile) .v-item-group ::v-deep .v-button {
&:first-child .button {
--border-radius: var(--v-button-height) 0px 0px var(--v-button-height);
}
&:last-child .button {
--border-radius: 0px var(--v-button-height) var(--v-button-height) 0px;
}
}
}
</style>

View File

@@ -4,19 +4,17 @@
<component
v-focus="autofocus"
:is="component"
:active-class="to ? 'activated' : null"
:exact="exact"
:download="download"
class="button"
:class="[
sizeClass,
`align-${align}`,
{
active: isActiveRoute,
rounded,
icon,
outlined,
loading,
active,
dashed,
tile,
'full-width': fullWidth,
@@ -44,13 +42,15 @@
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import { Location } from 'vue-router';
import { defineComponent, computed, PropType } from 'vue';
import { RouteLocation, useRoute, useLink } from 'vue-router';
import useSizeClass, { sizeProps } from '@/composables/size-class';
import { useGroupable } from '@/composables/groupable';
import { notEmpty } from '@/utils/is-empty';
import { isEqual } from 'lodash';
export default defineComponent({
emits: ['click'],
props: {
autofocus: {
type: Boolean,
@@ -85,17 +85,25 @@ export default defineComponent({
default: false,
},
to: {
type: [String, Object] as PropType<string | Location>,
type: [String, Object] as PropType<string | RouteLocation>,
default: null,
},
href: {
type: String,
default: null,
},
active: {
type: Boolean,
default: undefined,
},
exact: {
type: Boolean,
default: false,
},
query: {
type: Boolean,
default: false,
},
secondary: {
type: Boolean,
default: false,
@@ -124,6 +132,9 @@ export default defineComponent({
...sizeProps,
},
setup(props, { emit }) {
const route = useRoute();
const { route: linkRoute, isActive, isExactActive } = useLink(props);
const sizeClass = useSizeClass(props);
const component = computed<'a' | 'router-link' | 'button'>(() => {
@@ -135,10 +146,26 @@ export default defineComponent({
const { active, toggle } = useGroupable({
value: props.value,
group: 'button-group',
group: 'item-group',
});
return { sizeClass, onClick, component, active, toggle };
const isActiveRoute = computed(() => {
if (props.active !== undefined) return props.active;
if (props.to) {
const isQueryActive = !props.query || isEqual(route.query, linkRoute.value.query);
if (!props.exact) {
return (isActive.value && isQueryActive) || active.value;
} else {
return (isExactActive.value && isQueryActive) || active.value;
}
}
return false;
});
return { sizeClass, onClick, component, isActiveRoute, toggle };
function onClick(event: MouseEvent) {
if (props.loading === true) return;
@@ -150,204 +177,201 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-button-width: auto;
--v-button-height: 44px;
--v-button-color: var(--foreground-inverted);
--v-button-color-hover: var(--foreground-inverted);
--v-button-color-activated: var(--foreground-inverted);
--v-button-color-active: var(--foreground-inverted);
--v-button-color-disabled: var(--foreground-subdued);
--v-button-background-color: var(--primary);
--v-button-background-color-hover: var(--primary-125);
--v-button-background-color-activated: var(--primary);
--v-button-background-color-active: var(--primary);
--v-button-background-color-disabled: var(--background-normal);
--v-button-font-size: 16px;
--v-button-font-weight: 600;
--v-button-line-height: 22px;
--v-button-min-width: 140px;
}
</style>
<style lang="scss" scoped>
.v-button {
display: inline-flex;
align-items: center;
}
&.secondary {
--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(--border-subdued); // I'm so sorry! 🥺
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-background-color-activated: var(--background-normal-alt);
}
.secondary {
--v-button-color: var(--foreground-normal);
--v-button-color-hover: var(--foreground-normal);
--v-button-color-active: var(--foreground-normal);
--v-button-background-color: var(--border-subdued);
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-background-color-active: var(--background-normal-alt);
}
&.full-width {
display: flex;
min-width: 100%;
}
.v-button.full-width {
display: flex;
min-width: 100%;
}
.button {
position: relative;
display: flex;
align-items: center;
width: var(--v-button-width);
min-width: var(--v-button-min-width);
height: var(--v-button-height);
padding: 0 19px;
color: var(--v-button-color);
font-weight: var(--v-button-font-weight);
font-size: var(--v-button-font-size);
line-height: var(--v-button-line-height);
text-decoration: none;
background-color: var(--v-button-background-color);
border: var(--border-width) solid var(--v-button-background-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--fast) var(--transition);
transition-property: background-color border;
.button {
position: relative;
display: flex;
align-items: center;
width: var(--v-button-width);
min-width: var(--v-button-min-width);
height: var(--v-button-height);
padding: 0 19px;
color: var(--v-button-color);
font-weight: var(--v-button-font-weight);
font-size: var(--v-button-font-size);
line-height: var(--v-button-line-height);
text-decoration: none;
background-color: var(--v-button-background-color);
border: var(--border-width) solid var(--v-button-background-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: var(--fast) var(--transition);
transition-property: background-color border;
}
&:hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border-color: var(--v-button-background-color-hover);
}
.button:hover {
color: var(--v-button-color-hover);
background-color: var(--v-button-background-color-hover);
border-color: var(--v-button-background-color-hover);
}
&.align-left {
justify-content: flex-start;
}
.align-left {
justify-content: flex-start;
}
&.align-center {
justify-content: center;
}
.align-center {
justify-content: center;
}
&.align-right {
justify-content: flex-end;
}
.align-right {
justify-content: flex-end;
}
&:focus {
outline: 0;
}
.button:focus {
outline: 0;
}
&:disabled {
color: var(--v-button-color-disabled);
background-color: var(--v-button-background-color-disabled);
border: var(--border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
}
.button:disabled {
color: var(--v-button-color-disabled);
background-color: var(--v-button-background-color-disabled);
border: var(--border-width) solid var(--v-button-background-color-disabled);
cursor: not-allowed;
}
&.rounded {
border-radius: calc(var(--v-button-height) / 2);
}
.rounded {
border-radius: calc(var(--v-button-height) / 2);
}
&.outlined {
--v-button-color: var(--v-button-background-color);
.outlined {
--v-button-color: var(--v-button-background-color);
background-color: transparent;
background-color: transparent;
}
&:not(.activated):hover {
color: var(--v-button-background-color-hover);
background-color: transparent;
border-color: var(--v-button-background-color-hover);
}
.outlined:not(.active):hover {
color: var(--v-button-background-color-hover);
background-color: transparent;
border-color: var(--v-button-background-color-hover);
}
&.secondary {
--v-button-color: var(--foreground-subdued);
}
}
.outlined.secondary {
--v-button-color: var(--foreground-subdued);
}
&.dashed {
border-style: dashed;
}
.dashed {
border-style: dashed;
}
&.x-small {
--v-button-height: 28px;
--v-button-font-size: 12px;
--v-button-font-weight: 600;
--v-button-min-width: 60px;
--border-radius: 4px;
.x-small {
--v-button-height: 28px;
--v-button-font-size: 12px;
--v-button-font-weight: 600;
--v-button-min-width: 60px;
--border-radius: 4px;
padding: 0 12px;
}
padding: 0 12px;
}
&.small {
--v-button-height: 36px;
--v-button-font-size: 14px;
--v-button-min-width: 120px;
.small {
--v-button-height: 36px;
--v-button-font-size: 14px;
--v-button-min-width: 120px;
padding: 0 12px;
}
padding: 0 12px;
}
&.large {
--v-button-height: 52px;
--v-button-min-width: 154px;
.large {
--v-button-height: 52px;
--v-button-min-width: 154px;
padding: 0 12px;
}
padding: 0 12px;
}
&.x-large {
--v-button-height: 64px;
--v-button-font-size: 18px;
--v-button-min-width: 180px;
.x-large {
--v-button-height: 64px;
--v-button-font-size: 18px;
--v-button-min-width: 180px;
padding: 0 12px;
}
padding: 0 12px;
}
&.icon {
width: var(--v-button-height);
min-width: 0;
padding: 0;
}
.icon {
width: var(--v-button-height);
min-width: 0;
padding: 0;
}
&.full-width {
min-width: 100%;
}
.button.full-width {
min-width: 100%;
}
.content,
.spinner {
max-width: 100%;
margin: 0 -1px; // Fixes slightly cropped icons
padding: 0 1px; // Fixes slightly cropped icons
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.content,
.spinner {
max-width: 100%;
margin: 0 -1px;
padding: 0 1px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.content {
position: relative;
display: flex;
align-items: center;
line-height: normal;
.content {
position: relative;
display: flex;
align-items: center;
line-height: normal;
}
&.invisible {
opacity: 0;
}
}
.content.invisible {
opacity: 0;
}
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.v-progress-circular {
--v-progress-circular-color: var(--v-button-color);
--v-progress-circular-background-color: transparent;
}
}
.spinner .v-progress-circular {
--v-progress-circular-color: var(--v-button-color);
--v-progress-circular-background-color: transparent;
}
&.activated,
&.active {
--v-button-color: var(--v-button-color-activated) !important;
--v-button-color-hover: var(--v-button-color-activated) !important;
--v-button-background-color: var(--v-button-background-color-activated) !important;
--v-button-background-color-hover: var(--v-button-background-color-activated) !important;
}
.active {
--v-button-color: var(--v-button-color-active) !important;
--v-button-color-hover: var(--v-button-color-active) !important;
--v-button-background-color: var(--v-button-background-color-active) !important;
--v-button-background-color-hover: var(--v-button-background-color-active) !important;
}
&.tile {
border-radius: 0;
}
}
.tile {
border-radius: 0;
}
</style>

View File

@@ -1,15 +1,15 @@
<template functional>
<template>
<div class="v-card-actions"><slot /></div>
</template>
<style lang="scss" scoped>
<style scoped>
.v-card-actions {
display: flex;
justify-content: flex-end;
padding: var(--v-card-padding);
}
& ::v-deep > .v-button + .v-button {
margin-left: 12px;
}
.v-card-actions > :slotted(.v-button + .v-button) {
margin-left: 12px;
}
</style>

View File

@@ -1,4 +1,4 @@
<template function>
<template>
<div class="v-card-subtitle"><slot /></div>
</template>

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<div class="v-card-text"><slot /></div>
</template>

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<div class="v-card-title type-label"><slot /></div>
</template>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {

View File

@@ -9,31 +9,28 @@
:disabled="disabled"
:class="{ checked: isChecked, indeterminate, block }"
>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<div class="prepend" v-if="$slots.prepend"><slot name="prepend" /></div>
<v-icon class="checkbox" :name="icon" @click.stop="toggleInput" :disabled="disabled" />
<span class="label type-text">
<slot v-if="customValue === false">{{ label }}</slot>
<input @click.stop class="custom-input" v-else v-model="_value" />
<input @click.stop class="custom-input" v-else v-model="internalValue" />
</span>
<div class="append" v-if="$scopedSlots.append"><slot name="append" /></div>
<div class="append" v-if="$slots.append"><slot name="append" /></div>
</component>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSync from '@/composables/use-sync';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:indeterminate', 'update:modelValue', 'update:value'],
props: {
value: {
type: String,
default: null,
},
inputValue: {
modelValue: {
type: [Boolean, Array],
default: false,
},
@@ -71,14 +68,14 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const _value = useSync(props, 'value', emit);
const internalValue = useSync(props, 'value', emit);
const isChecked = computed<boolean>(() => {
if (props.inputValue instanceof Array) {
return props.inputValue.includes(props.value);
if (props.modelValue instanceof Array) {
return props.modelValue.includes(props.value);
}
return props.inputValue === true;
return props.modelValue === true;
});
const icon = computed<string>(() => {
@@ -86,15 +83,15 @@ export default defineComponent({
return isChecked.value ? props.iconOn : props.iconOff;
});
return { isChecked, toggleInput, icon, _value };
return { isChecked, toggleInput, icon, internalValue };
function toggleInput(): void {
if (props.indeterminate === true) {
emit('update:indeterminate', false);
}
if (props.inputValue instanceof Array) {
const newValue = [...props.inputValue];
if (props.modelValue instanceof Array) {
const newValue = [...props.modelValue];
if (isChecked.value === false) {
newValue.push(props.value);
@@ -102,9 +99,9 @@ export default defineComponent({
newValue.splice(newValue.indexOf(props.value), 1);
}
emit('change', newValue);
emit('update:modelValue', newValue);
} else {
emit('change', !isChecked.value);
emit('update:modelValue', !isChecked.value);
}
}
},
@@ -196,6 +193,7 @@ body {
.checkbox {
--v-icon-color: var(--primary);
}
&.block {
background-color: var(--background-subdued);
border-color: var(--border-normal-alt);

View File

@@ -1,5 +1,10 @@
<template>
<span v-if="_active" class="v-chip" :class="[sizeClass, { outlined, label, disabled, close }]" @click="onClick">
<span
v-if="internalActive"
class="v-chip"
:class="[sizeClass, { outlined, label, disabled, close }]"
@click="onClick"
>
<span class="chip-content">
<slot />
<span v-if="close" class="close-outline" :class="{ disabled }" @click.stop="onCloseClick">
@@ -10,10 +15,11 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { defineComponent, ref, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
emits: ['update:active', 'click', 'close'],
props: {
active: {
type: Boolean,
@@ -42,22 +48,22 @@ export default defineComponent({
...sizeProps,
},
setup(props, { emit }) {
const _localActive = ref(true);
const internalLocalActive = ref(true);
const _active = computed<boolean>({
const internalActive = computed<boolean>({
get: () => {
if (props.active !== null) return props.active;
return _localActive.value;
return internalLocalActive.value;
},
set: (active: boolean) => {
emit('update:active', active);
_localActive.value = active;
internalLocalActive.value = active;
},
});
const sizeClass = useSizeClass(props);
return { sizeClass, _active, onClick, onCloseClick };
return { sizeClass, internalActive, onClick, onCloseClick };
function onClick(event: MouseEvent) {
if (props.disabled) return;
@@ -66,7 +72,7 @@ export default defineComponent({
function onCloseClick(event: MouseEvent) {
if (props.disabled) return;
_active.value = !_active.value;
internalActive.value = !internalActive.value;
emit('close', event);
}
},
@@ -113,6 +119,7 @@ body {
color: var(--v-chip-color);
background-color: var(--v-chip-background-color);
border-color: var(--v-chip-background-color);
&:hover {
color: var(--v-chip-color);
background-color: var(--v-chip-background-color);

View File

@@ -1,11 +1,11 @@
<template>
<div class="v-detail" :class="{ disabled }">
<v-divider @click.native="_active = !_active">
<v-icon v-if="!disabled" :name="_active ? 'unfold_less' : 'unfold_more'" small />
<v-divider @click="internalActive = !internalActive">
<v-icon v-if="!disabled" :name="internalActive ? 'unfold_less' : 'unfold_more'" small />
<slot name="title">{{ label }}</slot>
</v-divider>
<transition-expand>
<div v-if="_active">
<div v-if="internalActive">
<slot />
</div>
</transition-expand>
@@ -13,23 +13,19 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import { defineComponent, computed, ref } from 'vue';
import { i18n } from '@/lang';
export default defineComponent({
model: {
prop: 'active',
event: 'toggle',
},
emits: ['update:modelValue'],
props: {
active: {
modelValue: {
type: Boolean,
default: undefined,
},
label: {
type: String,
default: i18n.t('toggle'),
default: i18n.global.t('toggle'),
},
startOpen: {
type: Boolean,
@@ -43,20 +39,20 @@ export default defineComponent({
setup(props, { emit }) {
const localActive = ref(props.startOpen);
const _active = computed({
const internalActive = computed({
get() {
if (props.active !== undefined) {
return props.active;
if (props.modelValue !== undefined) {
return props.modelValue;
}
return localActive.value;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
return { _active };
return { internalActive };
},
});
</script>
@@ -69,6 +65,7 @@ export default defineComponent({
.v-detail:not(.disabled) .v-divider {
--v-divider-label-color: var(--foreground-subdued);
&:hover {
--v-divider-label-color: var(--foreground-normal-alt);

View File

@@ -1,28 +1,28 @@
<template>
<div class="v-dialog">
<slot name="activator" v-bind="{ on: () => (_active = true) }" />
<slot name="activator" v-bind="{ on: () => (internalActive = true) }" />
<portal to="dialog-outlet">
<div v-if="_active" class="container" :class="[className, placement]" :key="id">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
</portal>
<teleport to="#dialog-outlet">
<transition-dialog @after-leave="leave">
<div v-if="internalActive" class="container" :class="[className, placement]">
<v-overlay active absolute @click="emitToggle" />
<slot />
</div>
</transition-dialog>
</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { defineComponent, ref, computed } from 'vue';
import { nanoid } from 'nanoid';
import useShortcut from '@/composables/use-shortcut';
import { useDialogRouteLeave } from '@/composables/use-dialog-route';
export default defineComponent({
model: {
prop: 'active',
event: 'toggle',
},
emits: ['esc', 'update:modelValue'],
props: {
active: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -38,7 +38,7 @@ export default defineComponent({
},
setup(props, { emit }) {
useShortcut('escape', (event, cancelNext) => {
if (_active.value) {
if (internalActive.value) {
emit('esc');
cancelNext();
}
@@ -49,21 +49,23 @@ export default defineComponent({
const className = ref<string | null>(null);
const id = computed(() => nanoid());
const _active = computed({
const internalActive = computed({
get() {
return props.active !== undefined ? props.active : localActive.value;
return props.modelValue !== undefined ? props.modelValue : localActive.value;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
return { emitToggle, className, nudge, id, _active };
const leave = useDialogRouteLeave();
return { emitToggle, className, nudge, leave, id, internalActive };
function emitToggle() {
if (props.persistent === false) {
emit('toggle', !props.active);
emit('update:modelValue', !props.modelValue);
} else {
nudge();
}
@@ -81,8 +83,6 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-dialog {
--v-dialog-z-index: 100;
@@ -97,86 +97,90 @@ export default defineComponent({
display: flex;
width: 100%;
height: 100%;
}
::v-deep > * {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
.container > :slotted(*) {
z-index: 2;
box-shadow: 0px 4px 12px rgba(38, 50, 56, 0.1);
}
.container.center {
align-items: center;
justify-content: center;
}
.container.center.nudge > :slotted(*:not(:first-child)) {
animation: nudge 200ms;
}
.container.right {
align-items: center;
justify-content: flex-end;
}
.container.right.nudge > :slotted(*:not(:first-child)) {
transform-origin: right;
animation: shake 200ms;
}
.container :slotted(.v-card) {
--v-card-min-width: calc(100vw - 40px);
--v-card-padding: 28px;
--v-card-background-color: var(--background-page);
}
.container :slotted(.v-card) .v-card-title {
padding-bottom: 8px;
}
.container :slotted(.v-card) .v-card-actions {
flex-direction: column-reverse;
flex-wrap: wrap;
}
.container :slotted(.v-card) .v-card-actions .v-button {
width: 100%;
}
.container :slotted(.v-card) .v-card-actions .v-button .button {
width: 100%;
}
.container :slotted(.v-card) .v-card-actions > .v-button + .v-button {
margin-bottom: 20px;
margin-left: 0;
}
.container :slotted(.v-sheet) {
--v-sheet-padding: 24px;
--v-sheet-max-width: 560px;
}
.container .v-overlay {
--v-overlay-z-index: 1;
}
@media (min-width: 600px) {
.container :slotted(.v-card) {
--v-card-min-width: 540px;
}
&.center {
align-items: center;
justify-content: center;
&.nudge > ::v-deep *:not(:first-child) {
animation: nudge 200ms;
}
.container :slotted(.v-card) .v-card-actions {
flex-direction: inherit;
flex-wrap: nowrap;
}
&.right {
align-items: center;
justify-content: flex-end;
&.nudge > ::v-deep *:not(:first-child) {
transform-origin: right;
animation: shake 200ms;
}
.container :slotted(.v-card) .v-card-actions .v-button {
width: auto;
}
::v-deep .v-card {
--v-card-min-width: calc(100vw - 40px);
--v-card-padding: 28px;
--v-card-background-color: var(--background-page);
.v-card-title {
padding-bottom: 8px;
}
.v-card-actions {
flex-direction: column-reverse;
flex-wrap: wrap;
.v-button {
width: 100%;
.button {
width: 100%;
}
}
& > .v-button + .v-button {
margin-bottom: 20px;
margin-left: 0;
}
}
@include breakpoint(small) {
--v-card-min-width: 540px;
.v-card-actions {
flex-direction: inherit;
flex-wrap: nowrap;
.v-button {
width: auto;
.button {
width: auto;
}
}
& > .v-button + .v-button {
margin-bottom: 0;
margin-left: 12px;
}
}
}
.container :slotted(.v-card) .v-card-actions .v-button .button {
width: auto;
}
::v-deep .v-sheet {
--v-sheet-padding: 24px;
--v-sheet-max-width: 560px;
}
.v-overlay {
--v-overlay-z-index: 1;
.container :slotted(.v-card) .v-card-actions > .v-button + .v-button {
margin-bottom: 0;
margin-left: 12px;
}
}

View File

@@ -9,7 +9,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -60,7 +60,7 @@ body {
margin-right: 16px;
color: var(--v-divider-label-color);
.v-icon {
:slotted(.v-icon) {
margin-right: 4px;
transform: translateY(-1px);
}

View File

@@ -1,18 +1,18 @@
<template>
<v-dialog v-model="_active" @esc="$emit('cancel')" :persistent="persistent" placement="right">
<v-dialog v-model="internalActive" @esc="$emit('cancel')" :persistent="persistent" placement="right">
<template #activator="{ on }">
<slot name="activator" v-bind="{ on }" />
</template>
<article class="v-drawer">
<v-button
v-if="showCancel"
v-if="cancelable"
class="cancel"
@click="$emit('cancel')"
icon
rounded
secondary
v-tooltip.bottom="$t('cancel')"
v-tooltip.bottom="t('cancel')"
>
<v-icon name="close" />
</v-button>
@@ -57,18 +57,16 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, computed, provide } from 'vue';
import HeaderBar from '@/views/private/components/header-bar/header-bar.vue';
import i18n from '@/lang';
import { i18n } from '@/lang';
export default defineComponent({
emits: ['cancel', 'update:modelValue'],
components: {
HeaderBar,
},
model: {
prop: 'active',
event: 'toggle',
},
props: {
title: {
type: String,
@@ -78,7 +76,7 @@ export default defineComponent({
type: String,
default: null,
},
active: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -92,31 +90,33 @@ export default defineComponent({
},
sidebarLabel: {
type: String,
default: i18n.t('sidebar'),
default: i18n.global.t('sidebar'),
},
cancelable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
setup(props, { emit }) {
const { t } = useI18n();
const localActive = ref(false);
const mainEl = ref<Element>();
provide('main-element', mainEl);
const _active = computed({
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
return props.modelValue === undefined ? localActive.value : props.modelValue;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('toggle', newActive);
emit('update:modelValue', newActive);
},
});
const showCancel = computed(() => {
return 'cancel' in listeners;
});
return { _active, mainEl, showCancel };
return { t, internalActive, mainEl };
},
});
</script>
@@ -128,8 +128,6 @@ body {
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-drawer {
position: relative;
display: flex;
@@ -151,7 +149,7 @@ body {
.header-icon {
--v-button-background-color: var(--background-normal);
--v-button-background-color-activated: var(--background-normal);
--v-button-background-color-active: var(--background-normal);
--v-button-background-color-hover: var(--background-normal-alt);
--v-button-color-disabled: var(--foreground-normal);
}
@@ -177,7 +175,7 @@ body {
display: none;
@include breakpoint(medium) {
@media (min-width: 960px) {
position: relative;
z-index: 2;
display: block;
@@ -193,7 +191,7 @@ body {
.v-overlay {
--v-overlay-z-index: 1;
@include breakpoint(medium) {
@media (min-width: 960px) {
--v-overlay-z-index: none;
display: none;
@@ -207,14 +205,14 @@ body {
flex-grow: 1;
overflow: auto;
@include breakpoint(small) {
@media (min-width: 600px) {
--content-padding: 32px;
--content-padding-bottom: 132px;
}
}
}
@include breakpoint(medium) {
@media (min-width: 960px) {
width: calc(100% - 64px);
}
}
@@ -227,7 +225,7 @@ body {
border-radius: var(--border-radius);
}
@include breakpoint(medium) {
@media (min-width: 960px) {
display: none;
}
}

View File

@@ -2,18 +2,20 @@
<div class="v-error selectable">
<output>[{{ code }}] {{ message }}</output>
<v-icon
v-tooltip="$t('copy_details')"
v-tooltip="t('copy_details')"
v-if="showCopy"
small
class="copy-error"
:name="copied ? 'check' : 'content_copy'"
clickable
@click="copyError"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType, ref } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, computed, PropType, ref } from 'vue';
import { isPlainObject } from 'lodash';
export default defineComponent({
@@ -24,6 +26,8 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const code = computed(() => {
return props.error?.response?.data?.errors?.[0]?.extensions?.code || props.error?.extensions?.code || 'UNKNOWN';
});
@@ -42,7 +46,7 @@ export default defineComponent({
const showCopy = computed(() => !!navigator.clipboard?.writeText);
return { code, copyError, showCopy, copied, message };
return { t, code, copyError, showCopy, copied, message };
async function copyError() {
const error = props.error?.response?.data || props.error;

View File

@@ -1,10 +1,8 @@
import { TranslateResult } from 'vue-i18n';
export type FancySelectItem = {
icon: string;
value: string | number;
text: string | TranslateResult;
description?: string | TranslateResult;
text: string;
description?: string;
divider?: boolean;
iconRight?: string;
};

View File

@@ -1,13 +1,12 @@
<template>
<div class="v-fancy-select">
<transition-group tag="div" name="option">
<template v-for="(item, index) in visibleItems">
<v-divider :key="index" v-if="item.divider === true" />
<template v-for="(item, index) in visibleItems" :key="index">
<v-divider v-if="item.divider === true" />
<div
v-else
:key="item.value"
class="v-fancy-select-option"
:class="{ active: item.value === value, disabled }"
:class="{ active: item.value === modelValue, disabled }"
:style="{
'--index': index,
}"
@@ -22,7 +21,7 @@
<div class="description">{{ item.description }}</div>
</div>
<v-icon v-if="value === item.value && disabled === false" name="cancel" @click.stop="toggle(item)" />
<v-icon v-if="modelValue === item.value && disabled === false" name="cancel" @click.stop="toggle(item)" />
<v-icon class="icon-right" v-else-if="item.iconRight" :name="item.iconRight" />
</div>
</template>
@@ -31,16 +30,17 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { defineComponent, PropType, computed } from 'vue';
import { FancySelectItem } from './types';
export default defineComponent({
emits: ['update:modelValue'],
props: {
items: {
type: Array as PropType<FancySelectItem[]>,
required: true,
},
value: {
modelValue: {
type: [String, Number],
default: null,
},
@@ -51,10 +51,10 @@ export default defineComponent({
},
setup(props, { emit }) {
const visibleItems = computed(() => {
if (props.value === null) return props.items;
if (props.modelValue === null) return props.items;
return props.items.filter((item) => {
return item.value === props.value;
return item.value === props.modelValue;
});
});
@@ -62,8 +62,8 @@ export default defineComponent({
function toggle(item: FancySelectItem) {
if (props.disabled === true) return;
if (props.value === item.value) emit('input', null);
else emit('input', item.value);
if (props.modelValue === item.value) emit('update:modelValue', null);
else emit('update:modelValue', item.value);
}
},
});
@@ -147,7 +147,7 @@ export default defineComponent({
transition: all 500ms var(--transition);
}
.option-enter,
.option-enter-from,
.option-leave-to {
opacity: 0;
}

View File

@@ -1,31 +1,28 @@
<template>
<v-notice v-if="!availableFields || availableFields.length === 0">
{{ $t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
{{ t('no_fields_in_collection', { collection: (collectionInfo && collectionInfo.name) || collection }) }}
</v-notice>
<draggable
v-else
:force-fallback="true"
v-model="selectedFields"
item-key="field"
draggable=".draggable"
:set-data="hideDragImage"
class="v-field-select"
>
<v-chip
v-for="(field, index) in selectedFields"
:key="index"
class="field draggable"
v-tooltip="field.field"
@click="removeField(field.field)"
>
{{ field.name }}
</v-chip>
<template #item="{ element }">
<v-chip class="field draggable" v-tooltip="element.field" @click="removeField(element.field)">
{{ element.name }}
</v-chip>
</template>
<template #footer>
<v-menu show-arrow v-model="menuActive" class="add" placement="bottom">
<template #activator="{ toggle }">
<v-button @click="toggle" small>
{{ $t('add_field') }}
{{ t('add_field') }}
<v-icon small name="add" />
</v-button>
</template>
@@ -45,7 +42,8 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, toRefs, ref, PropType, computed } from 'vue';
import FieldListItem from '../v-field-template/field-list-item.vue';
import { Field, Collection, Relation } from '@/types';
import Draggable from 'vuedraggable';
@@ -55,13 +53,14 @@ import { FieldTree } from '../v-field-template/types';
import hideDragImage from '@/utils/hide-drag-image';
export default defineComponent({
emits: ['update:modelValue'],
components: { FieldListItem, Draggable },
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Array as PropType<string[]>,
default: null,
},
@@ -79,30 +78,32 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const menuActive = ref(false);
const { collection, inject } = toRefs(props);
const { info } = useCollection(collection);
const { tree } = useFieldTree(collection, false, inject);
const _value = computed({
const internalValue = computed({
get() {
return props.value || [];
return props.modelValue || [];
},
set(newVal: string[]) {
emit('input', newVal);
emit('update:modelValue', newVal);
},
});
const selectedFields = computed({
get() {
return _value.value.map((field) => ({
return internalValue.value.map((field) => ({
field,
name: findTree(tree.value, field.split('.'))?.name as string,
}));
},
set(newVal: { field: string; name: string }[]) {
_value.value = newVal.map((field) => field.field);
internalValue.value = newVal.map((field) => field.field);
},
});
@@ -111,6 +112,7 @@ export default defineComponent({
});
return {
t,
menuActive,
addField,
removeField,
@@ -139,7 +141,7 @@ export default defineComponent({
name: field.name,
field: field.field,
key: field.key,
disabled: _value.value.includes(prefix + field.field),
disabled: internalValue.value.includes(prefix + field.field),
children: parseTree(field.children, prefix + field.field + '.'),
};
});
@@ -148,13 +150,13 @@ export default defineComponent({
}
function removeField(field: string) {
_value.value = _value.value.filter((f) => f !== field);
internalValue.value = internalValue.value.filter((f) => f !== field);
}
function addField(field: string) {
const newArray = _value.value;
const newArray = internalValue.value;
newArray.push(field);
_value.value = [...new Set(newArray)];
internalValue.value = [...new Set(newArray)];
}
},
});

View File

@@ -2,6 +2,7 @@
<v-list-item
v-if="field.children === undefined || depth === 0"
:disabled="field.disabled"
clickable
@click="$emit('add', `${parent ? parent + '.' : ''}${field.field}`)"
>
<v-list-item-content>{{ field.name || formatTitle(field.field) }}</v-list-item-content>
@@ -20,11 +21,12 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
import { FieldTree } from './types';
import formatTitle from '@directus/format-title';
export default defineComponent({
emits: ['add'],
name: 'field-list-item',
props: {
field: {

View File

@@ -1,8 +1,6 @@
import { TranslateResult } from 'vue-i18n';
export type FieldTree = {
field: string;
name: string | TranslateResult;
name: string;
key: string;
disabled?: boolean;
children?: FieldTree[];

View File

@@ -6,11 +6,11 @@
<span ref="contentEl" class="content" contenteditable @keydown="onKeyDown" @input="onInput" @click="onClick">
<span class="text" />
</span>
<span class="placeholder" v-if="placeholder && !value">{{ placeholder }}</span>
<span class="placeholder" v-if="placeholder && !modelValue">{{ placeholder }}</span>
</template>
<template #append>
<v-icon name="add_box" outline @click="toggle" :disabled="disabled" />
<v-icon name="add_box" outline clickable @click="toggle" :disabled="disabled" />
</template>
</v-input>
</template>
@@ -22,20 +22,21 @@
</template>
<script lang="ts">
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType } from '@vue/composition-api';
import { defineComponent, toRefs, ref, watch, onMounted, onUnmounted, PropType } from 'vue';
import FieldListItem from './field-list-item.vue';
import useFieldTree from '@/composables/use-field-tree';
import { FieldTree } from './types';
import { Field, Relation } from '@/types';
export default defineComponent({
emits: ['update:modelValue'],
components: { FieldListItem },
props: {
disabled: {
type: Boolean,
default: false,
},
value: {
modelValue: {
type: String,
default: null,
},
@@ -68,7 +69,7 @@ export default defineComponent({
const { collection, inject } = toRefs(props);
const { tree } = useFieldTree(collection, true, inject);
watch(() => props.value, setContent, { immediate: true });
watch(() => props.modelValue, setContent, { immediate: true });
onMounted(() => {
if (contentEl.value) {
@@ -89,7 +90,7 @@ export default defineComponent({
if (!contentEl.value) return;
const valueString = getInputValue();
emit('input', valueString);
emit('update:modelValue', valueString);
}
function onClick(event: MouseEvent) {
@@ -98,7 +99,7 @@ export default defineComponent({
if (target.tagName.toLowerCase() !== 'button') return;
const field = target.dataset.field;
emit('input', props.value.replace(`{{${field}}}`, ''));
emit('update:modelValue', props.modelValue.replace(`{{${field}}}`, ''));
const before = target.previousElementSibling;
const after = target.nextElementSibling;
@@ -256,15 +257,15 @@ export default defineComponent({
function setContent() {
if (!contentEl.value) return;
if (props.value === null || props.value === '') {
if (props.modelValue === null || props.modelValue === '') {
contentEl.value.innerHTML = '<span class="text"></span>';
return;
}
if (props.value !== getInputValue()) {
if (props.modelValue !== getInputValue()) {
const regex = /({{.*?}})/g;
const newInnerHTML = props.value
const newInnerHTML = props.modelValue
.split(regex)
.map((part) => {
if (part.startsWith('{{') === false) {
@@ -285,7 +286,7 @@ export default defineComponent({
});
</script>
<style lang="scss" scoped>
<style scoped>
.content {
display: block;
flex-grow: 1;
@@ -295,47 +296,45 @@ export default defineComponent({
font-size: 14px;
font-family: var(--family-monospace);
white-space: nowrap;
}
::v-deep {
> * {
display: inline-block;
white-space: nowrap;
}
:deep(br) {
display: none;
}
br {
display: none;
}
:deep(span) {
min-width: 1px;
min-height: 1em;
}
span {
min-width: 1px;
min-height: 1em;
}
:deep(button) {
margin: -1px 4px 0;
padding: 2px 4px 0;
color: var(--primary);
background-color: var(--primary-alt);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: background-color, color;
user-select: none;
}
button {
margin: -1px 4px 0; // top offset for monospace
padding: 2px 4px 0; // top offset for monospace
color: var(--primary);
background-color: var(--primary-alt);
border-radius: var(--border-radius);
transition: var(--fast) var(--transition);
transition-property: background-color, color;
user-select: none;
:deep(button:hover) {
color: var(--white);
background-color: var(--danger);
}
&:hover {
color: var(--white);
background-color: var(--danger);
}
}
}
.placeholder {
position: absolute;
top: 50%;
left: 14px;
color: var(--foreground-subdued);
transform: translateY(-50%);
user-select: none;
pointer-events: none;
}
.placeholder {
position: absolute;
top: 50%;
left: 14px;
color: var(--foreground-subdued);
transform: translateY(-50%);
user-select: none;
pointer-events: none;
}
.content > :deep(*) {
display: inline-block;
white-space: nowrap;
}
</style>

View File

@@ -18,30 +18,32 @@
:autofocus="disabled !== true && autofocus"
:disabled="disabled"
:loading="loading"
:value="value === undefined ? field.schema.default_value : value"
:value="modelValue === undefined ? field.schema.default_value : modelValue"
:width="(field.meta && field.meta.width) || 'full'"
:type="field.type"
:collection="field.collection"
:field="field.field"
:primary-key="primaryKey"
:length="field.schema && field.schema.max_length"
@input="$emit('input', $event)"
@input="$emit('update:modelValue', $event)"
/>
<v-notice v-else type="warning">
{{ $t('interface_not_found', { interface: field.meta && field.meta.interface }) }}
{{ t('interface_not_found', { interface: field.meta && field.meta.interface }) }}
</v-notice>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed } from 'vue';
import { Field } from '@/types';
import { getInterfaces } from '@/interfaces';
import { getDefaultInterfaceForType } from '@/utils/get-default-interface-for-type';
import { InterfaceConfig } from '@/interfaces/types';
export default defineComponent({
emits: ['update:modelValue'],
props: {
field: {
type: Object as PropType<Field>,
@@ -59,7 +61,7 @@ export default defineComponent({
type: [Number, String],
default: null,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: null,
},
@@ -77,13 +79,15 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const { interfaces } = getInterfaces();
const interfaceExists = computed(() => {
return !!interfaces.value.find((inter: InterfaceConfig) => inter.id === props.field?.meta?.interface || 'input');
});
return { interfaceExists, getDefaultInterfaceForType };
return { t, interfaceExists, getDefaultInterfaceForType };
},
});
</script>

View File

@@ -2,11 +2,11 @@
<div class="label type-label" :class="{ disabled, edited: edited && !batchMode && !hasError }">
<v-checkbox
v-if="batchMode"
:input-value="batchActive"
:model-value="batchActive"
:value="field.field"
@change="$emit('toggle-batch', field)"
@update:model-value="$emit('toggle-batch', field)"
/>
<span @click="toggle" v-tooltip="edited ? $t('edited') : null">
<span @click="toggle" v-tooltip="edited ? t('edited') : null">
{{ field.name }}
<v-icon class="required" sup name="star" v-if="field.schema && field.schema.is_nullable === false" />
<v-icon v-if="!disabled" class="ctx-arrow" :class="{ active }" name="arrow_drop_down" />
@@ -15,10 +15,12 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType } from 'vue';
import { Field } from '@/types/';
export default defineComponent({
emits: ['toggle-batch'],
props: {
batchMode: {
type: Boolean,
@@ -53,6 +55,10 @@ export default defineComponent({
default: false,
},
},
setup() {
const { t } = useI18n();
return { t };
},
});
</script>
@@ -111,6 +117,7 @@ export default defineComponent({
content: '';
pointer-events: none;
}
> span {
margin-left: -16px;
padding-left: 16px;

View File

@@ -1,43 +1,56 @@
<template>
<v-list>
<v-list-item v-if="defaultValue === null || !isRequired" :disabled="value === null" @click="$emit('input', null)">
<v-list-item
v-if="defaultValue === null || !isRequired"
:disabled="modelValue === null"
clickable
@click="$emit('update:modelValue', null)"
>
<v-list-item-icon><v-icon name="delete_outline" /></v-list-item-icon>
<v-list-item-content>{{ $t('clear_value') }}</v-list-item-content>
<v-list-item-content>{{ t('clear_value') }}</v-list-item-content>
</v-list-item>
<v-list-item v-if="defaultValue !== null" :disabled="value === defaultValue" @click="$emit('input', defaultValue)">
<v-list-item
v-if="defaultValue !== null"
:disabled="modelValue === defaultValue"
clickable
@click="$emit('update:modelValue', defaultValue)"
>
<v-list-item-icon>
<v-icon name="settings_backup_restore" />
</v-list-item-icon>
<v-list-item-content>{{ $t('reset_to_default') }}</v-list-item-content>
<v-list-item-content>{{ t('reset_to_default') }}</v-list-item-content>
</v-list-item>
<v-list-item
v-if="initialValue"
:disabled="initialValue === undefined || value === initialValue"
:disabled="initialValue === undefined || modelValue === initialValue"
clickable
@click="$emit('unset', field)"
>
<v-list-item-icon>
<v-icon name="undo" />
</v-list-item-icon>
<v-list-item-content>{{ $t('undo_changes') }}</v-list-item-content>
<v-list-item-content>{{ t('undo_changes') }}</v-list-item-content>
</v-list-item>
<v-list-item @click="$emit('edit-raw')">
<v-list-item clickable @click="$emit('edit-raw')">
<v-list-item-icon><v-icon name="code" /></v-list-item-icon>
<v-list-item-content>{{ $t('raw_value') }}</v-list-item-content>
<v-list-item-content>{{ t('raw_value') }}</v-list-item-content>
</v-list-item>
</v-list>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed } from 'vue';
import { Field } from '@/types';
export default defineComponent({
emits: ['update:modelValue', 'unset', 'edit-raw'],
props: {
field: {
type: Object as PropType<Field>,
required: true,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: null,
},
@@ -47,6 +60,8 @@ export default defineComponent({
},
},
setup(props) {
const { t } = useI18n();
const defaultValue = computed(() => {
const savedValue = props.field?.schema?.default_value;
return savedValue !== undefined ? savedValue : null;
@@ -56,7 +71,7 @@ export default defineComponent({
return props.field?.schema?.is_nullable === false;
});
return { defaultValue, isRequired };
return { t, defaultValue, isRequired };
},
});
</script>

View File

@@ -21,9 +21,9 @@
<form-field-menu
:field="field"
:value="_value"
:model-value="internalValue"
:initial-value="initialValue"
@input="emitValue($event)"
@update:model-value="emitValue($event)"
@unset="$emit('unset', $event)"
@edit-raw="showRaw = true"
/>
@@ -32,24 +32,24 @@
<form-field-interface
:autofocus="autofocus"
:value="_value"
:model-value="internalValue"
:field="field"
:loading="loading"
:batch-mode="batchMode"
:batch-active="batchActive"
:disabled="isDisabled"
:primary-key="primaryKey"
@input="emitValue($event)"
@update:model-value="emitValue($event)"
/>
<v-dialog v-model="showRaw" @esc="showRaw = false">
<v-card>
<v-card-title>{{ $t('edit_raw_value') }}</v-card-title>
<v-card-title>{{ t('edit_raw_value') }}</v-card-title>
<v-card-text>
<v-textarea class="raw-value" v-model="rawValue" :placeholder="$t('enter_raw_value')" />
<v-textarea class="raw-value" v-model="rawValue" :placeholder="t('enter_raw_value')" />
</v-card-text>
<v-card-actions>
<v-button @click="showRaw = false">{{ $t('done') }}</v-button>
<v-button @click="showRaw = false">{{ t('done') }}</v-button>
</v-card-actions>
</v-card>
</v-dialog>
@@ -63,7 +63,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref } from 'vue';
import { Field } from '@/types/';
import { md } from '@/utils/md';
import FormFieldLabel from './form-field-label.vue';
@@ -72,9 +73,9 @@ import FormFieldInterface from './form-field-interface.vue';
import { ValidationError } from './types';
import { getJSType } from '@/utils/get-js-type';
import { isEqual } from 'lodash';
import { i18n } from '@/lang';
export default defineComponent({
emits: ['toggle-batch', 'unset', 'update:modelValue'],
components: { FormFieldLabel, FormFieldMenu, FormFieldInterface },
props: {
field: {
@@ -93,7 +94,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: [String, Number, Object, Array, Boolean],
default: undefined,
},
@@ -119,6 +120,8 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const isDisabled = computed(() => {
if (props.disabled) return true;
if (props.field?.meta?.readonly === true) return true;
@@ -133,14 +136,14 @@ export default defineComponent({
return null;
});
const _value = computed(() => {
if (props.value !== undefined) return props.value;
const internalValue = computed(() => {
if (props.modelValue !== undefined) return props.modelValue;
if (props.initialValue !== undefined) return props.initialValue;
return defaultValue.value;
});
const isEdited = computed<boolean>(() => {
return props.value !== undefined && isEqual(props.value, props.initialValue) === false;
return props.modelValue !== undefined && isEqual(props.modelValue, props.initialValue) === false;
});
const { showRaw, rawValue } = useRaw();
@@ -149,13 +152,13 @@ export default defineComponent({
if (!props.validationError) return null;
if (props.validationError.code === 'RECORD_NOT_UNIQUE') {
return i18n.t('validationError.unique');
return t('validationError.unique');
} else {
return i18n.t(`validationError.${props.validationError.type}`, props.validationError);
return t(`validationError.${props.validationError.type}`, props.validationError);
}
});
return { isDisabled, md, _value, emitValue, showRaw, rawValue, validationMessage, isEdited };
return { t, isDisabled, md, internalValue, emitValue, showRaw, rawValue, validationMessage, isEdited };
function emitValue(value: any) {
if (
@@ -165,7 +168,7 @@ export default defineComponent({
) {
emit('unset', props.field);
} else {
emit('input', value);
emit('update:modelValue', value);
}
}
@@ -180,30 +183,30 @@ export default defineComponent({
get() {
switch (type.value) {
case 'object':
return JSON.stringify(_value.value, null, '\t');
return JSON.stringify(internalValue.value, null, '\t');
case 'string':
case 'number':
case 'boolean':
default:
return _value.value;
return internalValue.value;
}
},
set(newRawValue: string) {
switch (type.value) {
case 'string':
emit('input', newRawValue);
emit('update:modelValue', newRawValue);
break;
case 'number':
emit('input', Number(newRawValue));
emit('update:modelValue', Number(newRawValue));
break;
case 'boolean':
emit('input', newRawValue === 'true');
emit('update:modelValue', newRawValue === 'true');
break;
case 'object':
emit('input', JSON.parse(newRawValue));
emit('update:modelValue', JSON.parse(newRawValue));
break;
default:
emit('input', newRawValue);
emit('update:modelValue', newRawValue);
break;
}
},

View File

@@ -1,9 +1,8 @@
import { Field, FilterOperator } from '@/types';
import { TranslateResult } from 'vue-i18n';
export type FormField = DeepPartial<Field> & {
field: string;
name: string | TranslateResult;
name: string;
hideLabel?: boolean;
hideLoader?: boolean;
};

View File

@@ -2,15 +2,15 @@
<div class="v-form" ref="el" :class="gridClass">
<v-notice type="danger" v-if="unknownValidationErrors.length > 0" class="full">
<div>
<p>{{ $t('unknown_validation_errors') }}</p>
<p>{{ t('unknown_validation_errors') }}</p>
<ul>
<li v-for="(validationError, index) of unknownValidationErrors" :key="index">
<strong v-if="validationError.field">{{ validationError.field }}:</strong>
<template v-if="validationError.code === 'RECORD_NOT_UNIQUE'">
{{ $t('validationError.unique', validationError) }}
{{ t('validationError.unique', validationError) }}
</template>
<template v-else>
{{ $t(`validationError.${validationError.code}`, validationError) }}
{{ t(`validationError.${validationError.code}`, validationError) }}
</template>
</li>
</ul>
@@ -22,7 +22,7 @@
:field="field"
:autofocus="index === firstEditableFieldIndex && autofocus"
:key="field.field"
:value="(edits || {})[field.field]"
:model-value="(modelValue || {})[field.field]"
:initial-value="(initialValues || {})[field.field]"
:disabled="disabled"
:batch-mode="batchMode"
@@ -30,7 +30,7 @@
:primary-key="primaryKey"
:loading="loading"
:validation-error="validationErrors.find((err) => err.field === field.field)"
@input="setValue(field, $event)"
@update:model-value="setValue(field, $event)"
@unset="unsetValue(field)"
@toggle-batch="toggleBatchField(field)"
/>
@@ -38,7 +38,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, ref, provide } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, ref, provide } from 'vue';
import { useFieldsStore } from '@/stores/';
import { Field, FieldRaw } from '@/types';
import { useElementSize } from '@/composables/use-element-size';
@@ -54,10 +55,8 @@ type FieldValues = {
};
export default defineComponent({
emits: ['update:modelValue'],
components: { FormField },
model: {
prop: 'edits',
},
props: {
collection: {
type: String,
@@ -71,7 +70,7 @@ export default defineComponent({
type: Object as PropType<FieldValues>,
default: null,
},
edits: {
modelValue: {
type: Object as PropType<FieldValues>,
default: null,
},
@@ -102,11 +101,13 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const el = ref<Element>();
const fieldsStore = useFieldsStore();
const values = computed(() => {
return Object.assign({}, props.initialValues, props.edits);
return Object.assign({}, props.initialValues, props.modelValue);
});
const { formFields, gridClass } = useForm();
@@ -134,6 +135,7 @@ export default defineComponent({
provide('values', values);
return {
t,
el,
formFields,
gridClass,
@@ -205,16 +207,16 @@ export default defineComponent({
}
function setValue(field: Field, value: any) {
const edits = props.edits ? clone(props.edits) : {};
const edits = props.modelValue ? clone(props.modelValue) : {};
edits[field.field] = value;
emit('input', edits);
emit('update:modelValue', edits);
}
function unsetValue(field: Field) {
if (field.field in props.edits || {}) {
const newEdits = { ...props.edits };
if (field.field in (props.modelValue || {})) {
const newEdits = { ...props.modelValue };
delete newEdits[field.field];
emit('input', newEdits);
emit('update:modelValue', newEdits);
}
}

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/composition-api';
import { defineComponent, ref } from 'vue';
export default defineComponent({
props: {

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 22 22"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 24 24"
width="24"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,4 @@
<template functional>
<template>
<svg
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,10 +1,10 @@
<template>
<span
class="v-icon"
:class="[sizeClass, { 'has-click': !disabled && hasClick, left, right }]"
:role="hasClick ? 'button' : null"
:class="[sizeClass, { 'has-click': !disabled && clickable, left, right }]"
:role="clickable ? 'button' : null"
@click="emitClick"
:tabindex="hasClick ? 0 : null"
:tabindex="clickable ? 0 : null"
:style="{ '--v-icon-color': color }"
>
<component v-if="customIconName" :is="customIconName" />
@@ -13,7 +13,7 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
import CustomIconDirectus from './custom-icons/directus.vue';
@@ -55,6 +55,7 @@ const customIcons: string[] = [
];
export default defineComponent({
emits: ['click'],
components: {
CustomIconDirectus,
CustomIconBookmarkSave,
@@ -99,13 +100,18 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
color: {
type: String,
default: null,
},
...sizeProps,
},
setup(props, { emit, listeners }) {
setup(props, { emit }) {
const sizeClass = computed<string | null>(() => {
if (props.sup) return 'sup';
return useSizeClass(props).value;
@@ -116,12 +122,9 @@ export default defineComponent({
return null;
});
const hasClick = computed<boolean>(() => 'click' in listeners);
return {
sizeClass,
customIconName,
hasClick,
emitClick,
};

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
export default defineComponent({
props: {

View File

@@ -1,29 +1,25 @@
<template>
<div
class="v-input"
@click="$emit('click', $event)"
:class="{ 'full-width': fullWidth, 'has-click': hasClick, disabled: disabled }"
>
<div class="v-input" @click="$emit('click', $event)" :class="classes">
<div v-if="$slots['prepend-outer']" class="prepend-outer">
<slot name="prepend-outer" :value="value" :disabled="disabled" />
<slot name="prepend-outer" :value="modelValue" :disabled="disabled" />
</div>
<div class="input" :class="{ disabled, active }">
<div v-if="$slots.prepend" class="prepend">
<slot name="prepend" :value="value" :disabled="disabled" />
<slot name="prepend" :value="modelValue" :disabled="disabled" />
</div>
<span v-if="prefix" class="prefix">{{ prefix }}</span>
<slot name="input">
<input
v-bind="$attrs"
v-bind="attributes"
v-focus="autofocus"
v-on="_listeners"
v-on="listeners"
:autocomplete="autocomplete"
:type="type"
:min="min"
:max="max"
:step="step"
:disabled="disabled"
:value="value"
:value="modelValue"
ref="input"
/>
</slot>
@@ -33,6 +29,7 @@
:class="{ disabled: !isStepUpAllowed }"
name="keyboard_arrow_up"
class="step-up"
clickable
@click="stepUp"
:disabled="!isStepUpAllowed"
/>
@@ -40,25 +37,28 @@
:class="{ disabled: !isStepDownAllowed }"
name="keyboard_arrow_down"
class="step-down"
clickable
@click="stepDown"
:disabled="!isStepDownAllowed"
/>
</span>
<div v-if="$slots.append" class="append">
<slot name="append" :value="value" :disabled="disabled" />
<slot name="append" :value="modelValue" :disabled="disabled" />
</div>
</div>
<div v-if="$slots['append-outer']" class="append-outer">
<slot name="append-outer" :value="value" :disabled="disabled" />
<slot name="append-outer" :value="modelValue" :disabled="disabled" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from '@vue/composition-api';
import { defineComponent, computed, ref } from 'vue';
import slugify from '@sindresorhus/slugify';
import { omit } from 'lodash';
export default defineComponent({
emits: ['click', 'keydown', 'update:modelValue'],
inheritAttrs: false,
props: {
autofocus: {
@@ -69,6 +69,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
prefix: {
type: String,
default: null,
@@ -81,7 +85,7 @@ export default defineComponent({
type: Boolean,
default: true,
},
value: {
modelValue: {
type: [String, Number],
default: null,
},
@@ -135,32 +139,36 @@ export default defineComponent({
default: 'off',
},
},
setup(props, { emit, listeners }) {
setup(props, { emit, attrs }) {
const input = ref<HTMLInputElement | null>(null);
const _listeners = computed(() => ({
...listeners,
const listeners = computed(() => ({
input: emitValue,
keydown: processValue,
blur: (e: Event) => {
trimIfEnabled();
listeners.blur?.(e);
attrs?.onBlur?.(e);
},
}));
const hasClick = computed(() => {
return listeners.click !== undefined;
});
const attributes = computed(() => omit(attrs, ['class']));
const classes = computed(() => [
{
'full-width': props.fullWidth,
'has-click': props.clickable,
disabled: props.disabled,
},
...((attrs.class || '') as string).split(' '),
]);
const isStepUpAllowed = computed(() => {
return props.disabled === false && (props.max === null || parseInt(String(props.value), 10) < props.max);
return props.disabled === false && (props.max === null || parseInt(String(props.modelValue), 10) < props.max);
});
const isStepDownAllowed = computed(() => {
return props.disabled === false && (props.min === null || parseInt(String(props.value), 10) > props.min);
return props.disabled === false && (props.min === null || parseInt(String(props.modelValue), 10) > props.min);
});
return { _listeners, hasClick, stepUp, stepDown, isStepUpAllowed, isStepDownAllowed, input };
return { listeners, attributes, classes, stepUp, stepDown, isStepUpAllowed, isStepDownAllowed, input };
function processValue(event: KeyboardEvent) {
if (!event.key) return;
@@ -201,8 +209,8 @@ export default defineComponent({
}
function trimIfEnabled() {
if (props.value && props.trim) {
emit('input', String(props.value).trim());
if (props.modelValue && props.trim) {
emit('update:modelValue', String(props.modelValue).trim());
}
}
@@ -210,12 +218,12 @@ export default defineComponent({
let value = (event.target as HTMLInputElement).value;
if (props.nullable === true && !value) {
emit('input', null);
emit('update:modelValue', null);
return;
}
if (props.type === 'number') {
emit('input', Number(value));
emit('update:modelValue', Number(value));
} else {
if (props.slug === true) {
const endsWithSpace = value.endsWith(' ');
@@ -230,7 +238,7 @@ export default defineComponent({
value = value.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}
emit('input', value);
emit('update:modelValue', value);
}
}
@@ -241,7 +249,7 @@ export default defineComponent({
input.value.stepUp();
if (input.value.value != null) {
return emit('input', Number(input.value.value));
return emit('update:modelValue', Number(input.value.value));
}
}
@@ -252,9 +260,9 @@ export default defineComponent({
input.value.stepDown();
if (input.value.value) {
return emit('input', Number(input.value.value));
return emit('update:modelValue', Number(input.value.value));
} else {
return emit('input', props.min || 0);
return emit('update:modelValue', props.min || 0);
}
}
},
@@ -396,6 +404,7 @@ body {
}
/* Firefox */
&[type='number'] {
-moz-appearance: textfield;
}
@@ -418,6 +427,7 @@ body {
input {
pointer-events: none;
.prefix,
.suffix {
color: var(--foreground-subdued);

View File

@@ -5,10 +5,11 @@
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs } from '@vue/composition-api';
import { defineComponent, PropType, toRefs } from 'vue';
import { useGroupableParent } from '@/composables/groupable';
export default defineComponent({
emits: ['update:modelValue'],
props: {
mandatory: {
type: Boolean,
@@ -22,7 +23,7 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
@@ -32,11 +33,11 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { value: selection, multiple, max, mandatory } = toRefs(props);
const { modelValue: selection, multiple, max, mandatory } = toRefs(props);
useGroupableParent(
{
selection: selection,
onSelectionChange: (newSelectionValues) => emit('input', newSelectionValues),
onSelectionChange: (newSelectionValues) => emit('update:modelValue', newSelectionValues),
},
{
multiple: multiple,

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent, toRefs } from '@vue/composition-api';
import { defineComponent, toRefs } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({

View File

@@ -7,6 +7,7 @@
:exact="exact"
:disabled="disabled"
:dense="dense"
clickable
@click="onClick"
>
<slot name="activator" :active="groupActive" />
@@ -23,10 +24,11 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({
emits: ['click'],
props: {
multiple: {
type: Boolean,
@@ -48,6 +50,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: false,
},
scope: {
type: String,
default: undefined,
@@ -61,7 +67,7 @@ export default defineComponent({
default: false,
},
},
setup(props, { listeners, emit }) {
setup(props, { emit }) {
const { active: groupActive, toggle } = useGroupable({
group: props.scope,
value: props.value,
@@ -71,7 +77,7 @@ export default defineComponent({
function onClick(event: MouseEvent) {
if (props.to) return null;
if (listeners.click) return emit('click', event);
if (props.clickable) return emit('click', event);
event.stopPropagation();
toggle();

View File

@@ -11,7 +11,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-list-item-content {
display: flex;
flex-basis: 0;
@@ -23,28 +23,26 @@ body {
padding: var(--v-list-item-content-padding);
overflow: hidden;
font-family: var(--v-list-item-content-font-family);
}
.v-list.three-line &,
.v-list-item.three-line & {
align-self: stretch;
}
.v-list.three-line .v-list-item-content,
.v-list-item.three-line .v-list-item-content {
align-self: stretch;
}
::v-deep {
& > * {
flex-basis: 100%;
flex-grow: 1;
flex-shrink: 0;
line-height: 1.4;
.v-list-item-content > :deep(*) {
flex-basis: 100%;
flex-grow: 1;
flex-shrink: 0;
line-height: 1.4;
}
&:not(:last-child) {
margin-bottom: 2px;
}
}
}
.v-list-item-content > :slotted(*:not(:last-child)) {
margin-bottom: 2px;
}
.v-list:not(.large) &,
.v-list-item:not(.large) & {
--v-list-item-content-padding: 4px 0;
}
.v-list:not(.large) .v-list-item-content,
.v-list-item:not(.large) .v-list-item-content {
--v-list-item-content-padding: 4px 0;
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -30,6 +30,7 @@ export default defineComponent({
&:first-child {
margin-right: 12px;
}
&:last-child {
margin-left: 12px;
}
@@ -41,20 +42,24 @@ export default defineComponent({
#{$this} {
margin-top: 4px;
margin-bottom: 4px;
&:not(:only-child) {
&:first-child {
margin-right: 16px;
}
&:last-child {
margin-left: 16px;
}
}
}
&.large {
&.three-line,
&.two-line {
#{$this} {
align-self: flex-start;
&.center {
align-self: center;
}

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -61,11 +61,11 @@ body {
}
}
&.large #{$this} .v-icon {
&.large #{$this} :slotted(.v-icon) {
--v-icon-color: none;
}
&.disabled #{$this} .v-icon {
&.disabled #{$this} :slotted(.v-icon) {
--v-icon-color: var(--foreground-subdued) !important;
}
}

View File

@@ -1,14 +1,13 @@
<template>
<component
:is="component"
active-class="active"
v-bind="disabled === false && $attrs"
class="v-list-item"
:exact="exact"
:to="to"
:class="{
active,
active: isActiveRoute,
dense,
link: isClickable,
link: isLink,
disabled,
dashed,
block,
@@ -17,16 +16,16 @@
:href="href"
:download="download"
:target="component === 'a' ? '_blank' : null"
v-on="disabled === false && $listeners"
>
<slot />
</component>
</template>
<script lang="ts">
import { Location } from 'vue-router';
import { defineComponent, PropType, computed } from '@vue/composition-api';
import { RouteLocation, useLink, useRoute } from 'vue-router';
import { defineComponent, PropType, computed } from 'vue';
import { useGroupable } from '@/composables/groupable';
import { isEqual } from 'lodash';
export default defineComponent({
props: {
@@ -39,7 +38,7 @@ export default defineComponent({
default: false,
},
to: {
type: [String, Object] as PropType<string | Location>,
type: [String, Object] as PropType<string | RouteLocation>,
default: null,
},
href: {
@@ -50,10 +49,14 @@ export default defineComponent({
type: Boolean,
default: false,
},
active: {
clickable: {
type: Boolean,
default: false,
},
active: {
type: Boolean,
default: undefined,
},
dashed: {
type: Boolean,
default: false,
@@ -62,6 +65,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
query: {
type: Boolean,
default: false,
},
download: {
type: String,
default: null,
@@ -75,7 +82,11 @@ export default defineComponent({
default: false,
},
},
setup(props, { listeners }) {
setup(props) {
const route = useRoute();
const { route: linkRoute, isActive, isExactActive } = useLink(props);
const component = computed<string>(() => {
if (props.to) return 'router-link';
if (props.href) return 'a';
@@ -86,9 +97,25 @@ export default defineComponent({
value: props.value,
});
const isClickable = computed(() => Boolean(props.to || props.href || listeners.click !== undefined));
const isLink = computed(() => Boolean(props.to || props.href || props.clickable));
return { component, isClickable };
const isActiveRoute = computed(() => {
if (props.active !== undefined) return props.active;
if (props.to) {
const isQueryActive = !props.query || isEqual(route.query, linkRoute.value.query);
if (!props.exact) {
return isActive.value && isQueryActive;
} else {
return isExactActive.value && isQueryActive;
}
}
return false;
});
return { component, isLink, isActiveRoute };
},
});
</script>
@@ -177,13 +204,13 @@ body {
}
&.dense {
::v-deep .v-text-overflow {
:deep(.v-text-overflow) {
color: var(--foreground-normal);
}
&:hover,
&.active {
::v-deep .v-text-overflow {
:deep(.v-text-overflow) {
color: var(--primary);
}
}
@@ -200,7 +227,7 @@ body {
border-radius: var(--border-radius);
transition: border-color var(--fast) var(--transition);
.v-icon {
:slotted(.v-icon) {
color: var(--foreground-subdued);
&:hover {
@@ -208,15 +235,15 @@ body {
}
}
.drag-handle {
:slotted(.drag-handle) {
cursor: grab;
}
.drag-handle:active {
:slotted(.drag-handle:active) {
cursor: grabbing;
}
.spacer {
:slotted(.spacer) {
flex-grow: 1;
}

View File

@@ -5,16 +5,13 @@
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs } from '@vue/composition-api';
import { defineComponent, PropType, toRefs } from 'vue';
import { useGroupableParent } from '@/composables/groupable';
export default defineComponent({
model: {
prop: 'activeItems',
event: 'input',
},
emits: ['update:modelValue'],
props: {
activeItems: {
modelValue: {
type: Array as PropType<(number | string)[]>,
default: null,
},
@@ -32,13 +29,12 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { activeItems, multiple, mandatory } = toRefs(props);
const { modelValue, multiple, mandatory } = toRefs(props);
useGroupableParent(
{
selection: activeItems,
selection: modelValue,
onSelectionChange: (newSelection) => {
emit('input', newSelection);
emit('update:modelValue', newSelection);
},
},
{
@@ -52,8 +48,8 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-list-padding: 4px 0;
--v-list-max-height: none;
--v-list-max-width: none;
@@ -65,9 +61,7 @@ body {
--v-list-background-color-hover: var(--background-normal);
--v-list-background-color-active: var(--background-normal);
}
</style>
<style lang="scss" scoped>
.v-list {
position: static;
display: block;
@@ -80,18 +74,18 @@ body {
color: var(--v-list-color);
line-height: 22px;
border-radius: var(--border-radius);
}
&.large {
--v-list-padding: 12px;
}
.large {
--v-list-padding: 12px;
}
::v-deep .v-divider {
max-width: calc(100% - 16px);
margin: 8px;
}
:slotted(.v-divider) {
max-width: calc(100% - 16px);
margin: 8px;
}
::v-deep .v-button {
pointer-events: all;
}
:slotted(*) {
pointer-events: all;
}
</style>

View File

@@ -1,3 +1,4 @@
import { createPopper } from '@popperjs/core/lib/popper-lite';
import { Instance, Modifier, Placement } from '@popperjs/core';
import arrow from '@popperjs/core/lib/modifiers/arrow';
import computeStyles from '@popperjs/core/lib/modifiers/computeStyles';
@@ -6,8 +7,7 @@ import flip from '@popperjs/core/lib/modifiers/flip';
import offset from '@popperjs/core/lib/modifiers/offset';
import popperOffsets from '@popperjs/core/lib/modifiers/popperOffsets';
import preventOverflow from '@popperjs/core/lib/modifiers/preventOverflow';
import { createPopper } from '@popperjs/core/lib/popper-base';
import { onUnmounted, ref, Ref, watch } from '@vue/composition-api';
import { onUnmounted, ref, Ref, watch } from 'vue';
export function usePopper(
reference: Ref<HTMLElement | null>,
@@ -53,7 +53,7 @@ export function usePopper(
popperInstance.value.forceUpdate();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
observer.observe(popper.value!, {
attributes: true,
attributes: false,
childList: true,
characterData: true,
subtree: true,

View File

@@ -18,53 +18,54 @@
/>
</div>
<portal to="menu-outlet">
<div
v-if="isActive"
class="v-menu-popper"
:key="id"
:id="id"
:class="{ active: isActive, attached }"
:data-placement="popperPlacement"
:style="styles"
v-click-outside="{
handler: deactivate,
middleware: onClickOutsideMiddleware,
disabled: isActive === false || closeOnClick === false,
events: ['click'],
}"
>
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
<div class="v-menu-content" @click.stop="onContentClick">
<slot
:active="isActive"
v-bind="{
toggle: toggle,
active: isActive,
activate: activate,
deactivate: deactivate,
}"
/>
<teleport to="#menu-outlet">
<transition-bounce>
<div
v-if="isActive"
class="v-menu-popper"
:key="id"
:id="id"
:class="{ active: isActive, attached }"
:data-placement="popperPlacement"
:style="styles"
v-click-outside="{
handler: deactivate,
middleware: onClickOutsideMiddleware,
disabled: isActive === false || closeOnClick === false,
events: ['click'],
}"
>
<div class="arrow" :class="{ active: showArrow && isActive }" :style="arrowStyles" data-popper-arrow />
<div class="v-menu-content" @click.stop="onContentClick">
<slot
v-bind="{
toggle: toggle,
active: isActive,
activate: activate,
deactivate: deactivate,
}"
/>
</div>
</div>
</div>
</portal>
</transition-bounce>
</teleport>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, PropType, computed, watch } from '@vue/composition-api';
import { defineComponent, ref, PropType, computed, watch, nextTick } from 'vue';
import { usePopper } from './use-popper';
import { Placement } from '@popperjs/core';
import { nanoid } from 'nanoid';
import Vue from 'vue';
export default defineComponent({
emits: ['update:modelValue'],
props: {
placement: {
type: String as PropType<Placement>,
default: 'bottom',
},
value: {
modelValue: {
type: Boolean,
default: undefined,
},
@@ -138,9 +139,9 @@ export default defineComponent({
watch(isActive, (newActive) => {
if (newActive === true) {
reference.value = ((activator.value as HTMLElement)?.childNodes[0] as HTMLElement) || virtualReference.value;
reference.value = (activator.value?.children[0] as HTMLElement) || virtualReference.value;
Vue.nextTick(() => {
nextTick(() => {
popper.value = document.getElementById(id.value);
});
}
@@ -175,15 +176,15 @@ export default defineComponent({
const isActive = computed<boolean>({
get() {
if (props.value !== undefined) {
return props.value;
if (props.modelValue !== undefined) {
return props.modelValue;
}
return localIsActive.value;
},
async set(newActive) {
localIsActive.value = newActive;
emit('input', newActive);
emit('update:modelValue', newActive);
},
});

View File

@@ -6,7 +6,7 @@
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from '@vue/composition-api';
import { defineComponent, computed, PropType } from 'vue';
export default defineComponent({
props: {
@@ -55,7 +55,7 @@ body {
}
</style>
<style lang="scss" scoped>
<style scoped>
.v-notice {
display: flex;
align-items: center;
@@ -66,45 +66,43 @@ body {
color: var(--v-notice-color);
background-color: var(--v-notice-background-color);
border-radius: var(--border-radius);
}
.v-icon {
--v-icon-color: var(--v-notice-icon-color);
}
.v-icon {
--v-icon-color: var(--v-notice-icon-color);
}
&.info {
--v-notice-icon-color: var(--primary);
--v-notice-background-color: var(--background-normal);
--v-notice-color: var(--foreground-normal);
}
.info {
--v-notice-icon-color: var(--primary);
--v-notice-background-color: var(--background-normal);
--v-notice-color: var(--foreground-normal);
}
&.success {
--v-notice-icon-color: var(--success);
--v-notice-background-color: var(--success-alt);
--v-notice-color: var(--success);
}
.success {
--v-notice-icon-color: var(--success);
--v-notice-background-color: var(--success-alt);
--v-notice-color: var(--success);
}
&.warning {
--v-notice-icon-color: var(--warning);
--v-notice-background-color: var(--warning-alt);
--v-notice-color: var(--warning);
}
.warning {
--v-notice-icon-color: var(--warning);
--v-notice-background-color: var(--warning-alt);
--v-notice-color: var(--warning);
}
&.danger {
--v-notice-icon-color: var(--danger);
--v-notice-background-color: var(--danger-alt);
--v-notice-color: var(--danger);
}
.danger {
--v-notice-icon-color: var(--danger);
--v-notice-background-color: var(--danger-alt);
--v-notice-color: var(--danger);
}
&.center {
display: flex;
align-items: center;
justify-content: center;
}
.center {
display: flex;
align-items: center;
justify-content: center;
}
::v-deep {
a {
text-decoration: underline;
}
}
:slotted(a) {
text-decoration: underline;
}
</style>

View File

@@ -1,14 +1,15 @@
<template>
<div class="v-overlay" :class="{ active, absolute, 'has-click': hasClick }" @click="onClick">
<div class="v-overlay" :class="{ active, absolute, 'has-click': clickable }" @click="onClick">
<div class="overlay" />
<div v-if="active" class="content"><slot /></div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['click'],
props: {
active: {
type: Boolean,
@@ -18,11 +19,13 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners }) {
const hasClick = computed<boolean>(() => 'click' in listeners);
return { hasClick, onClick };
setup(props, { emit }) {
return { onClick };
function onClick(event: MouseEvent) {
emit('click', event);

View File

@@ -1,11 +1,11 @@
<template>
<div class="v-pagination">
<v-button class="previous" :disabled="disabled || value === 1" secondary icon small @click="toPrev">
<v-button class="previous" :disabled="disabled || modelValue === 1" secondary icon small @click="toPrev">
<v-icon name="chevron_left" />
</v-button>
<v-button
v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > totalVisible"
v-if="showFirstLast && modelValue > Math.ceil(totalVisible / 2) + 1 && length > totalVisible"
class="page"
@click="toPage(1)"
secondary
@@ -15,14 +15,14 @@
1
</v-button>
<span v-if="showFirstLast && value > Math.ceil(totalVisible / 2) + 1 && length > totalVisible + 1" class="gap">
<span v-if="showFirstLast && modelValue > Math.ceil(totalVisible / 2) + 1 && length > totalVisible + 1" class="gap">
...
</span>
<v-button
v-for="page in visiblePages"
:key="page"
:class="{ active: value === page }"
:class="{ active: modelValue === page }"
class="page"
@click="toPage(page)"
secondary
@@ -32,13 +32,16 @@
{{ page }}
</v-button>
<span v-if="showFirstLast && value < length - Math.ceil(totalVisible / 2) && length > totalVisible + 1" class="gap">
<span
v-if="showFirstLast && modelValue < length - Math.ceil(totalVisible / 2) && length > totalVisible + 1"
class="gap"
>
...
</span>
<v-button
v-if="showFirstLast && value <= length - Math.ceil(totalVisible / 2) && length > totalVisible"
:class="{ active: value === length }"
v-if="showFirstLast && modelValue <= length - Math.ceil(totalVisible / 2) && length > totalVisible"
:class="{ active: modelValue === length }"
class="page"
@click="toPage(length)"
secondary
@@ -48,17 +51,18 @@
{{ length }}
</v-button>
<v-button class="next" :disabled="disabled || value === length" secondary icon small @click="toNext">
<v-button class="next" :disabled="disabled || modelValue === length" secondary icon small @click="toNext">
<v-icon name="chevron_right" />
</v-button>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import { isEmpty } from '@/utils/is-empty';
export default defineComponent({
emits: ['update:modelValue'],
props: {
disabled: {
type: Boolean,
@@ -75,7 +79,7 @@ export default defineComponent({
default: undefined,
validator: (val: number) => val >= 0,
},
value: {
modelValue: {
type: Number,
default: null,
},
@@ -96,15 +100,15 @@ export default defineComponent({
const pagesBeforeCurrentPage = Math.floor(props.totalVisible / 2);
const pagesAfterCurrentPage = Math.ceil(props.totalVisible / 2) - 1;
if (props.value <= pagesBeforeCurrentPage) {
if (props.modelValue <= pagesBeforeCurrentPage) {
startPage = 1;
endPage = props.totalVisible;
} else if (props.value + pagesAfterCurrentPage >= props.length) {
} else if (props.modelValue + pagesAfterCurrentPage >= props.length) {
startPage = props.length - props.totalVisible + 1;
endPage = props.length;
} else {
startPage = props.value - pagesBeforeCurrentPage;
endPage = props.value + pagesAfterCurrentPage;
startPage = props.modelValue - pagesBeforeCurrentPage;
endPage = props.modelValue + pagesAfterCurrentPage;
}
}
@@ -114,79 +118,77 @@ export default defineComponent({
return { toPage, toPrev, toNext, visiblePages };
function toPrev() {
toPage(props.value - 1);
toPage(props.modelValue - 1);
}
function toNext() {
toPage(props.value + 1);
toPage(props.modelValue + 1);
}
function toPage(page: number) {
emit('input', page);
emit('update:modelValue', page);
}
},
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-pagination-active-color: var(--primary);
}
</style>
<style lang="scss" scoped>
@import '@/styles/mixins/breakpoint';
.v-pagination {
display: flex;
}
.gap {
display: none;
margin: 0 4px;
color: var(--foreground-subdued);
line-height: 2em;
}
@media (min-width: 600px) {
.gap {
display: none;
margin: 0 4px;
color: var(--foreground-subdued);
line-height: 2em;
@include breakpoint(small) {
display: inline;
}
}
.v-button {
--v-button-background-color-hover: var(--background-normal);
--v-button-background-color: var(--background-subdued);
--v-button-color: var(--foreground-normal);
margin: 0 2px;
vertical-align: middle;
&.page:not(.active) {
display: none;
@include breakpoint(small) {
display: inline;
}
}
& ::v-deep {
.small {
--v-button-min-width: 32px;
}
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&.active {
--v-button-background-color-hover: var(--primary);
--v-button-color-hover: var(--foreground-inverted);
--v-button-background-color: var(--primary);
--v-button-color: var(--foreground-inverted);
}
display: inline;
}
}
.v-button {
--v-button-background-color-hover: var(--background-normal);
--v-button-background-color: var(--background-subdued);
--v-button-color: var(--foreground-normal);
margin: 0 2px;
vertical-align: middle;
}
.v-button.page:not(.active) {
display: none;
}
@media (min-width: 600px) {
.v-button.page:not(.active) {
display: inline;
}
}
.v-button :deep(.small) {
--v-button-min-width: 32px;
}
.v-button:first-child {
margin-left: 0;
}
.v-button:last-child {
margin-right: 0;
}
.v-button.active {
--v-button-background-color-hover: var(--primary);
--v-button-color-hover: var(--foreground-inverted);
--v-button-background-color: var(--primary);
--v-button-color: var(--foreground-inverted);
}
</style>

View File

@@ -23,10 +23,11 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class';
export default defineComponent({
emits: ['animationiteration'],
props: {
indeterminate: {
type: Boolean,

View File

@@ -22,9 +22,10 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['animationiteration'],
props: {
absolute: {
type: Boolean,

View File

@@ -15,19 +15,16 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:modelValue'],
props: {
value: {
type: String,
required: true,
},
inputValue: {
modelValue: {
type: String,
default: null,
},
@@ -54,7 +51,7 @@ export default defineComponent({
},
setup(props, { emit }) {
const isChecked = computed<boolean>(() => {
return props.inputValue === props.value;
return props.modelValue === props.value;
});
const icon = computed<string>(() => {
@@ -64,7 +61,7 @@ export default defineComponent({
return { isChecked, emitValue, icon };
function emitValue(): void {
emit('change', props.value);
emit('update:modelValue', props.value);
}
},
});

View File

@@ -15,25 +15,26 @@
v-else
:full-width="fullWidth"
readonly
:value="displayValue"
:model-value="displayValue"
clickable
@click="toggle"
:placeholder="placeholder"
:disabled="disabled"
:active="active"
>
<template #prepend><slot name="prepend" /></template>
<template v-if="$slots.prepend" #prepend><slot name="prepend" /></template>
<template #append><v-icon name="expand_more" :class="{ active }" /></template>
</v-input>
</template>
<v-list class="list">
<template v-if="showDeselect">
<v-list-item @click="$emit('input', null)" :disabled="value === null">
<v-list-item clickable @click="$emit('update:modelValue', null)" :disabled="modelValue === null">
<v-list-item-icon v-if="multiple === true">
<v-icon name="close" />
</v-list-item-icon>
<v-list-item-content>
{{ multiple ? $t('deselect_all') : $t('deselect') }}
{{ multiple ? t('deselect_all') : t('deselect') }}
</v-list-item-content>
<v-list-item-icon v-if="multiple === false">
<v-icon name="close" />
@@ -42,15 +43,15 @@
<v-divider />
</template>
<template v-for="(item, index) in _items">
<v-divider :key="index" v-if="item.divider === true" />
<template v-for="(item, index) in internalItems" :key="index">
<v-divider v-if="item.divider === true" />
<v-list-item
v-else
:key="item.text + item.value"
:active="multiple ? (value || []).includes(item.value) : value === item.value"
:active="multiple ? (modelValue || []).includes(item.value) : modelValue === item.value"
:disabled="item.disabled"
@click="multiple ? null : $emit('input', item.value)"
clickable
@click="multiple ? null : $emit('update:modelValue', item.value)"
>
<v-list-item-icon v-if="multiple === false && allowOther === false && itemIcon !== null && item.icon">
<v-icon :name="item.icon" />
@@ -59,11 +60,11 @@
<span v-if="multiple === false" class="item-text">{{ item.text }}</span>
<v-checkbox
v-else
:inputValue="value || []"
:model-value="modelValue || []"
:label="item.text"
:value="item.value"
:disabled="item.disabled"
@change="$emit('input', $event.length > 0 ? $event : null)"
@update:model-value="$emit('update:modelValue', $event.length > 0 ? $event : null)"
/>
</v-list-item-content>
</v-list-item>
@@ -73,9 +74,9 @@
<v-list-item-content>
<input
class="other-input"
@focus="otherValue ? $emit('input', otherValue) : null"
@focus="otherValue ? $emit('update:modelValue', otherValue) : null"
v-model="otherValue"
:placeholder="$t('other')"
:placeholder="t('other')"
/>
</v-list-item-content>
</v-list-item>
@@ -84,34 +85,34 @@
<v-list-item
v-for="otherValue in otherValues"
:key="otherValue.key"
:active="(value || []).includes(otherValue.value)"
:active="(modelValue || []).includes(otherValue.value)"
@click.stop
>
<v-list-item-icon>
<v-checkbox
:inputValue="value || []"
:model-value="modelValue || []"
:value="otherValue.value"
@change="$emit('input', $event.length > 0 ? $event : null)"
@update:model-value="$emit('update:modelValue', $event.length > 0 ? $event : null)"
/>
</v-list-item-icon>
<v-list-item-content>
<input
class="other-input"
:value="otherValue.value"
:placeholder="$t('other')"
:placeholder="t('other')"
v-focus
@input="setOtherValue(otherValue.key, $event.target.value)"
@blur="otherValue.value.length === 0 && setOtherValue(otherValue.key, null)"
/>
</v-list-item-content>
<v-list-item-icon>
<v-icon name="close" @click="setOtherValue(otherValue.key, null)" />
<v-icon name="close" clickable @click="setOtherValue(otherValue.key, null)" />
</v-list-item-icon>
</v-list-item>
<v-list-item @click.stop="addOtherValue()">
<v-list-item-icon><v-icon name="add" /></v-list-item-icon>
<v-list-item-content>{{ $t('other') }}</v-list-item-content>
<v-list-item-content>{{ t('other') }}</v-list-item-content>
</v-list-item>
</template>
</v-list>
@@ -119,8 +120,8 @@
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRefs, Ref } from '@vue/composition-api';
import i18n from '@/lang';
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, computed, toRefs, Ref } from 'vue';
import { useCustomSelection, useCustomSelectionMultiple } from '@/composables/use-custom-selection';
import { get } from 'lodash';
@@ -128,6 +129,7 @@ type ItemsRaw = (string | any)[];
type InputValue = string[] | string;
export default defineComponent({
emits: ['update:modelValue'],
props: {
items: {
type: Array as PropType<ItemsRaw>,
@@ -149,7 +151,7 @@ export default defineComponent({
type: String,
default: 'disabled',
},
value: {
modelValue: {
type: [Array, String, Number, Boolean] as PropType<InputValue>,
default: null,
},
@@ -191,28 +193,24 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { _items } = useItems();
const { t } = useI18n();
const { internalItems } = useItems();
const { displayValue } = useDisplayValue();
const { value } = toRefs(props);
const { otherValue, usesOtherValue } = useCustomSelection(value as Ref<string>, _items, emit);
const { modelValue } = toRefs(props);
const { otherValue, usesOtherValue } = useCustomSelection(modelValue as Ref<string>, internalItems, (value) =>
emit('update:modelValue', value)
);
const { otherValues, addOtherValue, setOtherValue } = useCustomSelectionMultiple(
value as Ref<string[]>,
_items,
emit
modelValue as Ref<string[]>,
internalItems,
(value) => emit('update:modelValue', value)
);
return {
_items,
displayValue,
otherValue,
usesOtherValue,
otherValues,
addOtherValue,
setOtherValue,
};
return { t, internalItems, displayValue, otherValue, usesOtherValue, otherValues, addOtherValue, setOtherValue };
function useItems() {
const _items = computed(() => {
const internalItems = computed(() => {
const items = props.items.map((item) => {
if (typeof item === 'string') {
return {
@@ -234,50 +232,48 @@ export default defineComponent({
return items;
});
return { _items };
return { internalItems };
}
function useDisplayValue() {
const displayValue = computed(() => {
if (Array.isArray(props.value)) {
if (props.value.length < props.multiplePreviewThreshold) {
return props.value
if (Array.isArray(props.modelValue)) {
if (props.modelValue.length < props.multiplePreviewThreshold) {
return props.modelValue
.map((value) => {
return getTextForValue(value) || value;
})
.join(', ');
} else {
const itemCount = _items.value.length + otherValues.value.length;
const selectionCount = props.value.length;
const itemCount = internalItems.value.length + otherValues.value.length;
const selectionCount = props.modelValue.length;
if (itemCount === selectionCount) {
return i18n.t('all_items');
return t('all_items');
} else {
return i18n.tc('item_count', selectionCount);
return t('item_count', selectionCount);
}
}
}
return getTextForValue(props.value) || props.value;
return getTextForValue(props.modelValue) || props.modelValue;
});
return { displayValue };
function getTextForValue(value: string | number) {
return _items.value.find((item) => item.value === value)?.['text'];
return internalItems.value.find((item) => item.value === value)?.['text'];
}
}
},
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-select-font-family: var(--family-sans-serif);
}
</style>
<style lang="scss" scoped>
.list {
--v-list-min-width: 0;
}
@@ -290,19 +286,19 @@ body {
--v-input-font-family: var(--v-select-font-family);
cursor: pointer;
}
.v-icon {
transition: transform var(--medium) var(--transition-out);
.v-input .v-icon {
transition: transform var(--medium) var(--transition-out);
}
&.active {
transform: scaleY(-1);
transition-timing-function: var(--transition-in);
}
}
.v-input .v-icon.active {
transform: scaleY(-1);
transition-timing-function: var(--transition-in);
}
::v-deep input {
cursor: pointer;
}
.v-input :deep(input) {
cursor: pointer;
}
.other-input {
@@ -318,13 +314,13 @@ body {
width: max-content;
padding-right: 18px;
cursor: pointer;
}
.v-icon {
position: absolute;
}
.inline-display .v-icon {
position: absolute;
}
&.placeholder {
color: var(--foreground-subdued);
}
.inline-display.placeholder {
color: var(--foreground-subdued);
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {},

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
@@ -136,7 +136,7 @@ body {
transition: opacity var(--medium) var(--transition);
}
.fade-enter,
.fade-enter-from,
.fade-leave-to {
position: absolute;
opacity: 0;

View File

@@ -1,13 +1,13 @@
<template>
<div class="v-slider" :style="styles">
<div v-if="$slots['prepend']" class="prepend">
<slot name="prepend" :value="value" />
<div v-if="$slots.prepend" class="prepend">
<slot name="prepend" :value="modelValue" />
</div>
<div class="slider" :class="{ disabled }">
<input
:disabled="disabled"
type="range"
:value="value"
:value="modelValue"
:max="max"
:min="min"
:step="step"
@@ -20,22 +20,23 @@
</div>
<div v-if="showThumbLabel" class="thumb-label-wrapper">
<div class="thumb-label" :class="{ visible: alwaysShowValue }">
<slot name="thumb-label type-text" :value="value">
{{ value }}
<slot name="thumb-label type-text" :value="modelValue">
{{ modelValue }}
</slot>
</div>
</div>
</div>
<div v-if="$slots['append']" class="append">
<slot name="append" :value="value" />
<div v-if="$slots.append" class="append">
<slot name="append" :value="modelValue" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
emits: ['change', 'update:modelValue'],
props: {
disabled: {
type: Boolean,
@@ -65,16 +66,16 @@ export default defineComponent({
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Number,
default: 0,
},
},
setup(props, { emit }) {
const styles = computed(() => {
if (props.value === null) return { '--_v-slider-percentage': 50 };
if (props.modelValue === null) return { '--_v-slider-percentage': 50 };
let percentage = ((props.value - props.min) / (props.max - props.min)) * 100;
let percentage = ((props.modelValue - props.min) / (props.max - props.min)) * 100;
if (isNaN(percentage)) percentage = 0;
return { '--_v-slider-percentage': percentage };
});
@@ -92,7 +93,7 @@ export default defineComponent({
function onInput(event: InputEvent) {
const target = event.target as HTMLInputElement;
emit('input', Number(target.value));
emit('update:modelValue', Number(target.value));
}
},
});
@@ -244,6 +245,7 @@ body {
transform: translateX(-50%);
opacity: 0;
transition: opacity var(--fast) var(--transition);
&.visible {
opacity: 1;
}
@@ -253,6 +255,7 @@ body {
&:focus-within:not(.disabled) {
input {
height: 4px;
&::-webkit-slider-thumb {
width: 12px;
height: 12px;
@@ -269,6 +272,7 @@ body {
cursor: ew-resize;
}
}
.thumb-label {
opacity: 1;
}

View File

@@ -15,19 +15,16 @@
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
model: {
prop: 'inputValue',
event: 'change',
},
emits: ['update:modelValue'],
props: {
value: {
type: String,
default: null,
},
inputValue: {
modelValue: {
type: [Boolean, Array],
default: false,
},
@@ -42,18 +39,18 @@ export default defineComponent({
},
setup(props, { emit }) {
const isChecked = computed<boolean>(() => {
if (props.inputValue instanceof Array) {
return props.inputValue.includes(props.value);
if (props.modelValue instanceof Array) {
return props.modelValue.includes(props.value);
}
return props.inputValue === true;
return props.modelValue === true;
});
return { isChecked, toggleInput };
function toggleInput(): void {
if (props.inputValue instanceof Array) {
const newValue = [...props.inputValue];
if (props.modelValue instanceof Array) {
const newValue = [...props.modelValue];
if (isChecked.value === false) {
newValue.push(props.value);
@@ -61,9 +58,9 @@ export default defineComponent({
newValue.splice(newValue.indexOf(props.value), 1);
}
emit('change', newValue);
emit('update:modelValue', newValue);
} else {
emit('change', !isChecked.value);
emit('update:modelValue', !isChecked.value);
}
}
},

View File

@@ -8,11 +8,15 @@
@click="toggleManualSort"
scope="col"
>
<v-icon v-tooltip="$t('toggle_manual_sorting')" name="sort" small />
<v-icon v-tooltip="t('toggle_manual_sorting')" name="sort" small />
</th>
<th v-if="showSelect" class="select cell" scope="col">
<v-checkbox :inputValue="allItemsSelected" :indeterminate="someItemsSelected" @change="toggleSelectAll" />
<v-checkbox
:model-value="allItemsSelected"
:indeterminate="someItemsSelected"
@update:model-value="toggleSelectAll"
/>
</th>
<th v-for="header in headers" :key="header.value" :class="getClassesForHeader(header)" class="cell" scope="col">
@@ -27,7 +31,7 @@
name="sort"
class="sort-icon"
small
v-tooltip.top="$t(getTooltipForSortIcon(header))"
v-tooltip.top="t(getTooltipForSortIcon(header))"
/>
</div>
<span
@@ -45,12 +49,14 @@
</template>
<script lang="ts">
import { defineComponent, ref, PropType } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, PropType } from 'vue';
import useEventListener from '@/composables/use-event-listener';
import { Header, Sort } from '../types';
import { throttle, clone } from 'lodash';
export default defineComponent({
emits: ['update:sort', 'toggle-select-all', 'update:headers'],
props: {
headers: {
type: Array as PropType<Header[]>,
@@ -98,6 +104,8 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const dragging = ref<boolean>(false);
const dragStartX = ref<number>(0);
const dragStartWidth = ref<number>(0);
@@ -107,6 +115,7 @@ export default defineComponent({
useEventListener(window, 'pointerup', onMouseUp);
return {
t,
changeSort,
dragging,
dragHeader,
@@ -266,6 +275,7 @@ export default defineComponent({
.sortable {
cursor: pointer;
.sort-icon {
margin-left: 4px;
color: var(--foreground-subdued);

View File

@@ -1,33 +1,33 @@
<template functional>
<template>
<tr
class="table-row"
:class="{ subdued: props.subdued, clickable: props.hasClickListener }"
@click="listeners.click"
:class="{ subdued: subdued, clickable: hasClickListener }"
@click="$emit('click')"
:style="{
'--table-row-height': props.height + 2 + 'px',
'--table-row-height': height + 2 + 'px',
'--table-row-line-height': 1,
}"
>
<td v-if="props.showManualSort" class="manual cell" @click.stop>
<v-icon name="drag_handle" class="drag-handle" :class="{ 'sorted-manually': props.sortedManually }" />
<td v-if="showManualSort" class="manual cell" @click.stop>
<v-icon name="drag_handle" class="drag-handle" :class="{ 'sorted-manually': sortedManually }" />
</td>
<td v-if="props.showSelect" class="select cell" @click.stop>
<v-checkbox :inputValue="props.isSelected" @change="listeners['item-selected']" />
<td v-if="showSelect" class="select cell" @click.stop>
<v-checkbox :model-value="isSelected" @update:model-value="$emit('item-selected', $event)" />
</td>
<td class="cell" :class="`align-${header.align}`" v-for="header in props.headers" :key="header.value">
<slot :name="`item.${header.value}`" :item="props.item">
<td class="cell" :class="`align-${header.align}`" v-for="header in headers" :key="header.value">
<slot :name="`item.${header.value}`" :item="item">
<v-text-overflow
v-if="
header.value.split('.').reduce((acc, val) => {
return acc[val];
}, props.item)
}, item)
"
:text="
header.value.split('.').reduce((acc, val) => {
return acc[val];
}, props.item)
}, item)
"
/>
<value-null v-else />
@@ -35,17 +35,18 @@
</td>
<td class="spacer cell" />
<td v-if="$scopedSlots['item-append']" class="append cell" @click.stop>
<td v-if="$slots['item-append']" class="append cell" @click.stop>
<slot name="item-append" />
</td>
</tr>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
import { Header } from '../types';
export default defineComponent({
emits: ['click', 'item-selected'],
props: {
headers: {
type: Array as PropType<Header[]>,

View File

@@ -1,9 +1,7 @@
import VueI18n from 'vue-i18n';
export type Alignment = 'left' | 'center' | 'right';
export type HeaderRaw = {
text: string | VueI18n.TranslateResult;
text: string;
value: string;
align?: Alignment;
sortable?: boolean;

View File

@@ -1,14 +1,14 @@
<template>
<div class="v-table" :class="{ loading, inline, disabled }">
<table
:summary="_headers.map((header) => header.text).join(', ')"
:summary="internalHeaders.map((header) => header.text).join(', ')"
:style="{
'--grid-columns': columnStyle,
}"
>
<table-header
:headers.sync="_headers"
:sort.sync="_sort"
v-model:headers="internalHeaders"
v-model:sort="internalSort"
:show-select="showSelect"
:show-resize="showResize"
:some-items-selected="someItemsSelected"
@@ -20,7 +20,7 @@
:manual-sort-key="manualSortKey"
@toggle-select-all="onToggleSelectAll"
>
<template v-for="header in _headers" #[`header.${header.value}`]>
<template v-for="header in internalHeaders" #[`header.${header.value}`]>
<slot :header="header" :name="`header.${header.value}`" />
</template>
</table-header>
@@ -42,41 +42,42 @@
<draggable
:force-fallback="true"
v-else
v-model="_items"
v-model="internalItems"
:item-key="itemKey"
tag="tbody"
handle=".drag-handle"
:disabled="disabled || _sort.by !== manualSortKey"
:disabled="disabled || internalSort.by !== manualSortKey"
:set-data="hideDragImage"
@end="onSortChange"
>
<table-row
v-for="item in _items"
:key="item[itemKey]"
:headers="_headers"
:item="item"
:show-select="!disabled && showSelect"
:show-manual-sort="!disabled && showManualSort"
:is-selected="getSelectedState(item)"
:subdued="loading"
:sorted-manually="_sort.by === manualSortKey"
:has-click-listener="!disabled && hasRowClick"
:height="rowHeight"
@click="hasRowClick ? $emit('click:row', item) : null"
@item-selected="
onItemSelected({
item: item,
value: !getSelectedState(item),
})
"
>
<template v-for="header in _headers" #[`item.${header.value}`]>
<slot :item="item" :name="`item.${header.value}`" />
</template>
<template #item="{ element }">
<table-row
:headers="internalHeaders"
:item="element"
:show-select="!disabled && showSelect"
:show-manual-sort="!disabled && showManualSort"
:is-selected="getSelectedState(element)"
:subdued="loading"
:sorted-manually="internalSort.by === manualSortKey"
:has-click-listener="!disabled && clickable"
:height="rowHeight"
@click="clickable ? $emit('click:row', element) : null"
@item-selected="
onItemSelected({
item: element,
value: !getSelectedState(element),
})
"
>
<template v-for="header in internalHeaders" #[`item.${header.value}`]>
<slot :item="element" :name="`item.${header.value}`" />
</template>
<template v-if="hasItemAppendSlot" #item-append>
<slot name="item-append" :item="item" />
</template>
</table-row>
<template v-if="hasItemAppendSlot" #item-append>
<slot name="item-append" :item="element" />
</template>
</table-row>
</template>
</draggable>
</table>
<slot name="footer" />
@@ -84,13 +85,13 @@
</template>
<script lang="ts">
import { defineComponent, computed, ref, PropType } from '@vue/composition-api';
import { defineComponent, computed, ref, PropType } from 'vue';
import { Header, HeaderRaw, Item, ItemSelectEvent, Sort } from './types';
import TableHeader from './table-header/';
import TableRow from './table-row/';
import { sortBy, clone, forEach, pick } from 'lodash';
import { i18n } from '@/lang/';
import draggable from 'vuedraggable';
import Draggable from 'vuedraggable';
import hideDragImage from '@/utils/hide-drag-image';
const HeaderDefaults: Header = {
@@ -102,14 +103,19 @@ const HeaderDefaults: Header = {
};
export default defineComponent({
emits: [
'click:row',
'update:sort',
'update:items',
'item-selected',
'update:modelValue',
'manual-sort',
'update:headers',
],
components: {
TableHeader,
TableRow,
draggable,
},
model: {
prop: 'selection',
event: 'select',
Draggable,
},
props: {
headers: {
@@ -148,7 +154,7 @@ export default defineComponent({
type: String,
default: null,
},
selection: {
modelValue: {
type: Array as PropType<any>,
default: () => [],
},
@@ -162,11 +168,11 @@ export default defineComponent({
},
loadingText: {
type: String,
default: i18n.t('loading'),
default: i18n.global.t('loading'),
},
noItemsText: {
type: String,
default: i18n.t('no_items'),
default: i18n.global.t('no_items'),
},
serverSort: {
type: Boolean,
@@ -188,9 +194,13 @@ export default defineComponent({
type: Boolean,
default: false,
},
clickable: {
type: Boolean,
default: true,
},
},
setup(props, { emit, listeners, slots }) {
const _headers = computed({
setup(props, { emit, slots }) {
const internalHeaders = computed({
get: () => {
return props.headers
.map((header: HeaderRaw) => ({
@@ -229,23 +239,23 @@ export default defineComponent({
// In case the sort prop isn't used, we'll use this local sort state as a fallback.
// This allows the table to allow inline sorting on column ootb without the need for
const _localSort = ref<Sort>({
const internalLocalSort = ref<Sort>({
by: null,
desc: false,
});
const _sort = computed({
get: () => props.sort || _localSort.value,
const internalSort = computed({
get: () => props.sort || internalLocalSort.value,
set: (newSort: Sort) => {
emit('update:sort', newSort);
_localSort.value = newSort;
internalLocalSort.value = newSort;
},
});
const hasItemAppendSlot = computed(() => slots['item-append'] !== undefined);
const fullColSpan = computed<string>(() => {
let length = _headers.value.length + 1; // +1 account for spacer
let length = internalHeaders.value.length + 1; // +1 account for spacer
if (props.showSelect) length++;
if (props.showManualSort) length++;
if (hasItemAppendSlot.value) length++;
@@ -253,35 +263,33 @@ export default defineComponent({
return `1 / span ${length}`;
});
const _items = computed({
const internalItems = computed({
get: () => {
if (props.serverSort === true || _sort.value.by === props.manualSortKey) {
if (props.serverSort === true || internalSort.value.by === props.manualSortKey) {
return props.items;
}
if (_sort.value.by === null) return props.items;
if (internalSort.value.by === null) return props.items;
const itemsSorted = sortBy(props.items, [_sort.value.by]);
if (_sort.value.desc === true) return itemsSorted.reverse();
const itemsSorted = sortBy(props.items, [internalSort.value.by]);
if (internalSort.value.desc === true) return itemsSorted.reverse();
return itemsSorted;
},
set: (value: Record<string, any>) => {
set: (value: Item[]) => {
emit('update:items', value);
},
});
const allItemsSelected = computed<boolean>(() => {
return props.loading === false && props.selection.length === props.items.length;
return props.loading === false && props.modelValue.length === props.items.length;
});
const someItemsSelected = computed<boolean>(() => {
return props.selection.length > 0 && allItemsSelected.value === false;
return props.modelValue.length > 0 && allItemsSelected.value === false;
});
const hasRowClick = computed<boolean>(() => 'click:row' in listeners);
const columnStyle = computed<string>(() => {
let gridTemplateColumns = _headers.value
let gridTemplateColumns = internalHeaders.value
.map((header) => {
return header.width ? `${header.width}px` : '160px';
})
@@ -298,16 +306,15 @@ export default defineComponent({
});
return {
_headers,
_items,
_sort,
internalHeaders,
internalItems,
internalSort,
allItemsSelected,
getSelectedState,
onItemSelected,
onToggleSelectAll,
someItemsSelected,
onSortChange,
hasRowClick,
fullColSpan,
columnStyle,
hasItemAppendSlot,
@@ -319,7 +326,7 @@ export default defineComponent({
emit('item-selected', event);
let selection = clone(props.selection) as any[];
let selection = clone(props.modelValue) as any[];
if (event.value === true) {
if (props.selectionUseKeys) {
@@ -337,13 +344,13 @@ export default defineComponent({
});
}
emit('select', selection);
emit('update:modelValue', selection);
}
function getSelectedState(item: Item) {
const selectedKeys = props.selectionUseKeys
? props.selection
: props.selection.map((item: any) => item[props.itemKey]);
? props.modelValue
: props.modelValue.map((item: any) => item[props.itemKey]);
return selectedKeys.includes(item[props.itemKey]);
}
@@ -353,14 +360,14 @@ export default defineComponent({
if (value === true) {
if (props.selectionUseKeys) {
emit(
'select',
'update:modelValue',
clone(props.items).map((item) => item[props.itemKey])
);
} else {
emit('select', clone(props.items));
emit('update:modelValue', clone(props.items));
}
} else {
emit('select', []);
emit('update:modelValue', []);
}
}
@@ -372,8 +379,8 @@ export default defineComponent({
function onSortChange(event: EndEvent) {
if (props.disabled) return;
const item = _items.value[event.oldIndex][props.itemKey];
const to = _items.value[event.newIndex][props.itemKey];
const item = internalItems.value[event.oldIndex][props.itemKey];
const to = internalItems.value[event.newIndex][props.itemKey];
emit('manual-sort', { item, to });
}
@@ -381,126 +388,122 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-table-height: auto;
--v-table-sticky-offset-top: 0;
--v-table-color: var(--foreground-normal);
--v-table-background-color: var(--background-input);
}
</style>
<style lang="scss" scoped>
.v-table {
position: relative;
height: var(--v-table-height);
overflow-y: auto;
}
table {
min-width: 100%;
border-collapse: collapse;
border-spacing: 0;
table {
min-width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
tbody {
display: contents;
}
table tbody {
display: contents;
}
::v-deep {
thead {
display: contents;
}
table :deep(thead) {
display: contents;
}
tr,
.loading-indicator {
display: grid;
grid-template-columns: var(--grid-columns);
}
table :deep(td),
table :deep(th) {
color: var(--v-table-color);
}
td,
th {
color: var(--v-table-color);
table :deep(tr),
table :deep(.loading-indicator) {
display: grid;
grid-template-columns: var(--grid-columns);
}
&.align-left {
text-align: left;
}
table :deep(td.align-left),
table :deep(th.align-left) {
text-align: left;
}
&.align-center {
text-align: center;
}
table :deep(td.align-center),
table :deep(th.align-center) {
text-align: center;
}
&.align-right {
text-align: right;
}
}
table :deep(td.align-right),
table :deep(th.align-right) {
text-align: right;
}
.loading-indicator {
position: relative;
z-index: 3;
table :deep(.loading-indicator) {
position: relative;
z-index: 3;
}
> th {
margin-right: var(--content-padding);
}
}
table :deep(.loading-indicator > th) {
margin-right: var(--content-padding);
}
.sortable-ghost {
.cell {
background-color: var(--background-subdued);
}
}
}
}
table :deep(.sortable-ghost .cell) {
background-color: var(--background-subdued);
}
&.loading {
table {
pointer-events: none;
}
.loading table {
pointer-events: none;
}
.loading-indicator {
height: auto;
padding: 0;
border: none;
.loading .loading-indicator {
height: auto;
padding: 0;
border: none;
}
.v-progress-linear {
--v-progress-linear-height: 2px;
--v-progress-linear-color: var(--border-normal-alt);
.loading .loading-indicator .v-progress-linear {
--v-progress-linear-height: 2px;
--v-progress-linear-color: var(--border-normal-alt);
position: absolute;
top: -2px;
left: 0;
width: 100%;
}
position: absolute;
top: -2px;
left: 0;
width: 100%;
}
th {
padding: 0;
}
.loading .loading-indicator th {
padding: 0;
}
&.sticky th {
position: sticky;
top: 48px;
z-index: 2;
}
}
}
.loading .loading-indicator.sticky th {
position: sticky;
top: 48px;
z-index: 2;
}
.loading-text,
.no-items-text {
text-align: center;
background-color: var(--background-input);
.loading-text,
.no-items-text {
text-align: center;
background-color: var(--background-input);
}
td {
padding: 16px;
color: var(--foreground-subdued);
}
}
.loading-text td,
.no-items-text td {
padding: 16px;
color: var(--foreground-subdued);
}
&.inline {
border: 2px solid var(--border-normal);
border-radius: var(--border-radius);
.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 table :deep(.table-row:last-of-type .cell) {
border-bottom: none;
}
.disabled {

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({

View File

@@ -1,5 +1,5 @@
<template>
<v-list-item v-if="vertical" class="v-tab vertical" :active="active" :disabled="disabled" @click="onClick">
<v-list-item v-if="vertical" class="v-tab vertical" :active="active" :disabled="disabled" clickable @click="onClick">
<slot v-bind="{ active, toggle }" />
</v-list-item>
<div v-else class="v-tab horizontal" :class="{ active, disabled }" @click="onClick">
@@ -8,7 +8,7 @@
</template>
<script lang="ts">
import { defineComponent, inject, ref } from '@vue/composition-api';
import { defineComponent, inject, ref } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({

View File

@@ -1,22 +1,23 @@
<template>
<v-item-group class="v-tabs-items" :value="value" @input="update">
<v-item-group class="v-tabs-items" :model-value="modelValue" @update:model-value="update">
<slot />
</v-item-group>
</template>
<script lang="ts">
import { defineComponent, PropType } from '@vue/composition-api';
import { defineComponent, PropType } from 'vue';
export default defineComponent({
emits: ['update:modelValue'],
props: {
value: {
modelValue: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
},
setup(props, { emit }) {
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
emit('update:modelValue', newSelection);
}
return { update };

View File

@@ -8,22 +8,23 @@
</template>
<script lang="ts">
import { defineComponent, PropType, toRefs, provide, ref } from '@vue/composition-api';
import { defineComponent, PropType, toRefs, provide, ref } from 'vue';
import { useGroupableParent } from '@/composables/groupable';
export default defineComponent({
emits: ['update:modelValue'],
props: {
vertical: {
type: Boolean,
default: false,
},
value: {
modelValue: {
type: Array as PropType<(string | number)[]>,
default: undefined,
},
},
setup(props, { emit }) {
const { value: selection, vertical } = toRefs(props);
const { modelValue: selection, vertical } = toRefs(props);
provide('v-tabs-vertical', vertical);
@@ -40,7 +41,7 @@ export default defineComponent({
);
function update(newSelection: readonly (string | number)[]) {
emit('input', newSelection);
emit('update:modelValue', newSelection);
}
return { update, items };
@@ -48,27 +49,25 @@ export default defineComponent({
});
</script>
<style>
body {
<style scoped>
:global(body) {
--v-tabs-underline-color: var(--foreground-normal);
}
</style>
<style lang="scss" scoped>
.v-tabs.horizontal {
position: relative;
display: inline-flex;
}
::v-deep .v-tab {
display: flex;
flex-basis: 0px;
flex-grow: 1;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 38px;
padding: 8px 20px;
cursor: pointer;
}
.v-tabs.horizontal :slotted(.v-tab) {
display: flex;
flex-basis: 0px;
flex-grow: 1;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 38px;
padding: 8px 20px;
cursor: pointer;
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
<script lang="ts">
import { defineComponent, ref, watch } from '@vue/composition-api';
import { defineComponent, ref, watch } from 'vue';
import { useElementSize } from '@/composables/use-element-size';
export default defineComponent({

View File

@@ -8,23 +8,24 @@
'has-content': hasContent,
}"
>
<div class="prepend" v-if="$scopedSlots.prepend"><slot name="prepend" /></div>
<div class="prepend" v-if="$slots.prepend"><slot name="prepend" /></div>
<textarea
v-bind="$attrs"
v-focus="autofocus"
v-on="_listeners"
v-on="listeners"
:placeholder="placeholder"
:disabled="disabled"
:value="value"
:value="modelValue"
/>
<div class="append" v-if="$scopedSlots.append"><slot name="append" /></div>
<div class="append" v-if="$slots.append"><slot name="append" /></div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import { defineComponent, computed } from 'vue';
export default defineComponent({
emits: ['update:modelValue'],
props: {
disabled: {
type: Boolean,
@@ -38,7 +39,7 @@ export default defineComponent({
type: Boolean,
default: true,
},
value: {
modelValue: {
type: String,
default: null,
},
@@ -59,30 +60,29 @@ export default defineComponent({
default: false,
},
},
setup(props, { emit, listeners }) {
const _listeners = computed(() => ({
...listeners,
setup(props, { emit }) {
const listeners = computed(() => ({
input: emitValue,
blur: trimIfEnabled,
}));
const hasContent = computed(() => props.value && props.value.length > 0);
const hasContent = computed(() => props.modelValue && props.modelValue.length > 0);
return { _listeners, hasContent };
return { listeners, hasContent };
function emitValue(event: InputEvent) {
const value = (event.target as HTMLInputElement).value;
if (props.nullable === true && value === '') {
emit('input', null);
emit('update:modelValue', null);
} else {
emit('input', value);
emit('update:modelValue', value);
}
}
function trimIfEnabled() {
if (props.value && props.trim) {
emit('input', props.value.trim());
if (props.modelValue && props.trim) {
emit('update:modelValue', props.modelValue.trim());
}
}
},

View File

@@ -9,8 +9,8 @@
@drop.stop.prevent="onDrop"
>
<template v-if="dragging">
<p class="type-label">{{ $t('drop_to_upload') }}</p>
<p class="type-text">{{ $t('upload_pending') }}</p>
<p class="type-label">{{ t('drop_to_upload') }}</p>
<p class="type-text">{{ t('upload_pending') }}</p>
</template>
<template v-else-if="uploading">
@@ -18,35 +18,35 @@
<p class="type-text">
{{
multiple && numberOfFiles > 1
? $t('upload_files_indeterminate', { done: done, total: numberOfFiles })
: $t('upload_file_indeterminate')
? t('upload_files_indeterminate', { done: done, total: numberOfFiles })
: t('upload_file_indeterminate')
}}
</p>
<v-progress-linear :value="progress" rounded />
</template>
<template v-else>
<p class="type-label">{{ $t('drag_file_here') }}</p>
<p class="type-text">{{ $t('click_to_browse') }}</p>
<p class="type-label">{{ t('drag_file_here') }}</p>
<p class="type-text">{{ t('click_to_browse') }}</p>
<input class="browse" type="file" @input="onBrowseSelect" :multiple="multiple" />
<template v-if="fromUrl !== false || fromLibrary !== false">
<v-menu showArrow placement="bottom-end">
<template #activator="{ toggle }">
<v-icon @click="toggle" class="options" name="more_vert" />
<v-icon clickable @click="toggle" class="options" name="more_vert" />
</template>
<v-list>
<v-list-item @click="activeDialog = 'choose'" v-if="fromLibrary">
<v-list-item clickable @click="activeDialog = 'choose'" v-if="fromLibrary">
<v-list-item-icon><v-icon name="folder_open" /></v-list-item-icon>
<v-list-item-content>
{{ $t('choose_from_library') }}
{{ t('choose_from_library') }}
</v-list-item-content>
</v-list-item>
<v-list-item @click="activeDialog = 'url'" v-if="fromUrl">
<v-list-item clickable @click="activeDialog = 'url'" v-if="fromUrl">
<v-list-item-icon><v-icon name="link" /></v-list-item-icon>
<v-list-item-content>
{{ $t('import_from_url') }}
{{ t('import_from_url') }}
</v-list-item-content>
</v-list-item>
</v-list>
@@ -60,22 +60,22 @@
/>
<v-dialog
:active="activeDialog === 'url'"
:model-value="activeDialog === 'url'"
@esc="activeDialog = null"
@toggle="activeDialog = null"
@update:model-value="activeDialog = null"
:persistent="urlLoading"
>
<v-card>
<v-card-title>{{ $t('import_from_url') }}</v-card-title>
<v-card-title>{{ t('import_from_url') }}</v-card-title>
<v-card-text>
<v-input :placeholder="$t('url')" v-model="url" :nullable="false" :disabled="urlLoading" />
<v-input :placeholder="t('url')" v-model="url" :nullable="false" :disabled="urlLoading" />
</v-card-text>
<v-card-actions>
<v-button :disabled="urlLoading" @click="activeDialog = null" secondary>
{{ $t('cancel') }}
{{ t('cancel') }}
</v-button>
<v-button :loading="urlLoading" @click="importFromURL" :disabled="isValidURL === false">
{{ $t('import') }}
{{ t('import') }}
</v-button>
</v-card-actions>
</v-card>
@@ -86,7 +86,8 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, computed } from 'vue';
import uploadFiles from '@/utils/upload-files';
import uploadFile from '@/utils/upload-file';
import DrawerCollection from '@/views/private/components/drawer-collection';
@@ -94,6 +95,7 @@ import api from '@/api';
import { unexpectedError } from '@/utils/unexpected-error';
export default defineComponent({
emits: ['input'],
components: { DrawerCollection },
props: {
multiple: {
@@ -118,6 +120,8 @@ export default defineComponent({
},
},
setup(props, { emit }) {
const { t } = useI18n();
const { uploading, progress, upload, onBrowseSelect, done, numberOfFiles } = useUpload();
const { onDragEnter, onDragLeave, onDrop, dragging } = useDragging();
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
@@ -125,6 +129,7 @@ export default defineComponent({
const activeDialog = ref<'choose' | 'url' | null>(null);
return {
t,
uploading,
progress,
onDragEnter,

View File

@@ -1,7 +1,6 @@
import { isEmpty, notEmpty } from '@/utils/is-empty/';
import { computed, inject, onBeforeUnmount, provide, ref, Ref, watch } from '@vue/composition-api';
import { isEqual } from 'lodash';
import Vue from 'vue';
import { computed, inject, nextTick, onBeforeUnmount, provide, ref, shallowRef, Ref, watch } from 'vue';
type GroupableInstance = {
active: Ref<boolean>;
@@ -19,7 +18,14 @@ type GroupableOptions = {
watch?: boolean;
};
export function useGroupable(options?: GroupableOptions): Record<string, any> {
type UsableGroupable = {
active: Ref<boolean>;
toggle: () => void;
activate: () => void;
deactivate: () => void;
};
export function useGroupable(options?: GroupableOptions): UsableGroupable {
// Injects the registration / toggle functions from the parent scope
const parentFunctions = inject(options?.group || 'item-group', null);
@@ -96,6 +102,14 @@ type GroupableParentOptions = {
multiple?: Ref<boolean>;
};
type UsableGroupableParent = {
items: Ref<GroupableInstance[]>;
selection: Ref<readonly (string | number)[]>;
internalSelection: Ref<(string | number)[]>;
getValueForItem: (item: GroupableInstance) => string | number;
updateChildren: () => void;
};
/**
* Used to make a component a group parent component. Provides the registration / toggle functions
* to its group children
@@ -104,13 +118,13 @@ export function useGroupableParent(
state: GroupableParentState = {},
options: GroupableParentOptions = {},
group = 'item-group'
): Record<string, any> {
): UsableGroupableParent {
// References to the active state and value of the individual child items
const items = ref<GroupableInstance[]>([]);
const items = shallowRef<GroupableInstance[]>([]);
// Internal copy of the selection. This allows the composition to work without the state option
// being passed
const _selection = ref<(number | string)[]>([]);
const internalSelection = ref<(number | string)[]>([]);
// Uses either the internal state, or the passed in state. Will call the onSelectionChange
// handler if it's passed
@@ -120,14 +134,14 @@ export function useGroupableParent(
return state.selection.value;
}
return _selection.value;
return internalSelection.value;
},
set(newSelection) {
if (notEmpty(state.onSelectionChange)) {
state.onSelectionChange(newSelection);
}
_selection.value = [...newSelection];
internalSelection.value = [...newSelection];
},
});
@@ -141,7 +155,7 @@ export function useGroupableParent(
// It takes a tick before all children are rendered, this will make sure the start state of the
// children matches the start selection
Vue.nextTick().then(updateChildren);
nextTick().then(updateChildren);
watch(
() => options?.mandatory?.value,
@@ -157,7 +171,7 @@ export function useGroupableParent(
// These aren't exported with any particular use in mind. It's mostly for testing purposes.
// Treat them as readonly.
return { items, selection, _selection, getValueForItem, updateChildren };
return { items, selection, internalSelection, getValueForItem, updateChildren };
// Register a child within the context of this group
function register(item: GroupableInstance) {

View File

@@ -19,7 +19,7 @@ The `options` parameter can be used to set some behavioral options for the selec
`multiple: boolean`, `max: number`, `mandatory: boolean`.
```js
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useGroupableParent } from '@/composables/size-class/';
export default defineComponent({
@@ -54,7 +54,7 @@ groupable parent. The composition returns an object with the active state, and a
component's active state in the parent's selection.
```js
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useGroupable } from '@/composables/groupable';
export default defineComponent({

View File

@@ -18,7 +18,7 @@ A component that uses this composition and the corresponding props will accept t
## Usage
```js
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import useSizeClass, { sizeProps } from '@/composables/size-class/';
export default defineComponent({

View File

@@ -1,4 +1,4 @@
import { computed, ComputedRef } from '@vue/composition-api';
import { computed, ComputedRef } from 'vue';
export const sizeProps = {
xSmall: {

View File

@@ -1,4 +1,4 @@
import { onBeforeMount, onBeforeUnmount, Ref } from '@vue/composition-api';
import { onBeforeMount, onBeforeUnmount, Ref } from 'vue';
export default function unsavedChanges(isSavable: Ref<boolean>): void {
onBeforeMount(() => {

View File

@@ -1,18 +1,30 @@
import { useCollectionsStore, useFieldsStore } from '@/stores/';
import { Field } from '@/types';
import { computed, ComputedRef, Ref, ref } from '@vue/composition-api';
import { Collection, Field } from '@/types';
import { computed, ref, Ref, ComputedRef } from 'vue';
export function useCollection(collectionKey: string | Ref<string | null>): Record<string, ComputedRef> {
type UsableCollection = {
info: ComputedRef<Collection | null>;
fields: ComputedRef<Field[]>;
defaults: Record<string, any>;
primaryKeyField: ComputedRef<Field | null>;
userCreatedField: ComputedRef<Field | null>;
sortField: ComputedRef<string | null>;
isSingleton: ComputedRef<boolean>;
accountabilityScope: ComputedRef<'all' | 'activity' | null>;
};
export function useCollection(collectionKey: string | Ref<string | null>): UsableCollection {
const collectionsStore = useCollectionsStore();
const fieldsStore = useFieldsStore();
const collection: Ref<string | null> = typeof collectionKey === 'string' ? ref(collectionKey) : collectionKey;
const info = computed(() => {
return collectionsStore.state.collections.find(({ collection: key }) => key === collection.value);
return collectionsStore.collections.find(({ collection: key }) => key === collection.value) || null;
});
const fields = computed<Field[]>(() => {
const fields = computed(() => {
if (!collection.value) return [];
return fieldsStore.getFieldsForCollection(collection.value);
});
@@ -31,10 +43,10 @@ export function useCollection(collectionKey: string | Ref<string | null>): Recor
});
const primaryKeyField = computed(() => {
// Every collection has a primary key; rules of the land
return fields.value.find(
(field) => field.collection === collection.value && field.schema?.is_primary_key === true
)!;
return (
fields.value.find((field) => field.collection === collection.value && field.schema?.is_primary_key === true) ||
null
);
});
const userCreatedField = computed(() => {

View File

@@ -1,13 +1,16 @@
import { computed, ComputedRef, Ref, ref, watch } from '@vue/composition-api';
import { nanoid } from 'nanoid';
import { computed, ComputedRef, Ref, ref, watch } from 'vue';
type EmitFunction = (event: string, ...args: any[]) => void;
type UsableCustomSelection = {
otherValue: Ref<string | null>;
usesOtherValue: ComputedRef<boolean>;
};
export function useCustomSelection(
currentValue: Ref<string>,
items: Ref<any[]>,
emit: EmitFunction
): Record<string, ComputedRef> {
emit: (event: string | null) => void
): UsableCustomSelection {
const localOtherValue = ref('');
const otherValue = computed({
@@ -17,10 +20,10 @@ export function useCustomSelection(
set(newValue: string | null) {
if (newValue === null) {
localOtherValue.value = '';
emit('input', null);
emit(null);
} else {
localOtherValue.value = newValue;
emit('input', newValue);
emit(newValue);
}
},
});
@@ -38,16 +41,22 @@ export function useCustomSelection(
return { otherValue, usesOtherValue };
}
type OtherValue = {
key: string;
value: string;
};
type UsableCustomSelectionMultiple = {
otherValues: Ref<OtherValue[]>;
addOtherValue: (value?: string) => void;
setOtherValue: (key: string, newValue: string | null) => void;
};
export function useCustomSelectionMultiple(
currentValues: Ref<string[]>,
items: Ref<any[]>,
emit: EmitFunction
): Record<string, any> {
type OtherValue = {
key: string;
value: string;
};
emit: (event: string[] | null) => void
): UsableCustomSelectionMultiple {
const otherValues = ref<OtherValue[]>([]);
watch(currentValues, (newValue) => {
@@ -94,9 +103,9 @@ export function useCustomSelectionMultiple(
otherValues.value = otherValues.value.filter((o) => o.key !== key);
if (valueWithoutPrevious.length === 0) {
emit('input', null);
emit(null);
} else {
emit('input', valueWithoutPrevious);
emit(valueWithoutPrevious);
}
} else {
otherValues.value = otherValues.value.map((otherValue) => {
@@ -106,7 +115,7 @@ export function useCustomSelectionMultiple(
const newEmitValue = [...valueWithoutPrevious, newValue];
emit('input', newEmitValue);
emit(newEmitValue);
}
}
}

View File

@@ -0,0 +1,37 @@
import { ref, provide, inject, onMounted, Ref, InjectionKey } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
const dialogRouteSymbol: InjectionKey<() => void> = Symbol();
export function useDialogRoute(): Ref<boolean> {
const isOpen = ref(false);
let resolveGuard: () => void;
const leaveGuard: Promise<void> = new Promise((resolve) => {
resolveGuard = resolve;
});
onMounted(() => {
isOpen.value = true;
});
onBeforeRouteLeave(() => {
isOpen.value = false;
return leaveGuard;
});
provide(dialogRouteSymbol, leave);
return isOpen;
function leave() {
resolveGuard();
}
}
export function useDialogRouteLeave(): (() => void) | undefined {
const leave = inject(dialogRouteSymbol, undefined);
return leave;
}

View File

@@ -14,7 +14,7 @@ Allows you to reactively watch an elements width and height.
</template>
<script lang="ts">
import { defineComponent } from '@vue/composition-api';
import { defineComponent } from 'vue';
import { useElementSize } from '@/composables/use-element-size';
export default defineComponent({

View File

@@ -1,6 +1,5 @@
import { notEmpty } from '@/utils/is-empty';
import { isRef, onMounted, onUnmounted, Ref, ref } from '@vue/composition-api';
import { ResizeObserver as ResizeObserverPolyfill } from 'resize-observer';
import { isRef, onMounted, onUnmounted, Ref, ref } from 'vue';
declare global {
interface Window {
@@ -10,17 +9,14 @@ declare global {
export default function useElementSize<T extends Element>(
target: T | Ref<T> | Ref<undefined>
): Record<string, Ref<number>> {
): {
width: Ref<number>;
height: Ref<number>;
} {
const width = ref(0);
const height = ref(0);
let RO = ResizeObserverPolyfill;
if ('ResizeObserver' in window) {
RO = window.ResizeObserver;
}
const resizeObserver = new RO(([entry]) => {
const resizeObserver = new ResizeObserver(([entry]) => {
width.value = entry.contentRect.width;
height.value = entry.contentRect.height;
});

Some files were not shown because too many files have changed in this diff Show More