@oxygen-cms/ui 1.9.1 → 2.1.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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/CrudApi.js +39 -0
  3. package/src/PagesApi.js +2 -0
  4. package/src/PartialsApi.js +7 -0
  5. package/src/components/ContentResourceEdit.vue +980 -0
  6. package/src/components/{PageEdit.vue → PageEditWysiwyg.vue} +1 -1
  7. package/src/components/ResourceList.vue +8 -3
  8. package/src/components/VersionsDrawer.vue +386 -0
  9. package/src/components/content/PartialNodeView.vue +1 -1
  10. package/src/components/pages/CreatePageDropdown.vue +5 -5
  11. package/src/components/pages/PageActions.vue +67 -0
  12. package/src/components/pages/PageChooseParent.vue +170 -0
  13. package/src/components/pages/PageEdit.vue +149 -0
  14. package/src/components/pages/PageList.vue +1 -1
  15. package/src/components/{PageNestedRow.vue → pages/PageNestedRow.vue} +3 -3
  16. package/src/components/{PageStatusIcon.vue → pages/PageStatusIcon.vue} +1 -1
  17. package/src/components/{PageTable.vue → pages/PageTable.vue} +5 -5
  18. package/src/components/partials/CreatePartialDropdown.vue +1 -1
  19. package/src/components/{PartialActions.vue → partials/PartialActions.vue} +1 -1
  20. package/src/components/partials/PartialEdit.vue +49 -0
  21. package/src/components/{PartialList.vue → partials/PartialList.vue} +3 -3
  22. package/src/components/{PartialStatusIcon.vue → partials/PartialStatusIcon.vue} +1 -1
  23. package/src/components/{PartialTable.vue → partials/PartialTable.vue} +1 -1
  24. package/src/icons.js +11 -2
  25. package/src/modules/PagesPartials.js +10 -27
  26. package/src/components/LegacyPage.vue +0 -263
  27. package/src/components/PageActions.vue +0 -151
  28. /package/src/components/{PageNestedPagination.vue → pages/PageNestedPagination.vue} +0 -0
@@ -101,7 +101,7 @@
101
101
 
102
102
  <script>
103
103
  import ContentEditor from "./content/ContentEditor.vue";
104
- import PagesApi from "../PagesApi";
104
+ import PagesApi from "../PagesApi.js";
105
105
  import CodeEditor from "./CodeEditor.vue";
106
106
  import {morphToNotification} from "../api.js";
