Do not load sidebar details until they are opened (#20848)

Co-authored-by: ian <licitdev@gmail.com>
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
daedalus
2023-12-29 05:56:28 -05:00
committed by GitHub
parent 0d1fd7c77e
commit 2ed6da0ff9
8 changed files with 453 additions and 159 deletions

View File

@@ -0,0 +1,5 @@
---
'@directus/app': minor
---
Prevented loading of sidebar details (revisions, comments, shares etc) until they are opened

View File

@@ -27,13 +27,25 @@ export function useRevisions(
const revisions = ref<Revision[] | null>(null);
const revisionsByDate = ref<RevisionsByDate[] | null>(null);
const loading = ref(false);
const loadingCount = ref(false);
const revisionsCount = ref(0);
const created = ref<Revision>();
const pagesCount = ref(0);
watch([collection, primaryKey, version], () => getRevisions(), { immediate: true });
watch([collection, primaryKey, version], () => refresh());
return { created, revisions, revisionsByDate, loading, refresh, revisionsCount, pagesCount };
return {
created,
revisions,
revisionsByDate,
getRevisions,
loading,
refresh,
loadingCount,
revisionsCount,
getRevisionsCount,
pagesCount,
};
async function getRevisions(page = 0) {
if (typeof unref(primaryKey) === 'undefined') return;
@@ -98,7 +110,6 @@ export function useRevisions(
'activity.user_agent',
'activity.origin',
],
meta: ['filter_count'],
},
});
@@ -137,7 +148,6 @@ export function useRevisions(
'activity.user_agent',
'activity.origin',
],
meta: ['filter_count'],
},
});
@@ -193,7 +203,6 @@ export function useRevisions(
revisionsByDate.value = orderBy(revisionsGrouped, ['date'], ['desc']);
revisions.value = orderBy(response.data.data, ['activity.timestamp'], ['desc']);
revisionsCount.value = response.data.meta.filter_count;
pagesCount.value = Math.ceil(revisionsCount.value / pageSize);
} catch (error) {
unexpectedError(error);
@@ -202,7 +211,63 @@ export function useRevisions(
}
}
async function getRevisionsCount() {
if (typeof unref(primaryKey) === 'undefined') return;
loadingCount.value = true;
try {
const filter: Filter = {
_and: [
{
collection: {
_eq: unref(collection),
},
},
{
item: {
_eq: unref(primaryKey),
},
},
{
version: version?.value
? {
_eq: version.value.id,
}
: { _null: true },
},
],
};
if (options?.action) {
filter._and.push({
activity: {
action: {
_eq: options?.action,
},
},
});
}
const response = await api.get(`/revisions`, {
params: {
filter,
aggregate: {
count: 'id',
},
},
});
revisionsCount.value = Number(response.data.data[0].count.id);
} catch (error) {
unexpectedError(error);
} finally {
loadingCount.value = false;
}
}
async function refresh(page = 0) {
await getRevisionsCount();
await getRevisions(page);
}

View File

