script[setup]: Remaining private components (#18458)

* script[setup]: Remaining private components

* Remove redundant default

* Remove redundant default

* Remove redudant default

* Remove redundant default
This commit is contained in:
Rijk van Zanten
2023-05-03 12:31:55 -04:00
committed by GitHub
parent 04a3effe6b
commit 60886ff42c
20 changed files with 1231 additions and 1450 deletions

View File

@@ -15,58 +15,44 @@
</sidebar-detail>
</template>
<script lang="ts">
<script lang="ts" setup>
import { computed, ref, unref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { computed, defineComponent, watch, ref } from 'vue';
export default defineComponent({
props: {
collection: {
type: String,
default: null,
},
archive: {
type: String,
default: null,
},
const props = defineProps<{
collection?: string;
archive?: string;
}>();
const { t } = useI18n();
const router = useRouter();
const selectedItem = ref<string | undefined>(props.archive);
const items = [
{
text: t('show_active_items'),
value: null,
},
setup(props) {
const { t } = useI18n();
{ text: t('show_archived_items'), value: 'archived' },
{ text: t('show_all_items'), value: 'all' },
];
const router = useRouter();
const active = computed(() => !!unref(selectedItem));
const selectedItem = ref<string | null>(props.archive);
watch(selectedItem, () => {
const url = new URL(unref(router.currentRoute).fullPath, window.location.origin);
const items = [
{
text: t('show_active_items'),
value: null,
},
{ text: t('show_archived_items'), value: 'archived' },
{ text: t('show_all_items'), value: 'all' },
];
url.searchParams.delete('archived');
url.searchParams.delete('all');
const active = computed(() => selectedItem.value !== null);
if (unref(selectedItem)) {
url.searchParams.set(selectedItem.value!, '');
}
watch(
() => selectedItem.value,
() => {
const url = new URL(router.currentRoute.value.fullPath, window.location.origin);
url.searchParams.delete('archived');
url.searchParams.delete('all');
if (selectedItem.value !== null) {
url.searchParams.set(selectedItem.value, '');
}
router.push(url.pathname + url.search);
}
);
return { t, active, selectedItem, items };
},
router.push(url.pathname + url.search);
});
</script>

View File

@@ -35,49 +35,42 @@
</v-dialog>
</template>
<script lang="ts">
<script lang="ts" setup>
import { reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { defineComponent, reactive } from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
saving: {
type: Boolean,
default: false,
},
},
emits: ['save', 'update:modelValue'],
setup(props, { emit }) {
const { t } = useI18n();
defineProps<{
modelValue?: boolean;
saving?: boolean;
}>();
const bookmarkValue = reactive({
name: null,
icon: 'bookmark',
color: null,
});
const emit = defineEmits<{
(e: 'save', value: { name: string | null; icon: string | null; color: string | null }): void;
(e: 'update:modelValue', value: boolean): void;
}>();
return { t, bookmarkValue, setIcon, setColor, cancel };
const { t } = useI18n();
function setIcon(icon: any) {
bookmarkValue.icon = icon;
}
function setColor(color: any) {
bookmarkValue.color = color;
}
function cancel() {
bookmarkValue.name = null;
bookmarkValue.icon = 'bookmark';
bookmarkValue.color = null;
emit('update:modelValue', false);
}
},
const bookmarkValue = reactive({
name: null,
icon: 'bookmark',
color: null,
});
function setIcon(icon: any) {
bookmarkValue.icon = icon;
}
function setColor(color: any) {
bookmarkValue.color = color;
}
function cancel() {
bookmarkValue.name = null;
bookmarkValue.icon = 'bookmark';
bookmarkValue.color = null;
emit('update:modelValue', false);
}
</script>
<style lang="scss" scoped>

View File

@@ -23,129 +23,116 @@
</v-drawer>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, computed, PropType, toRefs } from 'vue';
<script lang="ts" setup>
import api from '@/api';
import { VALIDATION_TYPES } from '@/constants';
import { APIError } from '@/types/error';
import { unexpectedError } from '@/utils/unexpected-error';
import { getEndpoint } from '@directus/utils';
import { computed, ref, toRefs } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
props: {
active: {
type: Boolean,
default: false,
},
collection: {
type: String,
required: true,
},
edits: {
type: Object as PropType<Record<string, any>>,
default: undefined,
},
primaryKeys: {
type: Array as PropType<(number | string)[]>,
required: true,
},
},
emits: ['update:active', 'refresh'],
setup(props, { emit }) {
const { t } = useI18n();
const props = defineProps<{
collection: string;
primaryKeys: (number | string)[];
active?: boolean;
edits?: Record<string, any>;
}>();
const { internalEdits } = useEdits();
const { internalActive } = useActiveState();
const { save, cancel, saving, validationErrors } = useActions();
const emit = defineEmits<{
(e: 'update:active', value: boolean): void;
(e: 'refresh'): void;
}>();
const { collection } = toRefs(props);
const { t } = useI18n();
return { t, internalActive, internalEdits, save, saving, cancel, validationErrors };
const { internalEdits } = useEdits();
const { internalActive } = useActiveState();
const { save, cancel, saving, validationErrors } = useActions();
function useEdits() {
const localEdits = ref<Record<string, any>>({});
const { collection } = toRefs(props);
const internalEdits = computed<Record<string, any>>({
get() {
if (props.edits !== undefined) {
return {
...props.edits,
...localEdits.value,
};
}
function useEdits() {
const localEdits = ref<Record<string, any>>({});
return localEdits.value;
},
set(newEdits) {
localEdits.value = newEdits;
},
});
return { internalEdits };
}
function useActiveState() {
const localActive = ref(false);
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('update:active', newActive);
},
});
return { internalActive };
}
function useActions() {
const saving = ref(false);
const validationErrors = ref([]);
return { save, cancel, saving, validationErrors };
async function save() {
saving.value = true;
try {
await api.patch(getEndpoint(collection.value), {
keys: props.primaryKeys,
data: internalEdits.value,
});
emit('refresh');
internalActive.value = false;
internalEdits.value = {};
} catch (err: any) {
validationErrors.value = err.response.data.errors
.filter((err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code))
.map((err: APIError) => {
return err.extensions;
});
const otherErrors = err.response.data.errors.filter(
(err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code) === false
);
if (otherErrors.length > 0) {
otherErrors.forEach(unexpectedError);
}
} finally {
saving.value = false;
}
const internalEdits = computed<Record<string, any>>({
get() {
if (props.edits !== undefined) {
return {
...props.edits,
...localEdits.value,
};
}
function cancel() {
internalActive.value = false;
internalEdits.value = {};
return localEdits.value;
},
set(newEdits) {
localEdits.value = newEdits;
},
});
return { internalEdits };
}
function useActiveState() {
const localActive = ref(false);
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('update:active', newActive);
},
});
return { internalActive };
}
function useActions() {
const saving = ref(false);
const validationErrors = ref([]);
return { save, cancel, saving, validationErrors };
async function save() {
saving.value = true;
try {
await api.patch(getEndpoint(collection.value), {
keys: props.primaryKeys,
data: internalEdits.value,
});
emit('refresh');
internalActive.value = false;
internalEdits.value = {};
} catch (err: any) {
validationErrors.value = err.response.data.errors
.filter((err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code))
.map((err: APIError) => {
return err.extensions;
});
const otherErrors = err.response.data.errors.filter(
(err: APIError) => VALIDATION_TYPES.includes(err?.extensions?.code) === false
);
if (otherErrors.length > 0) {
otherErrors.forEach(unexpectedError);
}
} finally {
saving.value = false;
}
},
});
}
function cancel() {
internalActive.value = false;
internalEdits.value = {};
}
}
</script>
<style lang="scss" scoped>

View File

