@dejwcake/craftable 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +671 -500
- package/package.json +1 -1
- package/src/components/listing/BulkOperationsBar.vue +19 -4
- package/src/components/listing/ForceDeleteButton.vue +57 -0
- package/src/components/listing/RestoreButton.vue +62 -0
- package/src/components/listing/ShowDeleted.vue +28 -0
- package/src/composables/useBaseListing.js +46 -11
- package/src/index.js +11 -1
- package/src/utils/dateFormatters.js +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dejwcake/craftable",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "This is a frontend dependency for Craftable. Craftable is a Laravel-based open-source toolkit for building administration interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -13,9 +13,20 @@
|
|
|
13
13
|
|
|
14
14
|
<span class="float-end pe-2">
|
|
15
15
|
<slot>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
<span class="d-inline-flex gap-2">
|
|
17
|
+
<button v-if="onBulkDelete" class="btn btn-sm btn-danger px-4" @click="onBulkDelete()">
|
|
18
|
+
<i class="fa fa-trash me-1"></i>
|
|
19
|
+
{{ translations.delete_btn }}
|
|
20
|
+
</button>
|
|
21
|
+
<button v-if="onBulkForceDelete" class="btn btn-sm btn-dark px-4" @click="onBulkForceDelete()">
|
|
22
|
+
<i class="fa fa-eraser me-1"></i>
|
|
23
|
+
{{ translations.force_delete_btn }}
|
|
24
|
+
</button>
|
|
25
|
+
<button v-if="onBulkRestore" class="btn btn-sm btn-success text-white px-4" @click="onBulkRestore()">
|
|
26
|
+
<i class="fa fa-trash-arrow-up me-1"></i>
|
|
27
|
+
{{ translations.restore_btn }}
|
|
28
|
+
</button>
|
|
29
|
+
</span>
|
|
19
30
|
</slot>
|
|
20
31
|
</span>
|
|
21
32
|
</template>
|
|
@@ -27,7 +38,9 @@ defineProps({
|
|
|
27
38
|
loading: { type: Boolean, default: false },
|
|
28
39
|
onCheckAll: { type: Function, required: true },
|
|
29
40
|
onUncheckAll: { type: Function, required: true },
|
|
30
|
-
onBulkDelete: { type: Function,
|
|
41
|
+
onBulkDelete: { type: Function, default: null },
|
|
42
|
+
onBulkForceDelete: { type: Function, default: null },
|
|
43
|
+
onBulkRestore: { type: Function, default: null },
|
|
31
44
|
translations: {
|
|
32
45
|
type: Object,
|
|
33
46
|
default: () => ({
|
|
@@ -35,6 +48,8 @@ defineProps({
|
|
|
35
48
|
check_all_items: 'Check all items',
|
|
36
49
|
uncheck_all_items: 'Uncheck all items',
|
|
37
50
|
delete_btn: 'Delete',
|
|
51
|
+
force_delete_btn: 'Delete permanently',
|
|
52
|
+
restore_btn: 'Restore',
|
|
38
53
|
}),
|
|
39
54
|
},
|
|
40
55
|
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="btn btn-sm btn-dark"
|
|
5
|
+
:title="translations.force_delete_btn"
|
|
6
|
+
@click="showConfirm = true"
|
|
7
|
+
>
|
|
8
|
+
<i class="fa fa-eraser"></i>
|
|
9
|
+
</button>
|
|
10
|
+
|
|
11
|
+
<ConfirmModal
|
|
12
|
+
:show="showConfirm"
|
|
13
|
+
variant="danger"
|
|
14
|
+
:translations="translations"
|
|
15
|
+
@confirm="onConfirm"
|
|
16
|
+
@cancel="showConfirm = false"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup>
|
|
21
|
+
import { ref } from 'vue';
|
|
22
|
+
const axios = window.axios;
|
|
23
|
+
import { notifySuccess, notifyError } from '../../utils/notify.js';
|
|
24
|
+
import ConfirmModal from '../ConfirmModal.vue';
|
|
25
|
+
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
url: { type: String, required: true },
|
|
28
|
+
translations: {
|
|
29
|
+
type: Object,
|
|
30
|
+
default: () => ({
|
|
31
|
+
force_delete_btn: 'Delete permanently',
|
|
32
|
+
confirm_title: 'Deletion',
|
|
33
|
+
confirm_text: 'Delete this item permanently? This cannot be undone.',
|
|
34
|
+
confirm_btn: 'Delete permanently',
|
|
35
|
+
cancel_btn: 'Cancel',
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits(['deleted']);
|
|
41
|
+
|
|
42
|
+
const showConfirm = ref(false);
|
|
43
|
+
|
|
44
|
+
function onConfirm() {
|
|
45
|
+
showConfirm.value = false;
|
|
46
|
+
|
|
47
|
+
axios.delete(props.url).then(
|
|
48
|
+
(response) => {
|
|
49
|
+
notifySuccess(response.data.message);
|
|
50
|
+
emit('deleted');
|
|
51
|
+
},
|
|
52
|
+
(error) => {
|
|
53
|
+
notifyError(error.response?.data?.message);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span :title="disabled ? translations.not_restorable : translations.restore_btn">
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class="btn btn-sm btn-success text-white"
|
|
6
|
+
:aria-label="disabled ? translations.not_restorable : translations.restore_btn"
|
|
7
|
+
:disabled="disabled"
|
|
8
|
+
@click="showConfirm = true"
|
|
9
|
+
>
|
|
10
|
+
<i class="fa fa-trash-arrow-up"></i>
|
|
11
|
+
</button>
|
|
12
|
+
</span>
|
|
13
|
+
|
|
14
|
+
<ConfirmModal
|
|
15
|
+
:show="showConfirm"
|
|
16
|
+
variant="success"
|
|
17
|
+
:translations="translations"
|
|
18
|
+
@confirm="onConfirm"
|
|
19
|
+
@cancel="showConfirm = false"
|
|
20
|
+
/>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
import { ref } from 'vue';
|
|
25
|
+
const axios = window.axios;
|
|
26
|
+
import { notifySuccess, notifyError } from '../../utils/notify.js';
|
|
27
|
+
import ConfirmModal from '../ConfirmModal.vue';
|
|
28
|
+
|
|
29
|
+
const props = defineProps({
|
|
30
|
+
url: { type: String, required: true },
|
|
31
|
+
disabled: { type: Boolean, default: false },
|
|
32
|
+
translations: {
|
|
33
|
+
type: Object,
|
|
34
|
+
default: () => ({
|
|
35
|
+
restore_btn: 'Restore',
|
|
36
|
+
not_restorable: 'This item cannot be restored, an item it belongs to is deleted.',
|
|
37
|
+
confirm_title: 'Restore',
|
|
38
|
+
confirm_text: 'Do you really want to restore this item?',
|
|
39
|
+
confirm_btn: 'Restore',
|
|
40
|
+
cancel_btn: 'Cancel',
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits(['restored']);
|
|
46
|
+
|
|
47
|
+
const showConfirm = ref(false);
|
|
48
|
+
|
|
49
|
+
function onConfirm() {
|
|
50
|
+
showConfirm.value = false;
|
|
51
|
+
|
|
52
|
+
axios.post(props.url).then(
|
|
53
|
+
(response) => {
|
|
54
|
+
notifySuccess(response.data.message);
|
|
55
|
+
emit('restored');
|
|
56
|
+
},
|
|
57
|
+
(error) => {
|
|
58
|
+
notifyError(error.response?.data?.message);
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="d-inline-flex align-items-center gap-2 mb-0 text-nowrap text-reset">
|
|
3
|
+
<span class="form-check form-switch form-switch-inline mb-0">
|
|
4
|
+
<input
|
|
5
|
+
class="form-check-input"
|
|
6
|
+
type="checkbox"
|
|
7
|
+
role="switch"
|
|
8
|
+
:checked="modelValue"
|
|
9
|
+
@change="$emit('update:modelValue', $event.target.checked)"
|
|
10
|
+
/>
|
|
11
|
+
</span>
|
|
12
|
+
{{ translations.show_deleted }}
|
|
13
|
+
</label>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
defineProps({
|
|
18
|
+
modelValue: { type: Boolean, default: false },
|
|
19
|
+
translations: {
|
|
20
|
+
type: Object,
|
|
21
|
+
default: () => ({
|
|
22
|
+
show_deleted: 'Show deleted',
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
defineEmits(['update:modelValue']);
|
|
28
|
+
</script>
|
|
@@ -32,15 +32,18 @@ export function useBaseListing(props) {
|
|
|
32
32
|
const bulkItems = reactive({});
|
|
33
33
|
const bulkCheckingAllLoader = ref(false);
|
|
34
34
|
|
|
35
|
+
const defaultConfirmTranslations = {
|
|
36
|
+
confirm_title: 'Warning!',
|
|
37
|
+
confirm_text: 'Are you sure?',
|
|
38
|
+
confirm_btn: 'Yes, confirm',
|
|
39
|
+
cancel_btn: 'Cancel',
|
|
40
|
+
};
|
|
41
|
+
|
|
35
42
|
const confirmModal = reactive({
|
|
36
43
|
show: false,
|
|
37
44
|
onConfirm: null,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
confirm_text: 'Are you sure?',
|
|
41
|
-
confirm_btn: 'Yes, confirm',
|
|
42
|
-
cancel_btn: 'Cancel',
|
|
43
|
-
},
|
|
45
|
+
variant: 'danger',
|
|
46
|
+
translations: { ...defaultConfirmTranslations },
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
// Update "now" every second
|
|
@@ -152,13 +155,14 @@ export function useBaseListing(props) {
|
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
function
|
|
156
|
-
const
|
|
158
|
+
function bulkAction(url, trans = {}, variant = 'danger') {
|
|
159
|
+
const selectedItems = Object.keys(bulkItems).filter((key) => bulkItems[key]);
|
|
157
160
|
|
|
161
|
+
confirmModal.variant = variant;
|
|
158
162
|
confirmModal.translations = {
|
|
159
|
-
...
|
|
163
|
+
...defaultConfirmTranslations,
|
|
160
164
|
...trans,
|
|
161
|
-
confirm_text: (trans.confirm_text ||
|
|
165
|
+
confirm_text: (trans.confirm_text || defaultConfirmTranslations.confirm_text).replace(
|
|
162
166
|
':number',
|
|
163
167
|
clickedBulkItemsCount.value,
|
|
164
168
|
),
|
|
@@ -167,7 +171,7 @@ export function useBaseListing(props) {
|
|
|
167
171
|
confirmModal.show = false;
|
|
168
172
|
axios
|
|
169
173
|
.post(url, {
|
|
170
|
-
ids:
|
|
174
|
+
ids: selectedItems,
|
|
171
175
|
})
|
|
172
176
|
.then((response) => {
|
|
173
177
|
Object.keys(bulkItems).forEach((key) => delete bulkItems[key]);
|
|
@@ -181,6 +185,34 @@ export function useBaseListing(props) {
|
|
|
181
185
|
confirmModal.show = true;
|
|
182
186
|
}
|
|
183
187
|
|
|
188
|
+
function bulkDelete(url, trans = {}) {
|
|
189
|
+
bulkAction(url, {
|
|
190
|
+
...trans,
|
|
191
|
+
confirm_text: trans.confirm_text || 'Do you really want to delete :number selected items?',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function bulkForceDelete(url, trans = {}) {
|
|
196
|
+
bulkAction(url, {
|
|
197
|
+
confirm_btn: 'Delete permanently',
|
|
198
|
+
...trans,
|
|
199
|
+
confirm_text: trans.confirm_text || 'Delete :number selected items permanently? This cannot be undone.',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function bulkRestore(url, trans = {}) {
|
|
204
|
+
bulkAction(
|
|
205
|
+
url,
|
|
206
|
+
{
|
|
207
|
+
confirm_title: 'Restore',
|
|
208
|
+
confirm_btn: 'Restore',
|
|
209
|
+
...trans,
|
|
210
|
+
confirm_text: trans.confirm_text || 'Do you really want to restore :number selected items?',
|
|
211
|
+
},
|
|
212
|
+
'success',
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
184
216
|
function loadData(resetCurrentPage) {
|
|
185
217
|
const options = {
|
|
186
218
|
params: {
|
|
@@ -298,7 +330,10 @@ export function useBaseListing(props) {
|
|
|
298
330
|
onBulkItemsClickedAll,
|
|
299
331
|
onBulkItemsClickedAllWithPagination,
|
|
300
332
|
onBulkItemsClickedAllUncheck,
|
|
333
|
+
bulkAction,
|
|
301
334
|
bulkDelete,
|
|
335
|
+
bulkForceDelete,
|
|
336
|
+
bulkRestore,
|
|
302
337
|
loadData,
|
|
303
338
|
filter,
|
|
304
339
|
resolveUrl,
|
package/src/index.js
CHANGED
|
@@ -25,6 +25,9 @@ export { default as ListingHeader } from './components/listing/ListingHeader.vue
|
|
|
25
25
|
export { default as ShowButton } from './components/listing/ShowButton.vue';
|
|
26
26
|
export { default as EditButton } from './components/listing/EditButton.vue';
|
|
27
27
|
export { default as DeleteButton } from './components/listing/DeleteButton.vue';
|
|
28
|
+
export { default as ForceDeleteButton } from './components/listing/ForceDeleteButton.vue';
|
|
29
|
+
export { default as RestoreButton } from './components/listing/RestoreButton.vue';
|
|
30
|
+
export { default as ShowDeleted } from './components/listing/ShowDeleted.vue';
|
|
28
31
|
export { default as BackButton } from './components/BackButton.vue';
|
|
29
32
|
export { default as ConfirmModal } from './components/ConfirmModal.vue';
|
|
30
33
|
export { default as ToggleSwitch } from './components/listing/ToggleSwitch.vue';
|
|
@@ -61,7 +64,14 @@ export { default as ActivationError } from './auth/ActivationError.vue';
|
|
|
61
64
|
export { default as TranslationListing } from './translation/TranslationListing.vue';
|
|
62
65
|
|
|
63
66
|
// Utilities
|
|
64
|
-
export {
|
|
67
|
+
export {
|
|
68
|
+
formatDate,
|
|
69
|
+
formatDatetime,
|
|
70
|
+
formatTime,
|
|
71
|
+
formatDuration,
|
|
72
|
+
nowInTimezone,
|
|
73
|
+
dayjs,
|
|
74
|
+
} from './utils/dateFormatters.js';
|
|
65
75
|
export { mediaCollectionProp, mediaCollectionDefaults } from './utils/mediaProps.js';
|
|
66
76
|
export { notifySuccess, notifyError } from './utils/notify.js';
|
|
67
77
|
export { getDateFnsLocale, initDateFnsLocale } from './utils/dateFnsLocale.js';
|
|
@@ -23,6 +23,47 @@ export function formatTime(time, format = 'HH:mm') {
|
|
|
23
23
|
return d.isValid() ? d.format(format) : '';
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const SECONDS_IN_MINUTE = 60;
|
|
27
|
+
const SECONDS_IN_HOUR = 3600;
|
|
28
|
+
const SECONDS_IN_DAY = 86400;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Formats a number of seconds as a compact duration, e.g. `359d 19h 1m 39s`.
|
|
32
|
+
*
|
|
33
|
+
* Zero-valued units are dropped, so an exact hour reads `2h` rather than `0d 2h 0m 0s`, and a
|
|
34
|
+
* duration of zero reads `0s`. `null`, `undefined`, negative and non-finite values (`NaN`,
|
|
35
|
+
* `Infinity`) return the placeholder, which lets a listing cell or detail row render a missing or
|
|
36
|
+
* invalid value without a guard at the call site.
|
|
37
|
+
*
|
|
38
|
+
* Deliberately plain arithmetic rather than dayjs's duration plugin: that plugin normalises a
|
|
39
|
+
* duration into years and months, so its `hours()` is the component of a year/month/day breakdown
|
|
40
|
+
* and does not line up with a total day count. Mixing the two reports 31,086,099 s as
|
|
41
|
+
* `359d 5h 1m 39s` instead of `359d 19h 1m 39s`.
|
|
42
|
+
*
|
|
43
|
+
* @param {number|null|undefined} seconds total seconds; fractions are floored.
|
|
44
|
+
* @param {string} [placeholder] returned for `null`, `undefined`, negative and non-finite values.
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function formatDuration(seconds, placeholder = '—') {
|
|
48
|
+
if (seconds === null || seconds === undefined) {
|
|
49
|
+
return placeholder;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const total = Math.floor(seconds);
|
|
53
|
+
if (!Number.isFinite(total) || total < 0) {
|
|
54
|
+
return placeholder;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parts = [
|
|
58
|
+
[Math.floor(total / SECONDS_IN_DAY), 'd'],
|
|
59
|
+
[Math.floor((total % SECONDS_IN_DAY) / SECONDS_IN_HOUR), 'h'],
|
|
60
|
+
[Math.floor((total % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE), 'm'],
|
|
61
|
+
[total % SECONDS_IN_MINUTE, 's'],
|
|
62
|
+
].filter(([amount]) => amount > 0);
|
|
63
|
+
|
|
64
|
+
return parts.length === 0 ? '0s' : parts.map(([amount, unit]) => `${amount}${unit}`).join(' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
export function nowInTimezone(tz = 'UTC', format = 'YYYY-MM-DD HH:mm:ss') {
|
|
27
68
|
return dayjs().tz(tz).format(format);
|
|
28
69
|
}
|