@@ -19,6 +19,7 @@ import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-d
import SaveOptions from '@/views/private/components/save-options.vue';
import SharesSidebarDetail from '@/views/private/components/shares-sidebar-detail.vue';
import { useCollection } from '@directus/composables';
import type { PrimaryKey } from '@directus/types';
import { useHead } from '@unhead/vue';
import { useRouter } from 'vue-router';
import LivePreview from '../components/live-preview.vue';
@@ -184,11 +185,25 @@ const isFormDisabled = computed(() => {
return true;
});
const actualPrimaryKey = computed(() => {
if (unref(isSingleton)) {
const singleton = unref(item);
const pkField = unref(primaryKeyField)?.field;
return (singleton && pkField ? singleton[pkField] ?? null : null) as PrimaryKey | null;
}
return props.primaryKey;
});
const internalPrimaryKey = computed(() => {
if (unref(loading)) return '+';
if (unref(isNew)) return '+';
if (unref(isSingleton)) return unref(item)?.[unref(primaryKeyField)?.field] ?? '+';
if (unref(isSingleton)) {
const singleton = unref(item);
const pkField = unref(primaryKeyField)?.field;
return (singleton && pkField ? singleton[pkField] ?? '+' : '+') as PrimaryKey;
}
return props.primaryKey;
});
@@ -706,12 +721,12 @@ function revert(values: Record<string, any>) {
<sidebar-detail icon="info" :title="t('information')" close>
<div v-md="t('page_help_collections_item')" class="page-description" />
</sidebar-detail>
<template v-if="isNew === false && loading === false && internalPrimaryKey">
<template v-if="isNew === false && actualPrimaryKey">
<revisions-drawer-detail
v-if="revisionsAllowed && accountabilityScope === 'all'"
ref="revisionsDrawerDetailRef"
:collection="collection"
:primary-key="internalPrimaryKey"
:primary-key="actualPrimaryKey"
:version="currentVersion"
:scope="accountabilityScope"
@revert="revert"
@@ -719,19 +734,19 @@ function revert(values: Record<string, any>) {
<comments-sidebar-detail
v-if="currentVersion === null"
:collection="collection"
:primary-key="internalPrimaryKey"
:primary-key="actualPrimaryKey"
/>
<shares-sidebar-detail
v-if="currentVersion === null"
:collection="collection"
:primary-key="internalPrimaryKey"
:primary-key="actualPrimaryKey"
:allowed="shareAllowed"
/>
<flow-sidebar-detail
v-if="currentVersion === null"
location="item"
:collection="collection"
:primary-key="internalPrimaryKey"
:primary-key="actualPrimaryKey"
:has-edits="hasEdits"
@refresh="refresh"
/>

View File

@@ -3,9 +3,10 @@ import { useRevisions } from '@/composables/use-revisions';
import { useExtensions } from '@/extensions';
import type { FlowRaw } from '@directus/types';
import { Action } from '@directus/constants';
import { computed, ref, toRefs, unref, watch } from 'vue';
import { computed, ref, toRefs, unref, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { getTriggers } from '../triggers';
import { abbreviateNumber } from '@directus/utils';
const props = defineProps<{
flow: FlowRaw;
@@ -24,14 +25,15 @@ const usedTrigger = computed(() => {
const page = ref<number>(1);
const { revisionsByDate, revisionsCount, loading, pagesCount, refresh } = useRevisions(
ref('directus_flows'),
computed(() => unref(flow).id),
ref(null),
{
action: Action.RUN,
},
);
const { revisionsByDate, getRevisions, revisionsCount, getRevisionsCount, loading, loadingCount, pagesCount, refresh } =
useRevisions(
ref('directus_flows'),
computed(() => unref(flow).id),
ref(null),
{
action: Action.RUN,
},
);
watch(
() => page.value,
@@ -40,6 +42,10 @@ watch(
},
);
onMounted(() => {
getRevisionsCount();
});
const previewing = ref();
const triggerData = computed(() => {
@@ -86,10 +92,19 @@ const steps = computed(() => {
},
);
});
function onToggle(open: boolean) {
if (open && revisionsByDate.value === null) getRevisions();
}
</script>
<template>
<sidebar-detail :title="t('logs')" icon="fact_check" :badge="revisionsCount">
<sidebar-detail
:title="t('logs')"
icon="fact_check"
:badge="!loadingCount && revisionsCount > 0 ? abbreviateNumber(revisionsCount) : null"
@toggle="onToggle"
>
<v-progress-linear v-if="!revisionsByDate && loading" indeterminate />
<div v-else-if="revisionsCount === 0" class="empty">{{ t('no_logs') }}</div>

View File

@@ -3,10 +3,11 @@ import api from '@/api';
import { Activity, ActivityByDate } from '@/types/activity';
import { localizedFormat } from '@/utils/localized-format';
import { userName } from '@/utils/user-name';
import type { User } from '@directus/types';
import type { PrimaryKey, User } from '@directus/types';
import { abbreviateNumber } from '@directus/utils';
import { isThisYear, isToday, isYesterday } from 'date-fns';
import { flatten, groupBy, orderBy } from 'lodash';
import { ref } from 'vue';
import { Ref, onMounted, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import CommentInput from './comment-input.vue';
import CommentItem from './comment-item.vue';
@@ -20,24 +21,46 @@ type ActivityByDateDisplay = ActivityByDate & {
const props = defineProps<{
collection: string;
primaryKey: string | number;
primaryKey: PrimaryKey;
}>();
const { t } = useI18n();
const { activity, loading, refresh, count, userPreviews } = useActivity(props.collection, props.primaryKey);
const { collection, primaryKey } = toRefs(props);
function useActivity(collection: string, primaryKey: string | number) {
const { activity, getActivity, loading, refresh, activityCount, getActivityCount, loadingCount, userPreviews } =
useActivity(collection, primaryKey);
onMounted(() => {
getActivityCount();
});
function onToggle(open: boolean) {
if (open && activity.value === null) getActivity();
}
function useActivity(collection: Ref<string>, primaryKey: Ref<PrimaryKey>) {
const regex = /\s@[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}/gm;
const activity = ref<ActivityByDateDisplay[] | null>(null);
const count = ref(0);
const activityCount = ref(0);
const error = ref(null);
const loading = ref(false);
const loadingCount = ref(false);
const userPreviews = ref<Record<string, any>>({});
getActivity();
watch([collection, primaryKey], () => refresh());
return { activity, error, loading, refresh, count, userPreviews };
return {
activity,
getActivity,
error,
loading,
refresh,
activityCount,
getActivityCount,
loadingCount,
userPreviews,
};
async function getActivity() {
error.value = null;
@@ -46,8 +69,8 @@ function useActivity(collection: string, primaryKey: string | number) {
try {
const response = await api.get(`/activity`, {
params: {
'filter[collection][_eq]': collection,
'filter[item][_eq]': primaryKey,
'filter[collection][_eq]': collection.value,
'filter[item][_eq]': primaryKey.value,
'filter[action][_eq]': 'comment',
sort: '-id', // directus_activity has auto increment and is therefore in chronological order
fields: [
@@ -65,8 +88,6 @@ function useActivity(collection: string, primaryKey: string | number) {
},
});
count.value = response.data.data.length;
userPreviews.value = await loadUserPreviews(response.data.data, regex);
const activityWithUsersInComments = (response.data.data as Activity[]).map((comment) => {
@@ -120,7 +141,48 @@ function useActivity(collection: string, primaryKey: string | number) {
}
}
async function getActivityCount() {
error.value = null;
loadingCount.value = true;
try {
const response = await api.get(`/activity`, {
params: {
filter: {
_and: [
{
collection: {
_eq: collection.value,
},
},
{
item: {
_eq: primaryKey.value,
},
},
{
action: {
_eq: 'comment',
},
},
],
},
aggregate: {
count: 'id',
},
},
});
activityCount.value = Number(response.data.data[0].count.id);
} catch (error: any) {
error.value = error;
} finally {
loadingCount.value = false;
}
}
async function refresh() {
await getActivityCount();
await getActivity();
}
}
@@ -158,7 +220,12 @@ async function loadUserPreviews(comments: Record<string, any>, regex: RegExp) {
</script>
<template>
<sidebar-detail :title="t('comments')" icon="chat_bubble_outline" :badge="count || null">
<sidebar-detail
:title="t('comments')"
icon="chat_bubble_outline"
:badge="!loadingCount && activityCount > 0 ? abbreviateNumber(activityCount) : null"
@toggle="onToggle"
>
<comment-input :refresh="refresh" :collection="collection" :primary-key="primaryKey" />
<v-progress-linear v-if="loading" indeterminate />

View File

@@ -2,7 +2,7 @@
import { useRevisions } from '@/composables/use-revisions';
import { ContentVersion } from '@directus/types';
import { abbreviateNumber } from '@directus/utils';
import { ref, toRefs, watch } from 'vue';
import { onMounted, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import RevisionsDateGroup from './revisions-date-group.vue';
import RevisionsDrawer from './revisions-drawer.vue';
@@ -19,16 +19,27 @@ const { t } = useI18n();
const { collection, primaryKey, version } = toRefs(props);
const { revisions, revisionsByDate, loading, refresh, revisionsCount, pagesCount, created } = useRevisions(
collection,
primaryKey,
version,
);
const modalActive = ref(false);
const modalCurrentRevision = ref<number | null>(null);
const page = ref<number>(1);
const {
revisions,
revisionsByDate,
loading,
refresh,
revisionsCount,
pagesCount,
created,
getRevisions,
loadingCount,
getRevisionsCount,
} = useRevisions(collection, primaryKey, version);
onMounted(() => {
getRevisionsCount();
});
watch(
() => page.value,
(newPage) => {
@@ -41,6 +52,10 @@ function openModal(id: number) {
modalActive.value = true;
}
function onToggle(open: boolean) {
if (open && revisions.value === null) getRevisions();
}
defineExpose({
refresh,
});
@@ -50,7 +65,8 @@ defineExpose({
<sidebar-detail
:title="t('revisions')"
icon="change_history"
:badge="!loading && revisionsCount > 0 ? abbreviateNumber(revisionsCount) : null"
:badge="!loadingCount && revisionsCount > 0 ? abbreviateNumber(revisionsCount) : null"
@toggle="onToggle"
>
<v-progress-linear v-if="!revisions && loading" indeterminate />

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import { useClipboard } from '@/composables/use-clipboard';
import { getRootPath } from '@/utils/get-root-path';
import { unexpectedError } from '@/utils/unexpected-error';
import { Share } from '@directus/types';
import { useClipboard } from '@/composables/use-clipboard';
import { PrimaryKey, Share } from '@directus/types';
import { Ref, computed, onMounted, ref, toRefs, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import api from '@/api';
import ShareItem from './share-item.vue';
import DrawerItem from '@/views/private/components/drawer-item.vue';
import { abbreviateNumber } from '@directus/utils';
import ShareItem from './share-item.vue';
const props = defineProps<{
collection: string;
@@ -18,44 +19,224 @@ const props = defineProps<{
const { t } = useI18n();
const { collection, primaryKey } = toRefs(props);
const { copyToClipboard } = useClipboard();
const shares = ref<Share[] | 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 {
shares,
shareToEdit,
shareToSend,
shareToDelete,
sharesCount,
loading,
loadingCount,
sendPublicLink,
sendEmails,
error,
getShares,
getSharesCount,
input,
send,
select,
unselect,
remove,
sending,
deleting,
} = useShares(collection, primaryKey);
const sendPublicLink = computed(() => {
if (!shareToSend.value) return null;
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
onMounted(() => {
getSharesCount();
});
refresh();
function onToggle(open: boolean) {
if (open && shares.value === null) getShares();
}
async function input(data: any) {
if (!data) return;
function useShares(collection: Ref<string>, primaryKey: Ref<PrimaryKey>) {
const shares = ref<Share[] | null>(null);
const sharesCount = ref(0);
const error = ref(null);
const loading = ref(false);
const loadingCount = 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('');
data.collection = props.collection;
data.item = props.primaryKey;
watch([collection, primaryKey], () => refresh());
try {
if (shareToEdit.value === '+') {
await api.post('/shares', data);
} else {
await api.patch(`/shares/${shareToEdit.value}`, data);
const sendPublicLink = computed(() => {
if (!shareToSend.value) return null;
return window.location.origin + getRootPath() + 'admin/shared/' + shareToSend.value.id;
});
return {
shares,
shareToEdit,
shareToSend,
shareToDelete,
sharesCount,
loading,
loadingCount,
sendPublicLink,
sendEmails,
error,
getShares,
getSharesCount,
input,
send,
select,
unselect,
remove,
sending,
deleting,
};
async function input(data: any) {
if (!data) return;
data.collection = collection.value;
data.item = primaryKey.value;
try {
if (shareToEdit.value === '+') {
await api.post('/shares', data);
} else {
await api.patch(`/shares/${shareToEdit.value}`, data);
}
await refresh();
shareToEdit.value = null;
} catch (error) {
unexpectedError(error);
}
}
await refresh();
function select(id: string) {
shareToEdit.value = id;
}
function unselect() {
shareToEdit.value = null;
} catch (error) {
unexpectedError(error);
}
async function refresh() {
await getSharesCount();
await getShares();
}
async function getShares() {
error.value = null;
loading.value = true;
try {
const response = await api.get(`/shares`, {
params: {
filter: {
_and: [
{
collection: {
_eq: collection.value,
},
},
{
item: {
_eq: primaryKey.value,
},
},
],
},
sort: 'name',
},
});
shares.value = response.data.data;
} catch (error: any) {
error.value = error;
} finally {
loading.value = false;
}
}
async function getSharesCount() {
error.value = null;
loadingCount.value = true;
try {
const response = await api.get(`/shares`, {
params: {
filter: {
_and: [
{
collection: {
_eq: collection.value,
},
},
{
item: {
_eq: primaryKey.value,
},
},
],
},
aggregate: {
count: 'id',
},
},
});
sharesCount.value = Number(response.data.data[0].count.id);
} catch (error: any) {
error.value = error;
} finally {
loadingCount.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 (error) {
unexpectedError(error);
} 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 (error) {
unexpectedError(error);
} finally {
sending.value = false;
}
}
}
@@ -63,94 +244,15 @@ 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',
},
});
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 (error) {
unexpectedError(error);
} 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 (error) {
unexpectedError(error);
} finally {
sending.value = false;
}
}
</script>
<template>
<sidebar-detail :title="t('shares')" icon="share" :badge="count">
<sidebar-detail
:title="t('shares')"
icon="share"
:badge="!loadingCount && sharesCount > 0 ? abbreviateNumber(sharesCount) : null"
@toggle="onToggle"
>
<v-notice v-if="error" type="danger">{{ t('unexpected_error') }}</v-notice>
<v-progress-linear v-else-if="loading" indeterminate />

View File

@@ -10,6 +10,10 @@ const props = defineProps<{
close?: boolean;
}>();
const emit = defineEmits<{
toggle: [open: boolean];
}>();
const { active, toggle } = useGroupable({
value: props.title,
group: 'sidebar-detail',
@@ -17,11 +21,16 @@ const { active, toggle } = useGroupable({
const appStore = useAppStore();
const { sidebarOpen } = toRefs(appStore);
function onClick() {
emit('toggle', !active.value);
toggle();
}
</script>
<template>
<div class="sidebar-detail" :class="{ open: sidebarOpen }">
<button v-tooltip.left="!sidebarOpen && title" class="toggle" :class="{ open: active }" @click="toggle">
<button v-tooltip.left="!sidebarOpen && title" class="toggle" :class="{ open: active }" @click="onClick">
<div class="icon">
<v-badge :dot="badge === true" bordered :value="badge" :disabled="!badge">
<v-icon :name="icon" />