Skip to content

Commit 02f6892

Browse files
authored
[6.x] Add skeleton ui to lazy loaded actions (#14217)
1 parent 857b535 commit 02f6892

3 files changed

Lines changed: 76 additions & 24 deletions

File tree

resources/js/components/actions/ItemActions.vue

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref, computed, useTemplateRef, watch } from 'vue';
33
import useActions from './Actions.js';
44
import ConfirmableAction from './ConfirmableAction.vue';
5+
import useSkeletonDelay from '@/composables/skeleton-delay.js';
56
import axios from 'axios';
67
78
const props = defineProps({
@@ -19,11 +20,17 @@ const { prepareActions, runServerAction } = useActions();
1920
const confirmableActions = useTemplateRef('confirmableActions');
2021
const actions = ref(props.actions);
2122
const actionsLoaded = ref(props.actions !== undefined);
23+
const loading = ref(false);
24+
const shouldShowSkeleton = useSkeletonDelay(loading);
25+
let loadActionsRequest = null;
2226
2327
watch(
24-
() => props.actions,
25-
() => actions.value = props.actions,
26-
{ deep: true }
28+
() => props.actions,
29+
() => {
30+
actions.value = props.actions;
31+
actionsLoaded.value = props.actions !== undefined;
32+
},
33+
{ deep: true }
2734
);
2835
2936
let preparedActions = computed(() => {
@@ -52,7 +59,11 @@ function runAction(action, values, onSuccess, onError) {
5259
5360
function loadActions() {
5461
if (actionsLoaded.value) {
55-
return;
62+
return Promise.resolve(actions.value);
63+
}
64+
65+
if (loading.value) {
66+
return loadActionsRequest;
5667
}
5768
5869
let params = {
@@ -63,9 +74,22 @@ function loadActions() {
6374
params.context = props.context;
6475
}
6576
66-
axios.post(props.url + '/list', params).then((response) => (actions.value = response.data));
77+
loading.value = true;
6778
68-
actionsLoaded.value = true;
79+
loadActionsRequest = axios
80+
.post(props.url + '/list', params)
81+
.then((response) => {
82+
actions.value = response.data;
83+
actionsLoaded.value = true;
84+
85+
return response.data;
86+
})
87+
.finally(() => {
88+
loading.value = false;
89+
loadActionsRequest = null;
90+
});
91+
92+
return loadActionsRequest;
6993
}
7094
7195
defineExpose({
@@ -84,5 +108,10 @@ defineExpose({
84108
:is-dirty="isDirty"
85109
@confirmed="runAction"
86110
/>
87-
<slot :actions="preparedActions" :load-actions="loadActions" />
111+
<slot
112+
:actions="preparedActions"
113+
:load-actions="loadActions"
114+
:loading="loading"
115+
:should-show-skeleton="shouldShowSkeleton"
116+
/>
88117
</template>

resources/js/components/ui/Listing/RowActions.vue

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
DropdownItem,
55
DropdownMenu,
66
DropdownSeparator,
7+
Skeleton,
78
} from '@ui';
89
import { injectListingContext } from '../Listing/Listing.vue';
910
import ItemActions from '@/components/actions/ItemActions.vue';
@@ -64,20 +65,37 @@ function dropdownHovered(loadActions) {
6465
:actions="row.actions"
6566
@started="actionStarted"
6667
@completed="actionCompleted"
67-
v-slot="{ actions, loadActions }"
68+
v-slot="{ actions, loadActions, shouldShowSkeleton }"
6869
>
69-
<Dropdown @mouseover="dropdownHovered(loadActions)" placement="left-start" class="me-3">
70+
<Dropdown
71+
@mouseover="dropdownHovered(loadActions)"
72+
@focus="dropdownHovered(loadActions)"
73+
@click="dropdownHovered(loadActions)"
74+
placement="left-start"
75+
class="me-3"
76+
>
7077
<DropdownMenu>
7178
<slot name="prepended-actions" :row="row" />
72-
<DropdownSeparator v-if="$slots['prepended-actions'] && actions.length" />
73-
<DropdownItem
74-
v-for="action in actions"
75-
:key="action.handle"
76-
:text="__(action.title)"
77-
:icon="action.icon"
78-
:variant="action.dangerous ? 'destructive' : 'default'"
79-
@click="action.run"
80-
/>
79+
<DropdownSeparator v-if="hasPrependedActionsContent && (shouldShowSkeleton || actions.length)" />
80+
<template v-if="shouldShowSkeleton">
81+
<div v-for="index in 3" :key="index" class="contents">
82+
<Skeleton class="m-1 size-5" />
83+
<Skeleton
84+
class="mx-2 my-1.5 h-5"
85+
:class="index === 1 ? 'w-28' : index === 2 ? 'w-36' : 'w-24'"
86+
/>
87+
</div>
88+
</template>
89+
<template v-else>
90+
<DropdownItem
91+
v-for="action in actions"
92+
:key="action.handle"
93+
:text="__(action.title)"
94+
:icon="action.icon"
95+
:variant="action.dangerous ? 'destructive' : 'default'"
96+
@click="action.run"
97+
/>
98+
</template>
8199
</DropdownMenu>
82100
</Dropdown>
83101
</ItemActions>

resources/js/composables/skeleton-delay.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import { ref, watch } from 'vue';
22

33
export default function useSkeletonDelay(isLoading, delay = 400) {
44
const shouldShowSkeleton = ref(false);
5-
6-
const timer = setTimeout(() => shouldShowSkeleton.value = isLoading.value, delay);
5+
let timer = null;
76

87
watch(isLoading, (loading) => {
9-
if (!loading) {
10-
clearTimeout(timer);
11-
shouldShowSkeleton.value = false;
8+
clearTimeout(timer);
9+
10+
if (loading) {
11+
timer = setTimeout(() => {
12+
shouldShowSkeleton.value = true;
13+
}, delay);
14+
return;
1215
}
13-
});
16+
17+
shouldShowSkeleton.value = false;
18+
}, { immediate: true });
1419

1520
return shouldShowSkeleton;
1621
}

0 commit comments

Comments
 (0)