107
107
  export default {
@@ -10,8 +10,8 @@
10
10
  </div>
11
11
  <b-input v-model.lazy="searchQuery" rounded :placeholder="'Search ' + displayName" icon="search" icon-pack="fas" class="mr-3"></b-input>
12
12
 
13
- <component v-if="!inTrash"
14
- :is="createDropdownComponent"
13
+ <component :is="createDropdownComponent"
14
+ v-if="!inTrash"
15
15
  class="mr-3"
16
16
  @created="fetchData" />
17
17
 
@@ -25,8 +25,9 @@
25
25
  <component :is="tableComponent" :paginated-items="paginatedItems" :on-page-change="page => paginatedItems.currentPage = page" :detailed="!searchQuery" :on-sort="onSort">
26
26
  <template #actions="slotProps">
27
27
  <div class="buttons" style="min-width: 18rem">
28
- <component :is="actionsComponent" :item="slotProps.row" @update="updateItem" @reload="fetchData"></component>
28
+ <b-button v-if="hasView" rounded icon-left="eye" tag="router-link" :to="{ path: '/' + routePrefix + '/' + slotProps.row.id, query: { fullscreen: 'true', fullPage: 'true', mode: 'preview', versions: 'false' } }" size="is-small">View</b-button>
29
29
  <b-button rounded icon-left="pencil-alt" tag="router-link" :to="'/' + routePrefix + '/' + slotProps.row.id" size="is-small">Edit</b-button>
30
+ <component :is="actionsComponent" :item="slotProps.row" @update="updateItem" @reload="fetchData"></component>
30
31
  <b-button
31
32
  v-if="inTrash" rounded outlined icon-left="recycle"
32
33
  size="is-small" @click="restoreItem(slotProps.row.id)">Restore
@@ -68,6 +69,10 @@ export default {
68
69
  createDropdownComponent: {
69
70
  type: Object,
70
71
  required: true
72
+ },
73
+ hasView: {
74
+ type: Boolean,
75
+ default: true
71
76
  }
72
77
  },
73
78
  data() {
@@ -0,0 +1,386 @@
1
+ <template>
2
+ <transition name="slide-right">
3
+ <div v-if="active" class="versions-drawer">
4
+ <div class="drawer-overlay" @click="close"></div>
5
+ <div class="drawer-content">
6
+ <div class="drawer-header px-4 py-3 has-background-light" style="border-bottom: 1px solid #dbdbdb;">
7
+ <div class="is-flex is-align-items-center">
8
+ <h2 class="title is-5 mb-0 is-flex-grow-1">Version History</h2>
9
+ <b-button icon-left="times" size="is-small" @click="close">Close</b-button>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="drawer-body is-relative">
14
+ <b-loading :active="versionsLoading" :is-full-page="false"></b-loading>
15
+
16
+ <div v-if="!versionsLoading && versions.length === 0" class="has-text-centered py-6 has-text-grey">
17
+ No versions found
18
+ </div>
19
+
20
+ <div v-for="version in versions" :key="version.id" class="version-row px-4 py-3" :class="{ 'is-current': isHeadVersion(version), 'is-editing': version.id === currentVersionId }">
21
+ <div class="is-flex is-align-items-start">
22
+ <div class="is-flex-grow-1">
23
+ <div class="is-flex is-align-items-center mb-1">
24
+ <strong>{{ version.title }}</strong>
25
+ <b-tag v-if="version.id === currentVersionId" type="is-primary" size="is-small" class="ml-2">
26
+ <span class="ml-1">This version</span>
27
+ </b-tag>
28
+ <b-tag v-if="isHeadVersion(version)" type="is-success" size="is-small" class="ml-2" icon="star">
29
+ Latest
30
+ </b-tag>
31
+ </div>
32
+ <div class="is-size-7 has-text-grey">
33
+ <span v-if="version.updatedBy">{{ version.updatedBy.fullName }} • </span>
34
+ {{ formatDateTime(version.updatedAt) }}
35
+ </div>
36
+ </div>
37
+
38
+ <b-field class="is-flex" style="gap: 0.25rem;">
39
+ <p v-if="hasPublish" class="control">
40
+ <b-button
41
+ icon-left="globe-asia"
42
+ size="is-small"
43
+ :disabled="version.stage == publishedStage"
44
+ @click="publishVersion(version)"
45
+ >
46
+ {{ version.stage == publishedStage ? 'Published' : 'Publish' }}
47
+ </b-button></p>
48
+
49
+ <p class="control">
50
+ <b-button icon-left="eye" size="is-small" @click="viewVersion(version)">
51
+ View
52
+ </b-button></p>
53
+
54
+ <p class="control">
55
+ <b-button
56
+ v-if="version.id !== currentVersionId"
57
+ icon-left="pencil-alt"
58
+ size="is-small"
59
+ @click="editVersion(version)"
60
+ >
61
+ Edit
62
+ </b-button></p>
63
+
64
+ <p class="control">
65
+ <b-dropdown
66
+ :disabled="shouldDisableDropdown(version)"
67
+ position="is-bottom-left"
68
+ aria-role="menu">
69
+ <template #trigger>
70
+ <b-button icon-left="bars" size="is-small"></b-button>
71
+ </template>
72
+
73
+ <!-- SLOT: Resource-specific actions (e.g., "View on Site" for pages) -->
74
+ <slot
75
+ name="version-dropdown-actions"
76
+ :version="version"
77
+ :is-published="version.stage === publishedStage"
78
+ ></slot>
79
+
80
+ <b-dropdown-item
81
+ v-if="!isHeadVersion(version)"
82
+ aria-role="menuitem"
83
+ @click="makeHeadVersion(version)"
84
+ >
85
+ <b-icon icon="arrow-up"></b-icon>
86
+ Promote to Latest Version
87
+ </b-dropdown-item>
88
+
89
+ <b-dropdown-item
90
+ v-if="!isHeadVersion(version) && !version.deletedAt"
91
+ aria-role="menuitem"
92
+ @click="deleteVersion(version)"
93
+ >
94
+ <b-icon icon="trash"></b-icon>
95
+ Delete Version
96
+ </b-dropdown-item>
97
+
98
+ <b-dropdown-item
99
+ v-if="version.deletedAt"
100
+ aria-role="menuitem"
101
+ @click="forceDeleteVersion(version)"
102
+ >
103
+ <b-icon icon="trash"></b-icon>
104
+ Delete Forever
105
+ </b-dropdown-item>
106
+
107
+ <b-dropdown-item
108
+ v-if="version.deletedAt"
109
+ aria-role="menuitem"
110
+ @click="restoreVersion(version)"
111
+ >
112
+ <b-icon icon="recycle"></b-icon>
113
+ Restore Version
114
+ </b-dropdown-item>
115
+ </b-dropdown>
116
+ </p>
117
+ </b-field>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </transition>
124
+ </template>
125
+
126
+ <script>
127
+ export default {
128
+ name: "VersionsDrawer",
129
+ props: {
130
+ active: {
131
+ type: Boolean,
132
+ default: false
133
+ },
134
+ resourceId: {
135
+ type: Number,
136
+ required: true
137
+ },
138
+ currentVersionId: {
139
+ type: Number,
140
+ default: null
141
+ },
142
+ resourceApi: {
143
+ type: Object,
144
+ required: true
145
+ },
146
+ publishedStage: {
147
+ type: Number,
148
+ required: false,
149
+ default: null
150
+ },
151
+ hasVersionActions: {
152
+ type: Boolean,
153
+ default: false // e.g., true for pages (View on Site), false for partials
154
+ },
155
+ hasPublish: {
156
+ type: Boolean,
157
+ default: true // Whether this resource type supports publish functionality
158
+ }
159
+ },
160
+ data() {
161
+ return {
162
+ versions: [],
163
+ versionsLoading: false
164
+ }
165
+ },
166
+ watch: {
167
+ active(newVal) {
168
+ if (newVal) {
169
+ this.loadVersions();
170
+ }
171
+ this.$emit('update:active', newVal);
172
+ }
173
+ },
174
+ mounted() {
175
+ // Load versions if drawer is active on initial mount (e.g., from URL query param)
176
+ if (this.active) {
177
+ this.loadVersions();
178
+ }
179
+ },
180
+ methods: {
181
+ shouldDisableDropdown(version) {
182
+ const isHead = this.isHeadVersion(version);
183
+ const isPublished = this.hasPublish && this.publishedStage !== null && version.stage === this.publishedStage;
184
+ const isDeleted = version.deletedAt;
185
+
186
+ // Disable if head version that's not published and not deleted (original behavior, only if has publish)
187
+ if (this.hasPublish && isHead && !isPublished && !isDeleted) {
188
+ return true;
189
+ }
190
+
191
+ // Also disable if no actions will be available
192
+ const hasPromoteAction = !isHead;
193
+ const hasRestoreAction = isDeleted;
194
+ const hasDeleteAction = !isHead && !isDeleted;
195
+ const hasForceDeleteAction = isDeleted;
196
+ const hasSlotActions = this.hasVersionActions && (!this.hasPublish || isPublished);
197
+
198
+ const hasAnyAction = hasPromoteAction || hasRestoreAction || hasDeleteAction || hasForceDeleteAction || hasSlotActions;
199
+
200
+ return !hasAnyAction;
201
+ },
202
+ async loadVersions() {
203
+ this.versionsLoading = true;
204
+ try {
205
+ const response = await this.resourceApi.listVersions(this.resourceId);
206
+ this.versions = response.items || [];
207
+ } catch (error) {
208
+ console.error('Failed to load versions:', error);
209
+ } finally {
210
+ this.versionsLoading = false;
211
+ }
212
+ },
213
+ close() {
214
+ this.$emit('update:active', false);
215
+ },
216
+ isHeadVersion(version) {
217
+ return version.headVersion === null;
218
+ },
219
+ formatDateTime(dateString) {
220
+ const date = new Date(dateString);
221
+ return date.toLocaleString();
222
+ },
223
+ async publishVersion(version) {
224
+ this.$emit('publish', version.id);
225
+ await this.loadVersions();
226
+ },
227
+ viewVersion(version) {
228
+ // Open in fullscreen preview mode with full page layout, close versions drawer
229
+ this.$emit('navigate', version.id, {
230
+ fullscreen: true,
231
+ fullPage: true,
232
+ mode: 'preview',
233
+ versions: false
234
+ });
235
+ },
236
+ editVersion(version) {
237
+ const options = {};
238
+
239
+ // Only force preview mode for non-head versions (historical versions)
240
+ if (!this.isHeadVersion(version)) {
241
+ options.mode = 'preview';
242
+ }
243
+ // For head versions, don't set mode - let the component use user's default preference
244
+
245
+ this.$emit('navigate', version.id, options);
246
+ // Don't close the drawer - the parent will handle navigation with query param
247
+ },
248
+ async makeHeadVersion(version) {
249
+ this.$buefy.dialog.confirm({
250
+ message: `Promote this version to the latest version? The existing 'latest' will become a historical version instead.`,
251
+ confirmText: 'Make Current',
252
+ type: 'is-warning',
253
+ onConfirm: async () => {
254
+ this.$emit('make-head', version.id);
255
+ await this.loadVersions();
256
+ }
257
+ });
258
+ },
259
+ async deleteVersion(version) {
260
+ // Soft delete - no confirmation needed
261
+ try {
262
+ await this.resourceApi.delete(version.id);
263
+ this.$buefy.toast.open({
264
+ message: 'Version deleted successfully',
265
+ type: 'is-success'
266
+ });
267
+ await this.loadVersions();
268
+ } catch (error) {
269
+ console.error('Failed to delete version:', error);
270
+ this.$buefy.toast.open({
271
+ message: 'Failed to delete version',
272
+ type: 'is-danger'
273
+ });
274
+ }
275
+ },
276
+ async forceDeleteVersion(version) {
277
+ // Permanent delete - uses confirmForceDelete which has built-in confirmation
278
+ await this.resourceApi.confirmForceDelete(version.id);
279
+ await this.loadVersions();
280
+ },
281
+ async restoreVersion(version) {
282
+ try {
283
+ await this.resourceApi.restoreAndNotify(version.id);
284
+ await this.loadVersions();
285
+ } catch (error) {
286
+ console.error('Failed to restore version:', error);
287
+ this.$buefy.toast.open({
288
+ message: 'Failed to restore version',
289
+ type: 'is-danger'
290
+ });
291
+ }
292
+ }
293
+ }
294
+ }
295
+ </script>
296
+
297
+ <style scoped lang="scss">
298
+ @import "../styles/_variables.scss";
299
+ .versions-drawer {
300
+ position: fixed;
301
+ top: 0;
302
+ left: 0;
303
+ right: 0;
304
+ bottom: 0;
305
+ z-index: 40;
306
+ pointer-events: none;
307
+ }
308
+
309
+ .drawer-overlay {
310
+ position: absolute;
311
+ top: 0;
312
+ left: 0;
313
+ right: 0;
314
+ bottom: 0;
315
+ background: rgba(0, 0, 0, 0.5);
316
+ pointer-events: all;
317
+ }
318
+
319
+ .drawer-content {
320
+ position: absolute;
321
+ top: 0;
322
+ right: 0;
323
+ bottom: 0;
324
+ width: 700px;
325
+ max-width: 90vw;
326
+ background: white;
327
+ box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
328
+ display: flex;
329
+ flex-direction: column;
330
+ pointer-events: all;
331
+ }
332
+
333
+ .drawer-header {
334
+ flex-shrink: 0;
335
+ }
336
+
337
+ .drawer-body {
338
+ flex: 1;
339
+ overflow-y: auto;
340
+ }
341
+
342
+ .version-row {
343
+ border-bottom: 1px solid #f5f5f5;
344
+ transition: background-color 0.2s;
345
+ }
346
+
347
+ .version-row:hover {
348
+ background-color: #fafafa;
349
+ }
350
+
351
+ .version-row.is-current {
352
+ background-color: $success-light;
353
+ }
354
+
355
+ .version-row.is-current:not(.is-editing) {
356
+ border-left: 4px solid $success;
357
+ padding-left: calc(1rem - 4px);
358
+ }
359
+
360
+ .version-row.is-editing {
361
+ background-color: $info-light;
362
+ border-left: 4px solid $info;
363
+ padding-left: calc(1rem - 4px);
364
+ }
365
+
366
+ /* Slide transition from right */
367
+ .slide-right-enter-active,
368
+ .slide-right-leave-active {
369
+ transition: transform 0.3s ease;
370
+ }
371
+
372
+ .slide-right-enter-active .drawer-overlay,
373
+ .slide-right-leave-active .drawer-overlay {
374
+ transition: opacity 0.3s ease;
375
+ }
376
+
377
+ .slide-right-enter .drawer-content,
378
+ .slide-right-leave-to .drawer-content {
379
+ transform: translateX(100%);
380
+ }
381
+
382
+ .slide-right-enter .drawer-overlay,
383
+ .slide-right-leave-to .drawer-overlay {
384
+ opacity: 0;
385
+ }
386
+ </style>
@@ -88,7 +88,7 @@
88
88
  import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-2'
89
89
  import ContentEditor from "./ContentEditor.vue";
90
90
  import PartialsApi from "../../PartialsApi.js";
91
- import PartialList from "../PartialList.vue";
91
+ import PartialList from "../partials/PartialList.vue";
92
92
 
93
93
  export default {
94
94
  name: "PartialNodeView",
@@ -64,7 +64,7 @@
64
64
  <span style="flex: 1;">
65
65
  <strong>{{ selectedParent.title }}</strong> - {{ selectedParent.slug }}
66
66
  </span>
67
- <a @click.stop="clearSelectedParent" style="color: #f14668; cursor: pointer; margin-left: 0.5rem;">
67
+ <a style="color: #f14668; cursor: pointer; margin-left: 0.5rem;" @click.stop="clearSelectedParent">
68
68
  <b-icon icon="times" size="is-small"></b-icon>
69
69
  </a>
70
70
  </div>
@@ -97,9 +97,6 @@ export default {
97
97
  default: false
98
98
  }
99
99
  },
100
- created() {
101
- this.fetchPages();
102
- },
103
100
  data() {
104
101
  return {
105
102
  title: '',
@@ -125,6 +122,9 @@ export default {
125
122
  watch: {
126
123
  'parentSearchQuery': 'fetchPages'
127
124
  },
125
+ created() {
126
+ this.fetchPages();
127
+ },
128
128
  methods: {
129
129
  slugifyTitle(str) {
130
130
  return slugify(str);
@@ -173,7 +173,7 @@ export default {
173
173
  this.$buefy.toast.open(morphToNotification(response));
174
174
  this.close();
175
175
  this.$emit('created', response.item);
176
- this.$router.push('/pages/' + response.item.id + '/edit');
176
+ this.$router.push('/pages/' + response.item.id);
177
177
  } catch(e) {
178
178
  // Error handled by API layer
179
179
  }
@@ -0,0 +1,67 @@
1
+ <template>
2
+ <div class="page-actions">
3
+ <b-button v-if="item.stage !== STAGE_PUBLISHED" rounded size="is-small" icon-left="globe-asia" class="mr-2" @click="publish">Publish</b-button>
4
+
5
+ <b-button rounded size="is-small" icon-left="folder-open" class="mr-2" @click="isMoveModalActive = true">Move</b-button>
6
+
7
+ <b-modal v-model="isMoveModalActive" has-modal-card trap-focus aria-role="dialog" aria-modal>
8
+ <div class="modal-card choose-parent-modal" style="width: 640px">
9
+ <header class="modal-card-head">
10
+ <p class="modal-card-title">Move "{{ item.title }}"</p>
11
+ <button type="button" class="delete" @click="isMoveModalActive = false"/>
12
+ </header>
13
+ <section class="modal-card-body choose-parent-modal-body">
14
+ <PageChooseParent
15
+ :current-parent-id="item.parent"
16
+ :exclude-page-id="item.id"
17
+ label="Parent Page"
18
+ @select="setParentPage"
19
+ />
20
+ </section>
21
+ <footer class="modal-card-foot is-flex is-justify-content-flex-end">
22
+ <b-button label="Close" @click="isMoveModalActive = false" />
23
+ </footer>
24
+ </div>
25
+ </b-modal>
26
+ </div>
27
+ </template>
28
+
29
+ <script>
30
+ import PagesApi from "../../PagesApi.js";
31
+ import {morphToNotification} from "../../api.js";
32
+ import PageChooseParent from "./PageChooseParent.vue";
33
+
34
+ export default {
35
+ name: "PageActions",
36
+ components: { PageChooseParent },
37
+ props: {
38
+ item: { type: Object, required: true }
39
+ },
40
+ data() {
41
+ return {
42
+ STAGE_PUBLISHED: PagesApi.STAGE_PUBLISHED,
43
+ pagesApi: new PagesApi(),
44
+ isMoveModalActive: false
45
+ }
46
+ },
47
+ methods: {
48
+ async publish() {
49
+ let item = await this.pagesApi.publish(this.item.id);
50
+ this.$emit('update', item);
51
+ },
52
+ async setParentPage(parentPage) {
53
+ let data = await this.pagesApi.update({id: this.item.id, parent: parentPage.id, autoConvertToDraft: 'no', version: false});
54
+ this.$buefy.toast.open(morphToNotification(data));
55
+ this.isMoveModalActive = false;
56
+ this.$emit('reload');
57
+ }
58
+ }
59
+ }
60
+ </script>
61
+
62
+
63
+ <style scoped>
64
+ .choose-parent-modal, .choose-parent-modal-body{
65
+ overflow: visible;
66
+ }
67
+ </style>