apostrophe 4.28.0 → 4.29.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +33 -3
  2. package/README.md +142 -0
  3. package/defaults.js +1 -0
  4. package/lib/safe-json-script.js +27 -0
  5. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposAdminBarLocale.vue +1 -1
  6. package/modules/@apostrophecms/admin-bar/ui/apos/components/TheAposContextBar.vue +1 -0
  7. package/modules/@apostrophecms/area/ui/apos/components/AposAreaWidget.vue +3 -5
  8. package/modules/@apostrophecms/area/ui/apos/components/AposBreadcrumbOperations.vue +13 -1
  9. package/modules/@apostrophecms/asset/lib/globalIcons.js +3 -0
  10. package/modules/@apostrophecms/attachment/index.js +43 -1
  11. package/modules/@apostrophecms/color-field/index.js +7 -1
  12. package/modules/@apostrophecms/doc/index.js +11 -1
  13. package/modules/@apostrophecms/doc-type/index.js +165 -32
  14. package/modules/@apostrophecms/doc-type/ui/apos/components/AposDocEditor.vue +1 -1
  15. package/modules/@apostrophecms/doc-type/ui/apos/logic/AposDocContextMenu.js +104 -59
  16. package/modules/@apostrophecms/file/index.js +109 -9
  17. package/modules/@apostrophecms/i18n/i18n/de.json +0 -2
  18. package/modules/@apostrophecms/i18n/i18n/en.json +40 -1
  19. package/modules/@apostrophecms/i18n/i18n/es.json +0 -1
  20. package/modules/@apostrophecms/i18n/i18n/fr.json +0 -1
  21. package/modules/@apostrophecms/i18n/i18n/it.json +0 -1
  22. package/modules/@apostrophecms/i18n/i18n/pt-BR.json +0 -1
  23. package/modules/@apostrophecms/i18n/i18n/sk.json +0 -1
  24. package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nBatchReporting.js +18 -1
  25. package/modules/@apostrophecms/i18n/ui/apos/apps/AposI18nLocalizeActions.js +50 -0
  26. package/modules/@apostrophecms/i18n/ui/apos/components/AposI18nLocalize.vue +56 -13
  27. package/modules/@apostrophecms/image/ui/apos/components/AposImageRelationshipEditor.vue +8 -2
  28. package/modules/@apostrophecms/layout-column-widget/index.js +156 -163
  29. package/modules/@apostrophecms/layout-widget/index.js +7 -2
  30. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposAreaLayoutEditor.vue +6 -11
  31. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridColumn.vue +3 -5
  32. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridLayout.vue +4 -4
  33. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposGridManager.vue +0 -16
  34. package/modules/@apostrophecms/layout-widget/ui/apos/lib/grid-state.mjs +7 -27
  35. package/modules/@apostrophecms/layout-widget/views/column.html +7 -9
  36. package/modules/@apostrophecms/login/index.js +39 -40
  37. package/modules/@apostrophecms/modal/ui/apos/components/AposDocsManagerToolbar.vue +17 -2
  38. package/modules/@apostrophecms/modal/ui/apos/components/AposModal.vue +3 -2
  39. package/modules/@apostrophecms/notification/ui/apos/components/AposNotification.vue +1 -0
  40. package/modules/@apostrophecms/page/index.js +2 -0
  41. package/modules/@apostrophecms/piece-type/index.js +3 -1
  42. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManager.vue +1 -0
  43. package/modules/@apostrophecms/piece-type/ui/apos/components/AposDocsManagerDisplay.vue +5 -0
  44. package/modules/@apostrophecms/recently-edited/index.js +831 -0
  45. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposCellTitle.vue +54 -0
  46. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedCombo.vue +454 -0
  47. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilterTag.vue +75 -0
  48. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedFilters.vue +287 -0
  49. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedIcon.vue +16 -0
  50. package/modules/@apostrophecms/recently-edited/ui/apos/components/AposRecentlyEditedManager.vue +346 -0
  51. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedBatch.js +193 -0
  52. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedData.js +276 -0
  53. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFetch.js +199 -0
  54. package/modules/@apostrophecms/recently-edited/ui/apos/composables/useRecentlyEditedFilters.js +100 -0
  55. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputRelationship.js +8 -4
  56. package/modules/@apostrophecms/schema/ui/apos/logic/AposInputWrapper.js +1 -1
  57. package/modules/@apostrophecms/styles/index.js +10 -0
  58. package/modules/@apostrophecms/styles/lib/apiRoutes.js +6 -0
  59. package/modules/@apostrophecms/styles/lib/handlers.js +5 -0
  60. package/modules/@apostrophecms/styles/lib/methods.js +9 -3
  61. package/modules/@apostrophecms/styles/lib/presets.js +119 -0
  62. package/modules/@apostrophecms/styles/ui/apos/components/TheAposStyles.vue +3 -8
  63. package/modules/@apostrophecms/styles/ui/apos/composables/AposStyles.js +1 -3
  64. package/modules/@apostrophecms/styles/ui/apos/render-factory.js +29 -0
  65. package/modules/@apostrophecms/styles/ui/apos/universal/backgroundHelpers.mjs +140 -0
  66. package/modules/@apostrophecms/styles/ui/apos/universal/customRules.mjs +105 -0
  67. package/modules/@apostrophecms/styles/ui/apos/universal/render.mjs +195 -15
  68. package/modules/@apostrophecms/template/index.js +22 -6
  69. package/modules/@apostrophecms/ui/ui/apos/components/AposCellContextMenu.vue +2 -0
  70. package/modules/@apostrophecms/ui/ui/apos/components/AposContextMenu.vue +18 -4
  71. package/modules/@apostrophecms/ui/ui/apos/composables/useInfiniteScroll.js +91 -0
  72. package/modules/@apostrophecms/ui/ui/apos/scss/global/_theme.scss +1 -0
  73. package/modules/@apostrophecms/ui/ui/apos/stores/modal.js +5 -2
  74. package/modules/@apostrophecms/ui/ui/apos/utils/index.js +9 -0
  75. package/modules/@apostrophecms/url/index.js +38 -4
  76. package/modules/@apostrophecms/widget-type/index.js +22 -6
  77. package/modules/@apostrophecms/widget-type/ui/apos/components/AposWidgetEditor.vue +8 -4
  78. package/package.json +19 -19
  79. package/test/files.js +129 -0
  80. package/test/layout-widget-migration.js +719 -0
  81. package/test/login-requirements.js +1 -1
  82. package/test/pieces-public-api.js +80 -0
  83. package/test/pieces.js +25 -0
  84. package/test/recently-edited.js +2311 -0
  85. package/test/schemas.js +39 -3
  86. package/test/static-build.js +642 -0
  87. package/test/styles.js +2569 -0
  88. package/modules/@apostrophecms/layout-widget/ui/apos/components/AposLayoutColControlDialog.vue +0 -171