@@ -53,171 +53,144 @@
</component>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, PropType, ref, computed, toRefs, watch } from 'vue';
import { Filter } from '@directus/types';
import { usePreset } from '@/composables/use-preset';
import { useCollection, useLayout } from '@directus/composables';
import SearchInput from '@/views/private/components/search-input.vue';
<script lang="ts" setup>
import { useExtension } from '@/composables/use-extension';
import { usePreset } from '@/composables/use-preset';
import SearchInput from '@/views/private/components/search-input.vue';
import { useCollection, useLayout } from '@directus/composables';
import { Filter } from '@directus/types';
import { computed, ref, toRefs, unref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
components: { SearchInput },
props: {
active: {
type: Boolean,
default: false,
},
selection: {
type: Array as PropType<(number | string)[]>,
default: () => [],
},
collection: {
type: String,
default: null,
},
multiple: {
type: Boolean,
default: false,
},
filter: {
type: Object as PropType<Filter>,
default: null,
},
const props = withDefaults(
defineProps<{
active?: boolean;
selection?: (number | string)[];
collection: string;
multiple?: boolean;
filter?: Filter;
}>(),
{
selection: () => [],
}
);
const emit = defineEmits<{
(e: 'update:active', value: boolean): void;
(e: 'input', value: (number | string)[] | null): void;
}>();
const { t } = useI18n();
const { save, cancel } = useActions();
const { internalActive } = useActiveState();
const { internalSelection, onSelect } = useSelection();
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
const { layout, layoutOptions, layoutQuery, search, filter: presetFilter } = usePreset(collection, ref(null), true);
// This is a local copy of the layout. This means that we can sync it the layout without
// having use-preset auto-save the values
const localLayout = ref(layout.value || 'tabular');
const localOptions = ref(layoutOptions.value);
const localQuery = ref(layoutQuery.value);
const currentLayout = useExtension('layout', localLayout);
const layoutSelection = computed<any>({
get() {
return internalSelection.value;
},
emits: ['update:active', 'input'],
setup(props, { emit }) {
const { t } = useI18n();
const { save, cancel } = useActions();
const { internalActive } = useActiveState();
const { internalSelection, onSelect } = useSelection();
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
const { layout, layoutOptions, layoutQuery, search, filter: presetFilter } = usePreset(collection, ref(null), true);
// This is a local copy of the layout. This means that we can sync it the layout without
// having use-preset auto-save the values
const localLayout = ref(layout.value || 'tabular');
const localOptions = ref(layoutOptions.value);
const localQuery = ref(layoutQuery.value);
const currentLayout = useExtension('layout', localLayout);
const layoutSelection = computed<any>({
get() {
return internalSelection.value;
},
set(newFilters) {
onSelect(newFilters);
},
});
const { layoutWrapper } = useLayout(layout);
const layoutFilter = computed({
get() {
if (!props.filter) return presetFilter.value;
if (!presetFilter.value) return props.filter;
return {
_and: [props.filter, presetFilter.value],
};
},
set(newFilter: Filter | null) {
presetFilter.value = newFilter;
},
});
return {
t,
save,
cancel,
internalActive,
layoutWrapper,
layoutSelection,
layoutFilter,
localLayout,
localOptions,
localQuery,
collectionInfo,
search,
presetFilter,
currentLayout,
};
function useActiveState() {
const localActive = ref(false);
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('update:active', newActive);
},
});
return { internalActive };
}
function useSelection() {
const localSelection = ref<(string | number)[] | null>(null);
const internalSelection = computed({
get() {
if (localSelection.value === null) {
return props.selection;
}
return localSelection.value;
},
set(newSelection: (string | number)[]) {
localSelection.value = newSelection;
},
});
watch(
() => props.active,
() => {
localSelection.value = null;
}
);
return { internalSelection, onSelect };
function onSelect(newSelection: (string | number)[]) {
if (newSelection.length === 0) {
localSelection.value = [];
return;
}
if (props.multiple === true) {
localSelection.value = newSelection;
} else {
localSelection.value = [newSelection[newSelection.length - 1]];
}
}
}
function useActions() {
return { save, cancel };
function save() {
emit('input', internalSelection.value);
internalActive.value = false;
}
function cancel() {
internalActive.value = false;
}
}
set(newFilters) {
onSelect(newFilters);
},
});
const { layoutWrapper } = useLayout(layout);
const layoutFilter = computed({
get() {
if (!props.filter) return presetFilter.value;
if (!presetFilter.value) return props.filter;
return {
_and: [props.filter, presetFilter.value],
};
},
set(newFilter: Filter | null) {
presetFilter.value = newFilter;
},
});
function useActiveState() {
const localActive = ref(false);
const internalActive = computed({
get() {
return props.active === undefined ? localActive.value : props.active;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('update:active', newActive);
},
});
return { internalActive };
}
function useSelection() {
const localSelection = ref<(string | number)[] | null>(null);
const internalSelection = computed({
get() {
if (localSelection.value === null) {
return props.selection;
}
return localSelection.value;
},
set(newSelection: (string | number)[]) {
localSelection.value = newSelection;
},
});
watch(
() => props.active,
() => {
localSelection.value = null;
}
);
return { internalSelection, onSelect };
function onSelect(newSelection: (string | number)[]) {
if (newSelection.length === 0) {
localSelection.value = [];
return;
}
if (props.multiple === true) {
localSelection.value = newSelection;
} else {
localSelection.value = [newSelection[newSelection.length - 1]];
}
}
}
function useActions() {
return { save, cancel };
function save() {
emit('input', unref(internalSelection));
internalActive.value = false;
}
function cancel() {
internalActive.value = false;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -22,22 +22,18 @@
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
<script lang="ts" setup>
import { ref } from 'vue';
export default defineComponent({
props: {
showSidebarToggle: {
type: Boolean,
default: false,
},
},
emits: ['toggle:sidebar'],
setup() {
const active = ref(false);
return { active };
},
});
defineProps<{
showSidebarToggle?: boolean;
}>();
defineEmits<{
(e: 'toggle:sidebar'): void;
}>();
const active = ref(false);
</script>
<style scoped>

View File

@@ -36,57 +36,46 @@
</header>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import HeaderBarActions from './header-bar-actions.vue';
export default defineComponent({
components: { HeaderBarActions },
props: {
title: {
type: String,
default: null,
},
showSidebarToggle: {
type: Boolean,
default: false,
},
primaryActionIcon: {
type: String,
default: 'menu',
},
small: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
withDefaults(
defineProps<{
title?: string;
showSidebarToggle?: boolean;
primaryActionIcon?: string;
small?: boolean;
shadow?: boolean;
}>(),
{
primaryActionIcon: 'menu',
shadow: true,
}
);
defineEmits<{
(e: 'primary'): void;
(e: 'toggle:sidebar'): void;
}>();
const headerEl = ref<Element>();
const collapsed = ref(false);
const observer = new IntersectionObserver(
([e]) => {
collapsed.value = e.boundingClientRect.y === -1;
},
emits: ['primary', 'toggle:sidebar'],
setup() {
const headerEl = ref<Element>();
{ threshold: [1] }
);
const collapsed = ref(false);
onMounted(() => {
observer.observe(headerEl.value as HTMLElement);
});
const observer = new IntersectionObserver(
([e]) => {
collapsed.value = e.boundingClientRect.y === -1;
},
{ threshold: [1] }
);
onMounted(() => {
observer.observe(headerEl.value as HTMLElement);
});
onUnmounted(() => {
observer.disconnect();
});
return { headerEl, collapsed };
},
onUnmounted(() => {
observer.disconnect();
});
</script>

View File

@@ -85,7 +85,7 @@
<v-list-item-icon><v-icon name="crop_square" /></v-list-item-icon>
<v-list-item-content>{{ t('square') }}</v-list-item-content>
</v-list-item>
<v-list-item clickable :active="aspectRatio === NaN" @click="aspectRatio = NaN">
<v-list-item clickable :active="Number.isNaN(aspectRatio)" @click="aspectRatio = NaN">
<v-list-item-icon><v-icon name="crop_free" /></v-list-item-icon>
<v-list-item-content>{{ t('free') }}</v-list-item-content>
</v-list-item>
@@ -123,17 +123,16 @@
</v-drawer>
</template>
<script lang="ts">
<script lang="ts" setup>
import api, { addTokenToURL } from '@/api';
import { computed, defineComponent, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/stores/settings';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import Cropper from 'cropperjs';
import throttle from 'lodash/throttle';
import { nanoid } from 'nanoid/non-secure';
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
type Image = {
type: string;
@@ -143,367 +142,337 @@ type Image = {
height: number;
};
export default defineComponent({
props: {
id: {
type: String,
required: true,
},
modelValue: {
type: Boolean,
default: undefined,
},
const props = defineProps<{
id: string;
modelValue?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
(e: 'refresh'): void;
}>();
const { t, n } = useI18n();
const settingsStore = useSettingsStore();
const localActive = ref(false);
const internalActive = computed({
get() {
return props.modelValue === undefined ? localActive.value : props.modelValue;
},
emits: ['update:modelValue', 'refresh'],
setup(props, { emit }) {
const { t, n } = useI18n();
const settingsStore = useSettingsStore();
const localActive = ref(false);
const internalActive = computed({
get() {
return props.modelValue === undefined ? localActive.value : props.modelValue;
},
set(newActive: boolean) {
localActive.value = newActive;
emit('update:modelValue', newActive);
},
});
const { loading, error, imageData, imageElement, save, saving, fetchImage, onImageLoad } = useImage();
const {
cropperInstance,
initCropper,
flip,
rotate,
reset,
aspectRatio,
aspectRatioIcon,
newDimensions,
dragMode,
cropping,
} = useCropper();
watch(internalActive, (isActive) => {
if (isActive === true) {
fetchImage();
} else {
if (cropperInstance.value) {
cropperInstance.value.destroy();
}
loading.value = false;
error.value = null;
imageData.value = null;
}
});
const randomId = ref<string>(nanoid());
const imageURL = computed(() => {
return addTokenToURL(`${getRootPath()}assets/${props.id}?${randomId.value}`);
});
const dimensionsString = computed(() => {
let output = '';
const isSVG = imageData.value?.type === 'image/svg+xml';
if (imageData.value) {
if (isSVG) {
output += 'SVG';
} else {
output += `${n(imageData.value.width ?? 0)}x${n(imageData.value.height ?? 0)}`;
}
if (imageData.value.width !== newDimensions.width || imageData.value.height !== newDimensions.height) {
if (isSVG) {
if (newDimensions.width || newDimensions.height) {
output += ` -> PNG ${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
} else {
output += ' -> PNG';
}
} else {
output += ` -> ${isSVG ? 'PNG ' : ''}${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
}
}
}
return output;
});
const customAspectRatios = settingsStore.settings?.custom_aspect_ratios ?? null;
return {
t,
n,
internalActive,
loading,
error,
imageData,
imageElement,
save,
onImageLoad,
flip,
rotate,
reset,
aspectRatio,
aspectRatioIcon,
saving,
imageURL,
newDimensions,
dragMode,
cropping,
setAspectRatio,
dimensionsString,
customAspectRatios,
};
function useImage() {
const loading = ref(false);
const error = ref(null);
const imageData = ref<Image | null>(null);
const saving = ref(false);
const imageElement = ref<HTMLImageElement | null>(null);
return {
loading,
error,
imageData,
saving,
fetchImage,
imageElement,
save,
onImageLoad,
};
async function fetchImage() {
try {
loading.value = true;
const response = await api.get(`/files/${props.id}`, {
params: {
fields: ['type', 'filesize', 'filename_download', 'width', 'height'],
},
});
imageData.value = response.data.data;
} catch (err: any) {
error.value = err;
} finally {
loading.value = false;
}
}
function save() {
saving.value = true;
cropperInstance.value
?.getCroppedCanvas({
imageSmoothingQuality: 'high',
})
.toBlob(async (blob) => {
if (blob === null) {
saving.value = false;
return;
}
const formData = new FormData();
formData.append('file', blob, imageData.value?.filename_download);
try {
await api.patch(`/files/${props.id}`, formData);
emit('refresh');
internalActive.value = false;
randomId.value = nanoid();
} catch (err: any) {
unexpectedError(err);
} finally {
saving.value = false;
}
}, imageData.value?.type);
}
async function onImageLoad() {
await nextTick();
initCropper();
}
}
function useCropper() {
const cropperInstance = ref<Cropper | null>(null);
const localAspectRatio = ref(NaN);
const newDimensions = reactive({
width: null as null | number,
height: null as null | number,
});
watch(imageData, () => {
if (!imageData.value) return;
localAspectRatio.value = imageData.value.width / imageData.value.height;
newDimensions.width = imageData.value.width;
newDimensions.height = imageData.value.height;
});
const aspectRatio = computed<number>({
get() {
return localAspectRatio.value;
},
set(newAspectRatio) {
localAspectRatio.value = newAspectRatio;
cropperInstance.value?.setAspectRatio(newAspectRatio);
cropperInstance.value?.crop();
dragMode.value = 'crop';
},
});
const aspectRatioIcon = computed(() => {
if (!imageData.value) return 'crop_original';
if (customAspectRatios) {
const customAspectRatio = customAspectRatios.find((customAR) => customAR.value == aspectRatio.value);
if (customAspectRatio) return 'crop_square';
}
switch (aspectRatio.value) {
case 16 / 9:
return 'crop_16_9';
case 3 / 2:
return 'crop_3_2';
case 5 / 4:
return 'crop_5_4';
case 7 / 5:
return 'crop_7_5';
case 1 / 1:
return 'crop_square';
case imageData.value.width / imageData.value.height:
return 'crop_original';
default:
return 'crop_free';
}
});
const localDragMode = ref<'move' | 'crop'>('move');
const dragMode = computed({
get() {
return localDragMode.value;
},
set(newMode: 'move' | 'crop') {
cropperInstance.value?.setDragMode(newMode);
localDragMode.value = newMode;
if (newMode === 'move') {
cropperInstance.value?.clear();
localCropping.value = false;
}
},
});
const localCropping = ref(false);
const cropping = computed({
get() {
return localCropping.value;
},
set(newCropping: boolean) {
if (newCropping === false) {
cropperInstance.value?.clear();
}
localCropping.value = newCropping;
},
});
return {
cropperInstance,
initCropper,
flip,
rotate,
reset,
aspectRatio,
aspectRatioIcon,
newDimensions,
dragMode,
cropping,
};
function initCropper() {
if (imageElement.value === null) return;
if (cropperInstance.value) {
cropperInstance.value.destroy();
}
localCropping.value = false;
cropperInstance.value = new Cropper(imageElement.value, {
autoCrop: false,
autoCropArea: 0.5,
toggleDragModeOnDblclick: false,
dragMode: 'move',
viewMode: 1,
crop: throttle((event) => {
if (!imageData.value) return;
if (cropping.value === false && (event.detail.width || event.detail.height)) {
cropping.value = true;
}
const newWidth = event.detail.width || imageData.value.width;
const newHeight = event.detail.height || imageData.value.height;
if (event.detail.rotate === 0 || event.detail.rotate === -180) {
newDimensions.width = Math.round(newWidth);
newDimensions.height = Math.round(newHeight);
} else {
newDimensions.height = Math.round(newWidth);
newDimensions.width = Math.round(newHeight);
}
}, 50),
});
}
function flip(type: 'horizontal' | 'vertical') {
if (type === 'vertical') {
if (cropperInstance.value?.getData().scaleX === -1) {
cropperInstance.value?.scaleX(1);
} else {
cropperInstance.value?.scaleX(-1);
}
}
if (type === 'horizontal') {
if (cropperInstance.value?.getData().scaleY === -1) {
cropperInstance.value?.scaleY(1);
} else {
cropperInstance.value?.scaleY(-1);
}
}
}
function rotate() {
cropperInstance.value?.rotate(-90);
}
function reset() {
cropperInstance.value?.reset();
dragMode.value = 'move';
}
}
function setAspectRatio() {
if (imageData.value) {
aspectRatio.value = imageData.value.width / imageData.value.height;
}
}
set(newActive: boolean) {
localActive.value = newActive;
emit('update:modelValue', newActive);
},
});
const { loading, error, imageData, imageElement, save, saving, fetchImage, onImageLoad } = useImage();
const {
cropperInstance,
initCropper,
flip,
rotate,
reset,
aspectRatio,
aspectRatioIcon,
newDimensions,
dragMode,
cropping,
} = useCropper();
watch(internalActive, (isActive) => {
if (isActive === true) {
fetchImage();
} else {
if (cropperInstance.value) {
cropperInstance.value.destroy();
}
loading.value = false;
error.value = null;
imageData.value = null;
}
});
const randomId = ref<string>(nanoid());
const imageURL = computed(() => {
return addTokenToURL(`${getRootPath()}assets/${props.id}?${randomId.value}`);
});
const dimensionsString = computed(() => {
let output = '';
const isSVG = imageData.value?.type === 'image/svg+xml';
if (imageData.value) {
if (isSVG) {
output += 'SVG';
} else {
output += `${n(imageData.value.width ?? 0)}x${n(imageData.value.height ?? 0)}`;
}
if (imageData.value.width !== newDimensions.width || imageData.value.height !== newDimensions.height) {
if (isSVG) {
if (newDimensions.width || newDimensions.height) {
output += ` -> PNG ${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
} else {
output += ' -> PNG';
}
} else {
output += ` -> ${isSVG ? 'PNG ' : ''}${n(newDimensions.width ?? 0)}x${n(newDimensions.height ?? 0)}`;
}
}
}
return output;
});
const customAspectRatios = settingsStore.settings?.custom_aspect_ratios ?? null;
function useImage() {
const loading = ref(false);
const error = ref(null);
const imageData = ref<Image | null>(null);
const saving = ref(false);
const imageElement = ref<HTMLImageElement | null>(null);
return {
loading,
error,
imageData,
saving,
fetchImage,
imageElement,
save,
onImageLoad,
};
async function fetchImage() {
try {
loading.value = true;
const response = await api.get(`/files/${props.id}`, {
params: {
fields: ['type', 'filesize', 'filename_download', 'width', 'height'],
},
});
imageData.value = response.data.data;
} catch (err: any) {
error.value = err;
} finally {
loading.value = false;
}
}
function save() {
saving.value = true;
cropperInstance.value
?.getCroppedCanvas({
imageSmoothingQuality: 'high',
})
.toBlob(async (blob) => {
if (blob === null) {
saving.value = false;
return;
}
const formData = new FormData();
formData.append('file', blob, imageData.value?.filename_download);
try {
await api.patch(`/files/${props.id}`, formData);
emit('refresh');
internalActive.value = false;
randomId.value = nanoid();
} catch (err: any) {
unexpectedError(err);
} finally {
saving.value = false;
}
}, imageData.value?.type);
}
async function onImageLoad() {
await nextTick();
initCropper();
}
}
function useCropper() {
const cropperInstance = ref<Cropper | null>(null);
const localAspectRatio = ref(NaN);
const newDimensions = reactive({
width: null as null | number,
height: null as null | number,
});
watch(imageData, () => {
if (!imageData.value) return;
localAspectRatio.value = imageData.value.width / imageData.value.height;
newDimensions.width = imageData.value.width;
newDimensions.height = imageData.value.height;
});
const aspectRatio = computed<number>({
get() {
return localAspectRatio.value;
},
set(newAspectRatio) {
localAspectRatio.value = newAspectRatio;
cropperInstance.value?.setAspectRatio(newAspectRatio);
cropperInstance.value?.crop();
dragMode.value = 'crop';
},
});
const aspectRatioIcon = computed(() => {
if (!imageData.value) return 'crop_original';
if (customAspectRatios) {
const customAspectRatio = customAspectRatios.find((customAR) => customAR.value == aspectRatio.value);
if (customAspectRatio) return 'crop_square';
}
switch (aspectRatio.value) {
case 16 / 9:
return 'crop_16_9';
case 3 / 2:
return 'crop_3_2';
case 5 / 4:
return 'crop_5_4';
case 7 / 5:
return 'crop_7_5';
case 1 / 1:
return 'crop_square';
case imageData.value.width / imageData.value.height:
return 'crop_original';
default:
return 'crop_free';
}
});
const localDragMode = ref<'move' | 'crop'>('move');
const dragMode = computed({
get() {
return localDragMode.value;
},
set(newMode: 'move' | 'crop') {
cropperInstance.value?.setDragMode(newMode);
localDragMode.value = newMode;
if (newMode === 'move') {
cropperInstance.value?.clear();
localCropping.value = false;
}
},
});
const localCropping = ref(false);
const cropping = computed({
get() {
return localCropping.value;
},
set(newCropping: boolean) {
if (newCropping === false) {
cropperInstance.value?.clear();
}
localCropping.value = newCropping;
},
});
return {
cropperInstance,
initCropper,
flip,
rotate,
reset,
aspectRatio,
aspectRatioIcon,
newDimensions,
dragMode,
cropping,
};
function initCropper() {
if (imageElement.value === null) return;
if (cropperInstance.value) {
cropperInstance.value.destroy();
}
localCropping.value = false;
cropperInstance.value = new Cropper(imageElement.value, {
autoCrop: false,
autoCropArea: 0.5,
toggleDragModeOnDblclick: false,
dragMode: 'move',
viewMode: 1,
crop: throttle((event) => {
if (!imageData.value) return;
if (cropping.value === false && (event.detail.width || event.detail.height)) {
cropping.value = true;
}
const newWidth = event.detail.width || imageData.value.width;
const newHeight = event.detail.height || imageData.value.height;
if (event.detail.rotate === 0 || event.detail.rotate === -180) {
newDimensions.width = Math.round(newWidth);
newDimensions.height = Math.round(newHeight);
} else {
newDimensions.height = Math.round(newWidth);
newDimensions.width = Math.round(newHeight);
}
}, 50),
});
}
function flip(type: 'horizontal' | 'vertical') {
if (type === 'vertical') {
if (cropperInstance.value?.getData().scaleX === -1) {
cropperInstance.value?.scaleX(1);
} else {
cropperInstance.value?.scaleX(-1);
}
}
if (type === 'horizontal') {
if (cropperInstance.value?.getData().scaleY === -1) {
cropperInstance.value?.scaleY(1);
} else {
cropperInstance.value?.scaleY(-1);
}
}
}
function rotate() {
cropperInstance.value?.rotate(-90);
}
function reset() {
cropperInstance.value?.reset();
dragMode.value = 'move';
}
}
function setAspectRatio() {
if (imageData.value) {
aspectRatio.value = imageData.value.width / imageData.value.height;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -5,76 +5,70 @@
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useLatencyStore } from '@/stores/latency';
import { sortBy } from 'lodash';
import prettyMS from 'pretty-ms';
export default defineComponent({
setup() {
const { t } = useI18n();
const { t } = useI18n();
const latencyStore = useLatencyStore();
const latencyStore = useLatencyStore();
const lastLatency = computed(() => {
const sorted = sortBy(latencyStore.latency, ['timestamp']);
return sorted[sorted.length - 1];
});
const lastLatency = computed(() => {
const sorted = sortBy(latencyStore.latency, ['timestamp']);
return sorted[sorted.length - 1];
});
const avgLatency = computed(() => {
if (!latencyStore.latency || latencyStore.latency.length === 0) return 0;
const sorted = sortBy(latencyStore.latency, ['timestamp']);
const lastFive = sorted.slice(Math.max(sorted.length - 5, 0));
let total = 0;
const avgLatency = computed(() => {
if (!latencyStore.latency || latencyStore.latency.length === 0) return 0;
const sorted = sortBy(latencyStore.latency, ['timestamp']);
const lastFive = sorted.slice(Math.max(sorted.length - 5, 0));
let total = 0;
for (const { latency } of lastFive) {
total += latency;
}
for (const { latency } of lastFive) {
total += latency;
}
return Math.round(total / lastFive.length);
});
return Math.round(total / lastFive.length);
});
const connectionStrength = computed(() => {
if (avgLatency.value <= 250) return 4;
else if (avgLatency.value > 250 && avgLatency.value <= 500) return 3;
else if (avgLatency.value > 500 && avgLatency.value <= 750) return 2;
return 1;
});
const connectionStrength = computed(() => {
if (avgLatency.value <= 250) return 4;
else if (avgLatency.value > 250 && avgLatency.value <= 500) return 3;
else if (avgLatency.value > 500 && avgLatency.value <= 750) return 2;
return 1;
});
const latencyTooltip = computed(() => {
switch (connectionStrength.value) {
case 4:
return `${t('connection_excellent')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 3:
return `${t('connection_good')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 2:
return `${t('connection_fair')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 1:
return `${t('connection_poor')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
default:
return null;
}
});
const latencyTooltip = computed(() => {
switch (connectionStrength.value) {
case 4:
return `${t('connection_excellent')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 3:
return `${t('connection_good')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 2:
return `${t('connection_fair')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
case 1:
return `${t('connection_poor')}\n(${prettyMS(avgLatency.value)} ${t('latency')})`;
default:
return null;
}
});
const icon = computed(() => {
switch (connectionStrength.value) {
case 4:
return 'signal_wifi_4_bar';
case 3:
return 'signal_wifi_3_bar';
case 2:
return 'signal_wifi_2_bar';
case 1:
return 'signal_wifi_1_bar';
default:
return null;
}
});
return { icon, lastLatency, latencyTooltip };
},
const icon = computed(() => {
switch (connectionStrength.value) {
case 4:
return 'signal_wifi_4_bar';
case 3:
return 'signal_wifi_3_bar';
case 2:
return 'signal_wifi_2_bar';
case 1:
return 'signal_wifi_1_bar';
default:
return null;
}
});
</script>

View File

@@ -15,35 +15,35 @@
</sidebar-detail>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent, computed } from 'vue';
import { computed } from 'vue';
import { useSync } from '@directus/composables';
import { useExtensions } from '@/extensions';
import { useExtension } from '@/composables/use-extension';
export default defineComponent({
props: {
modelValue: {
type: String,
default: 'tabular',
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
modelValue?: string;
}>(),
{
modelValue: 'tabular',
}
);
const { layouts } = useExtensions();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const selectedLayout = useExtension('layout', props.modelValue);
const fallbackLayout = useExtension('layout', 'tabular');
const currentLayout = computed(() => selectedLayout.value ?? fallbackLayout.value);
const { t } = useI18n();
const layout = useSync(props, 'modelValue', emit);
const { layouts } = useExtensions();
return { t, currentLayout, layouts, layout };
},
});
const selectedLayout = useExtension('layout', props.modelValue);
const fallbackLayout = useExtension('layout', 'tabular');
const currentLayout = computed(() => selectedLayout.value ?? fallbackLayout.value);
const layout = useSync(props, 'modelValue', emit);
</script>
<style lang="scss" scoped>

View File

@@ -8,22 +8,15 @@
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
<script lang="ts" setup>
import { computed } from 'vue';
import LatencyIndicator from './latency-indicator.vue';
import { useServerStore } from '@/stores/server';
export default defineComponent({
components: { LatencyIndicator },
setup() {
const serverStore = useServerStore();
const serverStore = useServerStore();
const name = computed(() => serverStore.info?.project?.project_name);
const descriptor = computed(() => serverStore.info?.project?.project_descriptor);
return { name, descriptor };
},
});
const name = computed(() => serverStore.info?.project?.project_name);
const descriptor = computed(() => serverStore.info?.project?.project_descriptor);
</script>
<style lang="scss" scoped>

View File

@@ -9,76 +9,77 @@
</sidebar-detail>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { computed, defineComponent, ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
export default defineComponent({
props: {
modelValue: {
type: Number,
default: null,
},
const props = withDefaults(
defineProps<{
modelValue: number | null;
}>(),
{
modelValue: null,
}
);
const emit = defineEmits<{
(e: 'update:modelValue', value: number | null): void;
(e: 'refresh'): void;
}>();
const { t } = useI18n();
const interval = computed<number | null>({
get() {
return props.modelValue;
},
emits: ['update:modelValue', 'refresh'],
setup(props, { emit }) {
const { t } = useI18n();
const interval = computed<number | null>({
get() {
return props.modelValue;
},
set(newVal) {
emit('update:modelValue', newVal);
},
});
const activeInterval = ref<NodeJS.Timeout | null>(null);
watch(
interval,
(newInterval) => {
if (activeInterval.value !== null) {
clearInterval(activeInterval.value);
}
if (newInterval !== null && newInterval > 0) {
activeInterval.value = setInterval(() => {
emit('refresh');
}, newInterval * 1000);
}
},
{ immediate: true }
);
const items = computed(() => {
const intervals = [null, 10, 30, 60, 300];
return intervals.map((seconds) => {
if (seconds === null) {
return {
text: t('no_refresh'),
value: null,
};
}
return seconds >= 60 && seconds % 60 === 0
? {
text: t('refresh_interval_minutes', { minutes: seconds / 60 }, seconds / 60),
value: seconds,
}
: {
text: t('refresh_interval_seconds', { seconds }, seconds),
value: seconds,
};
});
});
const active = computed(() => interval.value !== null);
return { t, active, interval, items };
set(newVal) {
emit('update:modelValue', newVal);
},
});
const activeInterval = ref<NodeJS.Timeout | null>(null);
watch(
interval,
(newInterval) => {
if (activeInterval.value !== null) {
clearInterval(activeInterval.value);
}
if (newInterval !== null && newInterval > 0) {
activeInterval.value = setInterval(() => {
emit('refresh');
}, newInterval * 1000);
}
},
{ immediate: true }
);
const items = computed(() => {
const intervals = [null, 10, 30, 60, 300];
return intervals.map((seconds) => {
if (seconds === null) {
return {
text: t('no_refresh'),
value: null,
};
}
return seconds >= 60 && seconds % 60 === 0
? {
text: t('refresh_interval_minutes', { minutes: seconds / 60 }, seconds / 60),
value: seconds,
}
: {
text: t('refresh_interval_seconds', { seconds }, seconds),
value: seconds,
};
});
});
const active = computed(() => interval.value !== null);
</script>
<style lang="scss" scoped>

View File

@@ -19,51 +19,24 @@
</v-error-boundary>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useExtension } from '@/composables/use-extension';
import { defineComponent } from 'vue';
import { toRefs } from 'vue';
export default defineComponent({
props: {
display: {
type: String,
default: null,
},
options: {
type: Object,
default: () => ({}),
},
interface: {
type: String,
default: null,
},
interfaceOptions: {
type: Object,
default: null,
},
value: {
type: [String, Number, Object, Array, Boolean],
default: null,
},
type: {
type: String,
required: true,
},
collection: {
type: String,
required: true,
},
field: {
type: String,
required: true,
},
},
setup(props) {
const displayInfo = useExtension('display', props.display);
const props = defineProps<{
display: string | null;
options?: Record<string, unknown>;
interface?: string;
interfaceOptions?: Record<string, unknown>;
value?: string | number | boolean | Record<string, unknown> | unknown[];
type: string;
collection: string;
field: string;
}>();
return { displayInfo };
},
});
const { display } = toRefs(props);
const displayInfo = useExtension('display', display);
</script>
<style lang="scss" scoped>

View File

@@ -53,7 +53,7 @@ import RevisionsDrawerUpdates from './revisions-drawer-updates.vue';
const props = defineProps<{
revisions: Revision[];
current?: number | string | null;
active?: boolean;
active: boolean;
}>();
const emit = defineEmits<{

View File

@@ -5,21 +5,21 @@
</template>
<v-list>
<v-list-item v-if="!disabledOptions.includes('save-and-stay')" clickable @click="$emit('save-and-stay')">
<v-list-item v-if="!disabledOptions?.includes('save-and-stay')" clickable @click="$emit('save-and-stay')">
<v-list-item-icon><v-icon name="check" /></v-list-item-icon>
<v-list-item-content>{{ t('save_and_stay') }}</v-list-item-content>
<v-list-item-hint>{{ translateShortcut(['meta', 's']) }}</v-list-item-hint>
</v-list-item>
<v-list-item v-if="!disabledOptions.includes('save-and-add-new')" clickable @click="$emit('save-and-add-new')">
<v-list-item v-if="!disabledOptions?.includes('save-and-add-new')" clickable @click="$emit('save-and-add-new')">
<v-list-item-icon><v-icon name="add" /></v-list-item-icon>
<v-list-item-content>{{ t('save_and_create_new') }}</v-list-item-content>
<v-list-item-hint>{{ translateShortcut(['meta', 'shift', 's']) }}</v-list-item-hint>
</v-list-item>
<v-list-item v-if="!disabledOptions.includes('save-as-copy')" clickable @click="$emit('save-as-copy')">
<v-list-item v-if="!disabledOptions?.includes('save-as-copy')" clickable @click="$emit('save-as-copy')">
<v-list-item-icon><v-icon name="done_all" /></v-list-item-icon>
<v-list-item-content>{{ t('save_as_copy') }}</v-list-item-content>
</v-list-item>
<v-list-item v-if="!disabledOptions.includes('discard-and-stay')" clickable @click="$emit('discard-and-stay')">
<v-list-item v-if="!disabledOptions?.includes('discard-and-stay')" clickable @click="$emit('discard-and-stay')">
<v-list-item-icon><v-icon name="undo" /></v-list-item-icon>
<v-list-item-content>{{ t('discard_all_changes') }}</v-list-item-content>
</v-list-item>
@@ -27,25 +27,22 @@
</v-menu>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent } from 'vue';
import { translateShortcut } from '@/utils/translate-shortcut';
export default defineComponent({
props: {
disabledOptions: {
type: Array,
default: () => [],
},
},
emits: ['save-and-stay', 'save-and-add-new', 'save-as-copy', 'discard-and-stay'],
setup() {
const { t } = useI18n();
defineProps<{
disabledOptions?: string[];
}>();
return { t, translateShortcut };
},
});
defineEmits<{
(e: 'save-and-stay'): void;
(e: 'save-and-add-new'): void;
(e: 'save-as-copy'): void;
(e: 'discard-and-stay'): void;
}>();
const { t } = useI18n();
</script>
<style scoped>

View File

@@ -42,134 +42,113 @@
</v-badge>
</template>
<script lang="ts">
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, watch, PropType, computed, inject, Ref } from 'vue';
<script lang="ts" setup>
import { useElementSize } from '@directus/composables';
import { Filter } from '@directus/types';
import { isObject } from 'lodash';
import { useElementSize } from '@directus/composables';
import { Ref, computed, inject, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
props: {
modelValue: {
type: String,
default: null,
},
collection: {
type: String,
required: true,
},
filter: {
type: Object as PropType<Filter>,
default: null,
},
const props = defineProps<{
modelValue: string | null;
collection: string;
filter?: Filter | null;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string | null): void;
(e: 'update:filter', value: Filter | null): void;
}>();
const { t } = useI18n();
const input = ref<HTMLInputElement | null>(null);
const active = ref(props.modelValue !== null);
const filterActive = ref(false);
const filterBorder = ref(false);
const mainElement = inject<Ref<Element | undefined>>('main-element');
const filterElement = ref<HTMLElement>();
const { width: mainElementWidth } = useElementSize(mainElement!);
const { width: filterElementWidth } = useElementSize(filterElement);
watch(
[mainElementWidth, filterElementWidth],
() => {
if (!filterElement.value) return;
const searchElement = filterElement.value.parentElement!;
const minWidth = searchElement.offsetWidth - 4;
if (filterElementWidth.value > minWidth) {
filterElement.value.style.borderTopLeftRadius =
filterElementWidth.value > minWidth + 22 ? 22 + 'px' : filterElementWidth.value - minWidth + 'px';
} else {
filterElement.value.style.borderTopLeftRadius = '0px';
}
const headerElement = mainElement?.value?.firstElementChild;
if (!headerElement) return;
const maxWidth =
searchElement.getBoundingClientRect().right -
(headerElement.getBoundingClientRect().left +
Number(window.getComputedStyle(headerElement).paddingLeft.replace('px', '')));
filterElement.value.style.maxWidth = maxWidth > minWidth ? `${String(maxWidth)}px` : '0px';
},
emits: ['update:modelValue', 'update:filter'],
setup(props, { emit }) {
const { t } = useI18n();
{ immediate: true }
);
const input = ref<HTMLInputElement | null>(null);
const active = ref(props.modelValue !== null);
const filterActive = ref(false);
const filterBorder = ref(false);
const mainElement = inject<Ref<Element | undefined>>('main-element');
const filterElement = ref<HTMLElement>();
const { width: mainElementWidth } = useElementSize(mainElement!);
const { width: filterElementWidth } = useElementSize(filterElement);
watch(
[mainElementWidth, filterElementWidth],
() => {
if (!filterElement.value) return;
const searchElement = filterElement.value.parentElement!;
const minWidth = searchElement.offsetWidth - 4;
if (filterElementWidth.value > minWidth) {
filterElement.value.style.borderTopLeftRadius =
filterElementWidth.value > minWidth + 22 ? 22 + 'px' : filterElementWidth.value - minWidth + 'px';
} else {
filterElement.value.style.borderTopLeftRadius = '0px';
}
const headerElement = mainElement?.value?.firstElementChild;
if (!headerElement) return;
const maxWidth =
searchElement.getBoundingClientRect().right -
(headerElement.getBoundingClientRect().left +
Number(window.getComputedStyle(headerElement).paddingLeft.replace('px', '')));
filterElement.value.style.maxWidth = maxWidth > minWidth ? `${String(maxWidth)}px` : '0px';
},
{ immediate: true }
);
watch(active, (newActive: boolean) => {
if (newActive === true && input.value !== null) {
input.value.focus();
}
});
const activeFilterCount = computed(() => {
if (!props.filter) return 0;
let filterOperators: string[] = [];
parseLevel(props.filter);
return filterOperators.length;
function parseLevel(level: Record<string, any>) {
for (const [key, value] of Object.entries(level)) {
if (key === '_and' || key === '_or') {
value.forEach(parseLevel);
} else if (key.startsWith('_')) {
filterOperators.push(key);
} else {
if (isObject(value)) {
parseLevel(value);
}
}
}
}
});
return {
t,
active,
disable,
input,
emitValue,
activeFilterCount,
filterActive,
onClickOutside,
filterBorder,
filterElement,
};
function onClickOutside(e: { path?: HTMLElement[]; composedPath?: () => HTMLElement[] }) {
const path = e.path || e.composedPath!();
if (path.some((el) => el?.classList?.contains('v-menu-content'))) return false;
return true;
}
function disable() {
active.value = false;
filterActive.value = false;
}
function emitValue() {
if (!input.value) return;
const value = input.value?.value;
emit('update:modelValue', value);
}
},
watch(active, (newActive: boolean) => {
if (newActive === true && input.value !== null) {
input.value.focus();
}
});
const activeFilterCount = computed(() => {
if (!props.filter) return 0;
let filterOperators: string[] = [];
parseLevel(props.filter);
return filterOperators.length;
function parseLevel(level: Record<string, any>) {
for (const [key, value] of Object.entries(level)) {
if (key === '_and' || key === '_or') {
value.forEach(parseLevel);
} else if (key.startsWith('_')) {
filterOperators.push(key);
} else {
if (isObject(value)) {
parseLevel(value);
}
}
}
}
});
function onClickOutside(e: { path?: HTMLElement[]; composedPath?: () => HTMLElement[] }) {
const path = e.path || e.composedPath!();
if (path.some((el) => el?.classList?.contains('v-menu-content'))) return false;
return true;
}
function disable() {
active.value = false;
filterActive.value = false;
}
function emitValue() {
if (!input.value) return;
const value = input.value?.value;
emit('update:modelValue', value);
}
</script>
<style lang="scss" scoped>

View File

@@ -49,56 +49,53 @@
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { format } from 'date-fns';
<script setup lang="ts">
import { isAllowed } from '@/utils/is-allowed';
import { Share } from '@directus/types';
import { format } from 'date-fns';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
export default defineComponent({
props: {
share: {
type: Object,
required: true,
},
},
emits: ['copy', 'edit', 'invite', 'delete'],
setup(props) {
const { t, d } = useI18n();
const props = defineProps<{
share: Share;
}>();
const editAllowed = computed(() => {
return isAllowed('directus_shares', 'update', props.share);
});
defineEmits<{
(e: 'copy'): void;
(e: 'edit'): void;
(e: 'invite'): void;
(e: 'delete'): void;
}>();
const deleteAllowed = computed(() => {
return isAllowed('directus_shares', 'delete', props.share);
});
const { t } = useI18n();
const usesLeft = computed(() => {
if (props.share.max_uses === null) return null;
return props.share.max_uses - props.share.times_used;
});
const editAllowed = computed(() => {
return isAllowed('directus_shares', 'update', props.share);
});
const status = computed(() => {
if (props.share.date_end && new Date(props.share.date_end) < new Date()) {
return 'expired';
}
const deleteAllowed = computed(() => {
return isAllowed('directus_shares', 'delete', props.share);
});
if (props.share.date_start && new Date(props.share.date_start) > new Date()) {
return 'upcoming';
}
const usesLeft = computed(() => {
if (props.share.max_uses === null) return null;
return props.share.max_uses - props.share.times_used;
});
return null;
});
const status = computed(() => {
if (props.share.date_end && new Date(props.share.date_end) < new Date()) {
return 'expired';
}
const formattedTime = computed(() => {
return format(new Date(props.share.date_created), String(t('date-fns_date_short')));
});
if (props.share.date_start && new Date(props.share.date_start) > new Date()) {
return 'upcoming';
}
const confirmDelete = ref<string | null>(null);
return null;
});
return { editAllowed, deleteAllowed, usesLeft, status, t, d, formattedTime, confirmDelete };
},
const formattedTime = computed(() => {
return format(new Date(props.share.date_created), String(t('date-fns_date_short')));
});
</script>

View File

@@ -74,10 +74,9 @@
</sidebar-detail>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, computed } from 'vue';
import DrawerItem from '@/views/private/components/drawer-item.vue';
import { computed, ref } from 'vue';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { Share } from '@directus/types';
@@ -85,179 +84,145 @@ import { useClipboard } from '@/composables/use-clipboard';
import api from '@/api';
import ShareItem from './share-item.vue';
import DrawerItem from '@/views/private/components/drawer-item.vue';
export default defineComponent({
components: { ShareItem, DrawerItem },
props: {
collection: {
type: String,
required: true,
},
primaryKey: {
type: [String, Number],
required: true,
},
allowed: {
type: Boolean,
required: true,
},
},
setup(props) {
const { t } = useI18n();
const props = defineProps<{
collection: string;
primaryKey: string | number;
allowed: boolean;
}>();
const { copyToClipboard } = useClipboard();
const { t } = useI18n();
const shares = ref<Share[] | null>(null);
const count = ref(0);
const error = ref(null);
const loading = ref(false);
const deleting = ref(false);
const shareToEdit = ref<string | null>(null);
const shareToSend = ref<Share | null>(null);
const shareToDelete = ref<Share | null>(null);
const sending = ref(false);
const sendEmails = ref('');
const { copyToClipboard } = useClipboard();
const sendPublicLink = computed(() => {
if (!shareToSend.value) return null;
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
const shares = ref<Share[]>([]);
const count = ref(0);
const error = ref(null);
const loading = ref(false);
const deleting = ref(false);
const shareToEdit = ref<string | null>(null);
const shareToSend = ref<Share | null>(null);
const shareToDelete = ref<Share | null>(null);
const sending = ref(false);
const sendEmails = ref('');
const sendPublicLink = computed(() => {
if (!shareToSend.value) return null;
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
});
refresh();
async function input(data: any) {
if (!data) return;
data.collection = props.collection;
data.item = props.primaryKey;
try {
if (shareToEdit.value === '+') {
await api.post('/shares', data);
} else {
await api.patch(`/shares/${shareToEdit.value}`, data);
}
await refresh();
shareToEdit.value = null;
} catch (error: any) {
unexpectedError(error);
}
}
async function copy(id: string) {
const url = window.location.origin + getRootPath() + 'admin/shared/' + id;
await copyToClipboard(url, { success: t('share_copy_link_success'), fail: t('share_copy_link_error') });
}
function select(id: string) {
shareToEdit.value = id;
}
function unselect() {
shareToEdit.value = null;
}
async function refresh() {
error.value = null;
loading.value = true;
try {
const response = await api.get(`/shares`, {
params: {
filter: {
_and: [
{
collection: {
_eq: props.collection,
},
},
{
item: {
_eq: props.primaryKey,
},
},
],
},
sort: 'name',
},
});
refresh();
count.value = response.data.data.length;
shares.value = response.data.data;
} catch (error: any) {
error.value = error;
} finally {
loading.value = false;
}
}
return {
shareToDelete,
t,
shares,
loading,
error,
refresh,
count,
select,
unselect,
shareToEdit,
input,
copy,
shareToSend,
remove,
deleting,
sendPublicLink,
send,
sending,
sendEmails,
};
async function remove() {
if (!shareToDelete.value) return;
async function input(data: any) {
if (!data) return;
deleting.value = true;
data.collection = props.collection;
data.item = props.primaryKey;
try {
await api.delete(`/shares/${shareToDelete.value.id}`);
await refresh();
shareToDelete.value = null;
} catch (err: any) {
unexpectedError(err);
} finally {
deleting.value = false;
}
}
try {
if (shareToEdit.value === '+') {
await api.post('/shares', data);
} else {
await api.patch(`/shares/${shareToEdit.value}`, data);
}
async function send() {
if (!shareToSend.value) return;
await refresh();
sending.value = true;
shareToEdit.value = null;
} catch (error: any) {
unexpectedError(error);
}
}
try {
const emailsParsed = sendEmails.value
.split(/,|\n/)
.filter((e) => e)
.map((email) => email.trim());
async function copy(id: string) {
const url = window.location.origin + getRootPath() + 'admin/shared/' + id;
await copyToClipboard(url, { success: t('share_copy_link_success'), fail: t('share_copy_link_error') });
}
await api.post('/shares/invite', {
emails: emailsParsed,
share: shareToSend.value.id,
});
function select(id: string) {
shareToEdit.value = id;
}
sendEmails.value = '';
function unselect() {
shareToEdit.value = null;
}
async function refresh() {
error.value = null;
loading.value = true;
try {
const response = await api.get(`/shares`, {
params: {
filter: {
_and: [
{
collection: {
_eq: props.collection,
},
},
{
item: {
_eq: props.primaryKey,
},
},
],
},
sort: 'name',
},
});
count.value = response.data.data.length;
shares.value = response.data.data;
} catch (error: any) {
error.value = error;
} finally {
loading.value = false;
}
}
async function remove() {
if (!shareToDelete.value) return;
deleting.value = true;
try {
await api.delete(`/shares/${shareToDelete.value.id}`);
await refresh();
shareToDelete.value = null;
} catch (err: any) {
unexpectedError(err);
} finally {
deleting.value = false;
}
}
async function send() {
if (!shareToSend.value) return;
sending.value = true;
try {
const emailsParsed = sendEmails.value
.split(/,|\n/)
.filter((e) => e)
.map((email) => email.trim());
await api.post('/shares/invite', {
emails: emailsParsed,
share: shareToSend.value.id,
});
sendEmails.value = '';
shareToSend.value = null;
} catch (err: any) {
unexpectedError(err);
} finally {
sending.value = false;
}
}
},
});
shareToSend.value = null;
} catch (err: any) {
unexpectedError(err);
} finally {
sending.value = false;
}
}
</script>
<style lang="scss" scoped>

View File

@@ -6,7 +6,7 @@
@click="$emit('click', $event)"
>
<div class="icon">
<v-icon :name="icon" />
<v-icon :name="icon!" />
</div>
<div v-if="sidebarOpen" class="title">
<slot />
@@ -15,8 +15,8 @@
</template>
<script lang="ts" setup>
import { useAppStore } from '@/stores/app';
import { toRefs } from 'vue';
import { useAppStore } from '@/stores/app';
withDefaults(
defineProps<{

View File

@@ -22,87 +22,79 @@
</v-avatar>
<div class="data">
<div class="name type-title">{{ userName(data) }}</div>
<div class="status-role" :class="data.status">{{ t(data.status) }} {{ data.role.name }}</div>
<div class="status-role" :class="data!.status">{{ t(data.status) }} {{ data.role.name }}</div>
<div class="email">{{ data.email }}</div>
</div>
</div>
</v-menu>
</template>
<script lang="ts">
<script lang="ts" setup>
import api from '@/api';
import { userName } from '@/utils/user-name';
import { User } from '@directus/types';
import { computed, defineComponent, onUnmounted, ref, watch } from 'vue';
import { computed, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
export default defineComponent({
props: {
user: {
type: String,
required: true,
},
},
setup(props) {
const { t } = useI18n();
const props = defineProps<{
user: string;
}>();
const router = useRouter();
const { t } = useI18n();
const loading = ref(false);
const error = ref(null);
const data = ref<User | null>(null);
const router = useRouter();
const avatarSrc = computed(() => {
if (data.value === null) return null;
const loading = ref(false);
const error = ref(null);
const data = ref<User | null>(null);
if (data.value.avatar?.id) {
return `/assets/${data.value.avatar.id}?key=system-medium-cover`;
}
const avatarSrc = computed(() => {
if (data.value === null) return null;
return null;
});
if (data.value.avatar?.id) {
return `/assets/${data.value.avatar.id}?key=system-medium-cover`;
}
const active = ref(false);
watch(active, () => {
if (active.value === true && data.value === null && loading.value === false) {
fetchUser();
}
});
onUnmounted(() => {
loading.value = false;
error.value = null;
data.value = null;
});
return { t, loading, error, data, active, avatarSrc, userName, navigateToUser };
async function fetchUser() {
loading.value = true;
error.value = null;
try {
const response = await api.get(`/users/${props.user}`, {
params: {
fields: ['id', 'first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
},
});
data.value = response.data.data;
} catch (err: any) {
error.value = err;
} finally {
loading.value = false;
}
}
function navigateToUser() {
if (data.value) router.push(`/users/${data.value.id}`);
}
},
return null;
});
const active = ref(false);
watch(active, () => {
if (active.value === true && data.value === null && loading.value === false) {
fetchUser();
}
});
onUnmounted(() => {
loading.value = false;
error.value = null;
data.value = null;
});
async function fetchUser() {
loading.value = true;
error.value = null;
try {
const response = await api.get(`/users/${props.user}`, {
params: {
fields: ['id', 'first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
},
});
data.value = response.data.data;
} catch (err: any) {
error.value = err;
} finally {
loading.value = false;
}
}
function navigateToUser() {
if (data.value) router.push(`/users/${data.value.id}`);
}
</script>
<style lang="scss" scoped>

View File

@@ -40,96 +40,93 @@
</v-dialog>
</template>
<script lang="ts">
<script lang="ts" setup>
import { useI18n } from 'vue-i18n';
import { defineComponent, ref, watch } from 'vue';
import api from '@/api';
import { ref, watch } from 'vue';
import { unexpectedError } from '@/utils/unexpected-error';
import { APIError } from '@/types/error';
import api from '@/api';
export default defineComponent({
props: {
modelValue: {
type: Boolean,
default: false,
},
role: {
type: String,
default: null,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { t } = useI18n();
const props = withDefaults(
defineProps<{
modelValue: boolean;
role?: string | null;
}>(),
{
role: null,
}
);
const emails = ref<string>('');
const roles = ref<Record<string, any>[]>([]);
const roleSelected = ref<string | null>(props.role);
const loading = ref(false);
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void;
}>();
const uniqueValidationErrors = ref([]);
const { t } = useI18n();
watch(
() => props.modelValue,
() => {
loadRoles();
}
const emails = ref<string>('');
const roles = ref<Record<string, any>[]>([]);
const roleSelected = ref<string | null>(props.role);
const loading = ref(false);
const uniqueValidationErrors = ref([]);
watch(
() => props.modelValue,
() => {
loadRoles();
}
);
async function inviteUsers() {
loading.value = true;
try {
const emailsParsed = emails.value
.split(/,|\n/)
.filter((e) => e)
.map((email) => email.trim());
await api.post('/users/invite', {
email: emailsParsed,
role: roleSelected.value,
});
emails.value = '';
emit('update:modelValue', false);
} catch (err: any) {
uniqueValidationErrors.value = err?.response?.data?.errors?.filter((error: APIError) => {
return error.extensions?.code === 'RECORD_NOT_UNIQUE';
});
const otherErrors = err?.response?.data?.errors?.filter(
(err: APIError) => err?.extensions?.code !== 'RECORD_NOT_UNIQUE'
);
return { t, emails, inviteUsers, roles, roleSelected, loading, uniqueValidationErrors };
async function inviteUsers() {
loading.value = true;
try {
const emailsParsed = emails.value
.split(/,|\n/)
.filter((e) => e)
.map((email) => email.trim());
await api.post('/users/invite', {
email: emailsParsed,
role: roleSelected.value,
});
emails.value = '';
emit('update:modelValue', false);
} catch (err: any) {
uniqueValidationErrors.value = err?.response?.data?.errors?.filter((error: APIError) => {
return error.extensions?.code === 'RECORD_NOT_UNIQUE';
});
const otherErrors = err?.response?.data?.errors?.filter(
(err: APIError) => err?.extensions?.code !== 'RECORD_NOT_UNIQUE'
);
if (otherErrors.length > 0) {
otherErrors.forEach((err: APIError) => unexpectedError(err));
}
} finally {
loading.value = false;
}
if (otherErrors.length > 0) {
otherErrors.forEach((err: APIError) => unexpectedError(err));
}
} finally {
loading.value = false;
}
}
async function loadRoles() {
const response = await api.get('/roles', {
params: {
sort: 'name',
fields: ['id', 'name'],
},
});
async function loadRoles() {
const response = await api.get('/roles', {
params: {
sort: 'name',
fields: ['id', 'name'],
},
});
roles.value = response.data.data.map((role: Record<string, any>) => ({
text: role.name,
value: role.id,
}));
roles.value = response.data.data.map((role: Record<string, any>) => ({
text: role.name,
value: role.id,
}));
if (roles.value.length > 0 && roleSelected.value === null) {
roleSelected.value = roles.value[0].value;
}
}
},
});
if (roles.value.length > 0 && roleSelected.value === null) {
roleSelected.value = roles.value[0].value;
}
}
</script>
<style lang="scss" scoped>