@@ -0,0 +1,287 @@
1
+ <template>
2
+ <AposContextMenu
3
+ :button="button"
4
+ menu-placement="bottom-end"
5
+ identifier="recently-edited-filter-menu"
6
+ >
7
+ <div
8
+ class="apos-recently-edited-filters"
9
+ data-apos-test="recently-edited-filters"
10
+ >
11
+ <div class="apos-recently-edited-filters__header">
12
+ <h3 class="apos-recently-edited-filters__title">
13
+ {{ $t('apostrophe:recentlyEditedFilters') }}
14
+ </h3>
15
+ <button
16
+ type="button"
17
+ class="apos-recently-edited-filters__clear"
18
+ data-apos-test="recently-edited-filter-reset"
19
+ @click="clearFilters"
20
+ >
21
+ {{ $t('apostrophe:recentlyEditedResetFilters') }}
22
+ </button>
23
+ </div>
24
+ <div
25
+ v-for="filterSet in filterSets"
26
+ :key="filterSet.key"
27
+ class="apos-recently-edited-filters__row"
28
+ :data-apos-test="'recently-edited-filter-' + filterSet.name"
29
+ >
30
+ <component
31
+ :is="COMPONENT_MAP[filterSet.field.type] || COMPONENT_MAP.select"
32
+ :field="filterSet.field"
33
+ :model-value="filterSet.value"
34
+ :status="filterSet.status"
35
+ :modifiers="['small', 'inline']"
36
+ :add-label="filterSet.addLabel"
37
+ :no-blur-emit="true"
38
+ @update:model-value="updateFilter(filterSet.name, $event)"
39
+ />
40
+ </div>
41
+ </div>
42
+ </AposContextMenu>
43
+ </template>
44
+
45
+ <script setup>
46
+ import {
47
+ computed, inject, ref, watch
48
+ } from 'vue';
49
+
50
+ const $t = inject('i18n');
51
+
52
+ const COMPONENT_MAP = {
53
+ select: 'AposInputSelect',
54
+ radio: 'AposInputSelect',
55
+ combo: 'AposRecentlyEditedCombo'
56
+ };
57
+
58
+ const props = defineProps({
59
+ filters: {
60
+ type: Array,
61
+ default: () => []
62
+ },
63
+ filterChoices: {
64
+ type: Object,
65
+ default: () => ({})
66
+ },
67
+ modelValue: {
68
+ type: Object,
69
+ default: () => ({})
70
+ }
71
+ });
72
+
73
+ const emit = defineEmits([ 'update:modelValue' ]);
74
+
75
+ const SORTED_FILTERS = new Set([ '_editedBy', '_docType' ]);
76
+
77
+ const currentUser = computed(() => apos?.login?.user || apos?.user || null);
78
+
79
+ const button = computed(() => ({
80
+ label: 'apostrophe:filter',
81
+ icon: 'tune-icon',
82
+ modifiers: [ 'small' ],
83
+ type: 'outline'
84
+ }));
85
+
86
+ // Per-filter generation: only remount the specific filter whose
87
+ // choices changed, rather than fingerprinting all choices at once.
88
+ const generationMap = ref({});
89
+
90
+ function choicesChanged(a, b) {
91
+ if (!Array.isArray(a) || !Array.isArray(b)) {
92
+ return a !== b;
93
+ }
94
+ if (a.length !== b.length) {
95
+ return true;
96
+ }
97
+ return a.some((choice, i) => choice.value !== b[i]?.value);
98
+ }
99
+
100
+ watch(() => props.filterChoices, (next, prev) => {
101
+ for (const name of Object.keys(next || {})) {
102
+ if (choicesChanged(next[name], prev?.[name])) {
103
+ generationMap.value[name] = (generationMap.value[name] || 0) + 1;
104
+ }
105
+ }
106
+ }, { deep: true });
107
+
108
+ // AposFilterMenu-style filterSets: creates fresh { data: value }
109
+ // wrappers per evaluation — no persistent reactive objects needed.
110
+ const filterSets = computed(() => {
111
+ return props.filters.map(filter => ({
112
+ name: filter.name,
113
+ key: `${generationMap.value[filter.name] || 0}:${filter.name}`,
114
+ field: {
115
+ name: filter.name,
116
+ type: isCheckboxFilter(filter) ? 'combo' : 'select',
117
+ label: filter.label,
118
+ choices: getChoices(filter),
119
+ def: filter.def
120
+ },
121
+ addLabel: isCheckboxFilter(filter) ? 'apostrophe:recentlyEditedAddFilter' : undefined,
122
+ value: {
123
+ data: props.modelValue[filter.name] ?? filterDefault(filter)
124
+ },
125
+ status: {}
126
+ }));
127
+ });
128
+
129
+ function getChoices(filter) {
130
+ const choices = props.filterChoices[filter.name];
131
+ if (!choices) {
132
+ // Combo renders nothing when choices are empty.
133
+ if (isCheckboxFilter(filter)) {
134
+ return [];
135
+ }
136
+ return [
137
+ {
138
+ label: 'apostrophe:filterMenuLoadingChoices',
139
+ value: null
140
+ }
141
+ ];
142
+ }
143
+
144
+ const mappedChoices = choices.map(choice => ({
145
+ ...choice,
146
+ label: formatChoiceLabel(filter.name, choice)
147
+ }));
148
+
149
+ // Sort dynamic-choices filters by translated label so the order
150
+ // is meaningful regardless of i18n key names. Static-choices
151
+ // filters (_status, _action, etc.) keep their intentional order.
152
+ if (SORTED_FILTERS.has(filter.name)) {
153
+ const currentUserId = currentUser.value?._id;
154
+ mappedChoices.sort((a, b) => {
155
+ if (a.value === null) {
156
+ return -1;
157
+ }
158
+ if (b.value === null) {
159
+ return 1;
160
+ }
161
+ // Pin current user right after "Any" for _editedBy.
162
+ if (filter.name === '_editedBy' && currentUserId) {
163
+ if (String(a.value) === String(currentUserId)) {
164
+ return -1;
165
+ }
166
+ if (String(b.value) === String(currentUserId)) {
167
+ return 1;
168
+ }
169
+ }
170
+ return String(a.label).localeCompare(String(b.label));
171
+ });
172
+ }
173
+
174
+ if (isCheckboxFilter(filter)) {
175
+ return mappedChoices;
176
+ }
177
+
178
+ if (mappedChoices.find(choice => choice.value === null)) {
179
+ return mappedChoices;
180
+ }
181
+
182
+ return [
183
+ {
184
+ label: 'apostrophe:any',
185
+ value: null
186
+ },
187
+ ...mappedChoices
188
+ ];
189
+ }
190
+
191
+ function formatChoiceLabel(name, choice) {
192
+ if (
193
+ name === '_editedBy' &&
194
+ choice?.value &&
195
+ currentUser.value?._id &&
196
+ String(choice.value) === String(currentUser.value._id)
197
+ ) {
198
+ const currentUserLabel = currentUser.value.title || currentUser.value.username;
199
+ return $t('apostrophe:recentlyEditedCurrentUser', {
200
+ user: currentUserLabel
201
+ });
202
+ }
203
+
204
+ if (choice.label && typeof choice.label === 'object') {
205
+ return $t(choice.label);
206
+ }
207
+
208
+ if (typeof choice.label === 'string' && choice.label.length) {
209
+ return $t(choice.label);
210
+ }
211
+
212
+ return choice.label;
213
+ }
214
+
215
+ function updateFilter(name, value) {
216
+ emit('update:modelValue', {
217
+ ...props.modelValue,
218
+ [name]: value.data
219
+ });
220
+ }
221
+
222
+ function clearFilters() {
223
+ emit('update:modelValue', props.filters.reduce((acc, filter) => {
224
+ acc[filter.name] = filterDefault(filter);
225
+ return acc;
226
+ }, {}));
227
+ }
228
+
229
+ function isCheckboxFilter(filter) {
230
+ return (filter.inputType || 'select') === 'checkbox';
231
+ }
232
+
233
+ function filterDefault(filter) {
234
+ if (filter.def !== undefined) {
235
+ return filter.def;
236
+ }
237
+ return isCheckboxFilter(filter) ? [] : null;
238
+ }
239
+ </script>
240
+
241
+ <style lang="scss" scoped>
242
+ .apos-recently-edited-filters {
243
+ width: 340px;
244
+ padding: 10px 14px 12px;
245
+ }
246
+
247
+ .apos-recently-edited-filters__header {
248
+ display: flex;
249
+ align-items: center;
250
+ justify-content: space-between;
251
+ margin-bottom: 8px;
252
+ padding-bottom: 8px;
253
+ border-bottom: 1px solid var(--a-base-9);
254
+ }
255
+
256
+ .apos-recently-edited-filters__title {
257
+ @include type-large;
258
+
259
+ & {
260
+ margin: 0;
261
+ font-weight: var(--a-weight-bold);
262
+ }
263
+ }
264
+
265
+ .apos-recently-edited-filters__clear {
266
+ @include type-base;
267
+
268
+ & {
269
+ border: 0;
270
+ color: var(--a-primary);
271
+ background: transparent;
272
+ cursor: pointer;
273
+ }
274
+ }
275
+
276
+ .apos-recently-edited-filters__row {
277
+ margin-bottom: 6px;
278
+ }
279
+
280
+ .apos-recently-edited-filters__row:last-child {
281
+ margin-bottom: 0;
282
+ }
283
+
284
+ .apos-recently-edited-filters :deep(.apos-field--inline .apos-input-wrapper) {
285
+ flex: 0 0 60%;
286
+ }
287
+ </style>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <AposButton
3
+ icon="recently-edited-icon"
4
+ type="subtle"
5
+ data-apos-test="recently-edited-icon"
6
+ :modifiers="['small', 'no-motion']"
7
+ :tooltip="$t('apostrophe:recentlyEditedDocuments')"
8
+ :icon-only="true"
9
+ @click="open"
10
+ />
11
+ </template>
12
+ <script setup>
13
+ function open() {
14
+ apos.bus.$emit('admin-menu-click', '@apostrophecms/recently-edited:manager');
15
+ }
16
+ </script>
@@ -0,0 +1,346 @@
1
+ <template>
2
+ <AposModal
3
+ :modal="modal"
4
+ :modal-title="{ key: 'apostrophe:recentlyEditedDocuments' }"
5
+ class="apos-recently-edited-manager"
6
+ data-apos-test="recently-edited-manager"
7
+ @esc="close"
8
+ @inactive="modal.active = false"
9
+ @show-modal="modal.showModal = true"
10
+ >
11
+ <template #secondaryControls>
12
+ <AposButton
13
+ type="default"
14
+ label="apostrophe:exit"
15
+ @click="close"
16
+ />
17
+ </template>
18
+ <template #main>
19
+ <AposModalBody>
20
+ <template #bodyHeader>
21
+ <AposDocsManagerToolbar
22
+ ref="toolbarRef"
23
+ :selected-state="selectAllState"
24
+ :total-pages="0"
25
+ :current-page="currentPage"
26
+ :labels="moduleLabels"
27
+ :displayed-items="items.length"
28
+ :checked-count="checked.length"
29
+ :batch-operations="batchOperations"
30
+ :filter-values="batchFilterValues"
31
+ :module-name="moduleName"
32
+ :search-placeholder="$t('apostrophe:recentlyEditedSearchDocuments')"
33
+ :options="{ noPager: true }"
34
+ @search="onSearch"
35
+ @select-click="selectAll"
36
+ @batch="handleBatchAction"
37
+ />
38
+ <AposDocsManagerSelectBox
39
+ v-if="batchOperations.length"
40
+ :selected-state="selectAllState"
41
+ :module-labels="moduleLabels"
42
+ :checked-ids="checked"
43
+ :all-pieces-selection="allPiecesSelection"
44
+ :displayed-items="items.length"
45
+ @select-all="selectAllPieces"
46
+ @set-all-pieces-selection="setAllPiecesSelection"
47
+ />
48
+ <div class="apos-recently-edited__filters-bar">
49
+ <div
50
+ v-if="isLoading"
51
+ class="apos-recently-edited__loading-spinner-container"
52
+ data-apos-test="recently-edited-spinner"
53
+ >
54
+ <AposSpinner class="apos-recently-edited__spinner" />
55
+ </div>
56
+ <div
57
+ v-if="activeFilterTags.length"
58
+ class="apos-recently-edited__active-filters"
59
+ >
60
+ <AposRecentlyEditedFilterTag
61
+ v-for="tag in activeFilterTags"
62
+ :key="`${tag.name}:${tag.value ?? 'none'}`"
63
+ :tag="tag"
64
+ @clear="clearFilter"
65
+ />
66
+ </div>
67
+ <AposRecentlyEditedFilters
68
+ :filters="managerFilters"
69
+ :filter-choices="filterChoices"
70
+ :model-value="filterState"
71
+ @update:model-value="updateFilters"
72
+ />
73
+ </div>
74
+ </template>
75
+ <template #bodyMain>
76
+ <AposDocsManagerDisplay
77
+ v-if="items.length > 0"
78
+ v-model:checked="checked"
79
+ :items="items"
80
+ :headers="headers"
81
+ :options="displayOptions"
82
+ @open="editDoc"
83
+ />
84
+ <div
85
+ v-else-if="!isLoading"
86
+ class="apos-pieces-manager__empty"
87
+ data-apos-test="recently-edited-empty"
88
+ >
89
+ <AposEmptyState :empty-state="emptyDisplay" />
90
+ <div
91
+ v-if="hasActiveSearch || hasActiveFilters"
92
+ class="apos-recently-edited__clear-actions"
93
+ >
94
+ <button
95
+ v-if="hasActiveSearch"
96
+ type="button"
97
+ class="apos-recently-edited__clear-all"
98
+ data-apos-test="recently-edited-clear-search"
99
+ @click="handleClearSearch"
100
+ >
101
+ {{ $t('apostrophe:recentlyEditedClearSearch') }}
102
+ </button>
103
+ <button
104
+ v-if="hasActiveFilters"
105
+ type="button"
106
+ class="apos-recently-edited__clear-all"
107
+ data-apos-test="recently-edited-clear-filters"
108
+ @click="clearAllFilters"
109
+ >
110
+ {{ $t('apostrophe:recentlyEditedClearAllFilters') }}
111
+ </button>
112
+ </div>
113
+ </div>
114
+ <div
115
+ ref="scrollSentinel"
116
+ class="apos-recently-edited__sentinel"
117
+ data-apos-test="recently-edited-sentinel"
118
+ />
119
+ <div
120
+ v-if="isLoadingMore"
121
+ class="apos-recently-edited__loading"
122
+ data-apos-test="recently-edited-loading-more"
123
+ >
124
+ {{ $t('apostrophe:loadingMore') }}
125
+ </div>
126
+ </template>
127
+ </AposModalBody>
128
+ </template>
129
+ </AposModal>
130
+ </template>
131
+
132
+ <script setup>
133
+ import {
134
+ computed, nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch
135
+ } from 'vue';
136
+ import { useInfiniteScroll } from 'Modules/@apostrophecms/ui/composables/useInfiniteScroll.js';
137
+ import { useRecentlyEditedData } from '../composables/useRecentlyEditedData.js';
138
+
139
+ const props = defineProps({
140
+ moduleName: {
141
+ type: String,
142
+ required: true
143
+ },
144
+ initialFilters: {
145
+ type: Object,
146
+ default: () => ({})
147
+ }
148
+ });
149
+
150
+ const modal = ref({
151
+ active: false,
152
+ showModal: false,
153
+ type: 'overlay'
154
+ });
155
+
156
+ const scrollSentinel = ref(null);
157
+ const toolbarRef = ref(null);
158
+
159
+ const {
160
+ moduleOptions,
161
+ managerFilters,
162
+ moduleLabels,
163
+ emptyDisplay,
164
+ items,
165
+ currentPage,
166
+ isLoading,
167
+ isLoadingMore,
168
+ filterChoices,
169
+ filterState,
170
+ activeFilterTags,
171
+ crossLocale,
172
+ reload,
173
+ loadMore,
174
+ updateFilters,
175
+ clearFilter,
176
+ clearAllFilters,
177
+ hasActiveFilters,
178
+ hasActiveSearch,
179
+ clearSearch,
180
+ onContentChanged,
181
+ onSearch,
182
+ cancel,
183
+ batchOperations,
184
+ batchFilterValues,
185
+ checked,
186
+ allPiecesSelection,
187
+ selectAllState,
188
+ selectAll,
189
+ selectAllPieces,
190
+ setAllPiecesSelection,
191
+ handleBatchAction
192
+ } = useRecentlyEditedData(props.moduleName, props.initialFilters);
193
+
194
+ const {
195
+ start: startScroll, stop: stopScroll, recheck
196
+ } = useInfiniteScroll(
197
+ scrollSentinel,
198
+ loadMore,
199
+ {
200
+ rootMargin: '100px',
201
+ // Use the modal's actual scroll container, not the viewport.
202
+ root: '.apos-modal__main'
203
+ }
204
+ );
205
+
206
+ // After items change (page-1 reload or loadMore append), re-check
207
+ // sentinel visibility. Without this, IntersectionObserver won't fire
208
+ // again if the sentinel remained visible (e.g. low perPage and tall viewport).
209
+ watch(items, () => {
210
+ nextTick(() => recheck());
211
+ });
212
+
213
+ const displayOptions = computed(() => ({
214
+ ...moduleOptions.value,
215
+ crossLocale: crossLocale.value,
216
+ batchOperations: batchOperations.value
217
+ }));
218
+
219
+ const headers = moduleOptions.value.columns || [];
220
+
221
+ async function editDoc(item) {
222
+ if (!item._edit) {
223
+ return;
224
+ }
225
+ const docModuleName = item.slug?.startsWith('/')
226
+ ? '@apostrophecms/page'
227
+ : item.type;
228
+ if (!apos.modules[docModuleName]) {
229
+ return;
230
+ }
231
+ const editorComponent = apos.modules[docModuleName]?.components?.editorModal || 'AposDocEditor';
232
+ const docLocale = item.aposLocale?.split(':')[0];
233
+
234
+ await apos.modal.execute(editorComponent, {
235
+ moduleName: docModuleName,
236
+ docId: item._id,
237
+ locale: docLocale
238
+ });
239
+ }
240
+
241
+ function handleClearSearch() {
242
+ clearSearch();
243
+ if (toolbarRef.value) {
244
+ toolbarRef.value.searchField.value = { data: '' };
245
+ }
246
+ }
247
+
248
+ async function close() {
249
+ modal.value.showModal = false;
250
+ }
251
+
252
+ onMounted(async () => {
253
+ modal.value.active = true;
254
+ await reload({ immediate: true });
255
+ startScroll();
256
+ apos.bus.$on('content-changed', onContentChanged);
257
+ apos.bus.$on('command-menu-manager-close', close);
258
+ });
259
+
260
+ onBeforeUnmount(() => {
261
+ cancel();
262
+ stopScroll();
263
+ });
264
+
265
+ onUnmounted(() => {
266
+ apos.bus.$off('content-changed', onContentChanged);
267
+ apos.bus.$off('command-menu-manager-close', close);
268
+ });
269
+ </script>
270
+
271
+ <style lang="scss" scoped>
272
+
273
+ .apos-pieces-manager__empty {
274
+ display: flex;
275
+ flex-direction: column;
276
+ align-items: center;
277
+ justify-content: center;
278
+ width: 100%;
279
+ height: 100%;
280
+ }
281
+
282
+ .apos-recently-edited__sentinel {
283
+ height: 1px;
284
+ }
285
+
286
+ .apos-recently-edited__loading {
287
+ padding: 10px 20px;
288
+ color: var(--a-base-4);
289
+ font-size: var(--a-type-small);
290
+ text-align: center;
291
+ }
292
+
293
+ .apos-recently-edited__filters-bar {
294
+ display: flex;
295
+ flex-wrap: wrap;
296
+ align-items: center;
297
+ justify-content: flex-end;
298
+ gap: 8px;
299
+ padding-top: 12px;
300
+ }
301
+
302
+ .apos-recently-edited__loading-spinner-container {
303
+ z-index: $z-index-admin-bar;
304
+ position: absolute;
305
+ top: 0;
306
+ left: 0;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ width: 100%;
311
+ height: 100%;
312
+ transition: opacity 300ms ease;
313
+ background-color: var(--a-background-overlay);
314
+ }
315
+
316
+ .apos-recently-edited__active-filters {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ justify-content: flex-end;
320
+ gap: 8px;
321
+ }
322
+
323
+ .apos-recently-edited__clear-actions {
324
+ display: flex;
325
+ gap: 16px;
326
+ margin-top: 10px;
327
+ }
328
+
329
+ .apos-recently-edited__clear-all {
330
+ @include type-base;
331
+
332
+ & {
333
+ padding: 0;
334
+ border: none;
335
+ color: var(--a-primary);
336
+ background: none;
337
+ cursor: pointer;
338
+ text-decoration: underline;
339
+ }
340
+
341
+ &:hover {
342
+ color: var(--a-primary-hover);
343
+ }
344
+ }
345
+
346
+ </style>