@oxygen-cms/ui 2.0.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-cms/ui",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Various utilities for UI-building in Vue.js",
5
5
  "main": "none",
6
6
  "repository": {
@@ -135,8 +135,8 @@
135
135
 
136
136
  <!-- SLOT: Inline settings (shown between toolbar and editor) -->
137
137
  <slot
138
- name="inline-settings"
139
138
  v-if="!isFullscreen"
139
+ name="inline-settings"
140
140
  :model="model"
141
141
  :is-dirty="isDirty"
142
142
  :server-model="serverModel"
@@ -144,11 +144,12 @@
144
144
  ></slot>
145
145
 
146
146
  <!-- Editor Toolbar -->
147
- <div v-if="!loading" class="has-background-white-bis px-4 py-3 is-flex is-align-items-center" style="border-bottom: 1px solid #dbdbdb; gap: 1rem;">
147
+ <div class="has-background-white-bis px-4 py-3 is-flex is-align-items-center" style="border-bottom: 1px solid #dbdbdb; gap: 1rem;">
148
148
  <b-field class="mb-0">
149
149
  <p class="control">
150
150
  <b-button
151
151
  :type="editorMode === 'code' ? 'is-dark' : ''"
152
+ :disabled="loading"
152
153
  @click="switchEditorMode('code')"
153
154
  >
154
155
  Code
@@ -156,6 +157,7 @@
156
157
  <p class="control">
157
158
  <b-button
158
159
  :type="editorMode === 'split' ? 'is-dark' : ''"
160
+ :disabled="loading"
159
161
  @click="switchEditorMode('split')"
160
162
  >
161
163
  Split
@@ -163,31 +165,32 @@
163
165
  <p class="control">
164
166
  <b-button
165
167
  :type="editorMode === 'preview' ? 'is-dark' : ''"
168
+ :disabled="loading"
166
169
  @click="switchEditorMode('preview')"
167
170
  >
168
171
  Preview
169
172
  </b-button></p>
170
173
  </b-field>
171
174
 
172
- <b-button icon-left="image" :disabled="editorMode === 'preview'" @click="isMediaModalActive = true">
175
+ <b-button icon-left="image" :disabled="loading || editorMode === 'preview'" @click="isMediaModalActive = true">
173
176
  Insert Photo or File
174
177
  </b-button>
175
178
 
176
- <b-switch v-if="hasFullPagePreview" :value="renderLayout" size="is-small" @input="updateQueryParam('fullPage', $event)">
179
+ <b-switch v-if="hasFullPagePreview" :value="renderLayout" :disabled="loading" size="is-small" @input="updateQueryParam('fullPage', $event)">
177
180
  Preview full page
178
181
  </b-switch>
179
182
 
180
183
  <b-switch
181
- :value="isFullscreen" size="is-small" @input="updateQueryParam('fullscreen', $event)"
184
+ :value="isFullscreen" :disabled="loading" size="is-small" @input="updateQueryParam('fullscreen', $event)"
182
185
  >Focus</b-switch>
183
186
 
184
187
  <div class="is-flex-grow-1"></div>
185
188
 
186
189
  <b-field>
187
190
  <p class="control">
188
- <b-dropdown position="is-bottom-left" aria-role="menu">
191
+ <b-dropdown position="is-bottom-left" aria-role="menu" :disabled="loading">
189
192
  <template #trigger>
190
- <b-button :label="versionStrategyLabel" icon-right="caret-down" :disabled="!isDirty" />
193
+ <b-button :label="versionStrategyLabel" icon-right="caret-down" :disabled="loading || !isDirty" />
191
194
  </template>
192
195
  <b-dropdown-item aria-role="menuitem" @click="versionStrategy = 'guess'">
193
196
  Create New Version if Needed
@@ -205,7 +208,7 @@
205
208
  type="is-primary"
206
209
  icon-left="save"
207
210
  :loading="saving"
208
- :disabled="!isDirty"
211
+ :disabled="loading || !isDirty"
209
212
  @click="save"
210
213
  >
211
214
  Save
@@ -444,7 +447,8 @@ export default {
444
447
  previewHtml: '',
445
448
  previewDebounceTimer: null,
446
449
  editingTitle: false,
447
- editingTitleValue: ''
450
+ editingTitleValue: '',
451
+ isLoadingFromServer: false
448
452
  }
449
453
  },
450
454
  computed: {
@@ -534,7 +538,12 @@ export default {
534
538
  watch: {
535
539
  'model.content': {
536
540
  handler(newVal) {
537
- console.log('model.content changed, length:', newVal ? newVal.length : 0, 'mode:', this.editorMode);
541
+ console.log('model.content changed, length:', newVal ? newVal.length : 0, 'mode:', this.editorMode, 'isLoadingFromServer:', this.isLoadingFromServer);
542
+ // Skip refresh if we're loading from server - the query watcher or mounted hook will handle it
543
+ if (this.isLoadingFromServer) {
544
+ console.log('Skipping refresh - loading from server');
545
+ return;
546
+ }
538
547
  if (this.editorMode === 'split' || this.editorMode === 'preview') {
539
548
  this.debouncedRefreshPreview();
540
549
  }
@@ -577,12 +586,19 @@ export default {
577
586
  },
578
587
  async fetchData() {
579
588
  this.loading = true;
589
+ this.isLoadingFromServer = true;
580
590
  try {
581
591
  const response = await this.resourceApi.get(this.resourceId);
582
592
  this.setModel(response.item);
583
593
  this.editingNonHead = !this.isHeadVersion;
594
+ // Trigger initial preview refresh after data loads
595
+ if (this.editorMode === 'split' || this.editorMode === 'preview') {
596
+ this.refreshPreview();
597
+ }
584
598
  } catch (error) {
585
599
  console.error('Failed to fetch resource:', error);
600
+ } finally {
601
+ this.isLoadingFromServer = false;
586
602
  }
587
603
  },
588
604
  setModel(model) {
@@ -1,4 +1,3 @@
1
- import LegacyPage from "../components/LegacyPage.vue";
2
1
  import { WEB_CONTENT } from "../main.js";
3
2
  import PageEdit from "../components/pages/PageEdit.vue";
4
3
  import PartialEdit from "../components/partials/PartialEdit.vue";
@@ -42,17 +41,6 @@ export default function(ui) {
42
41
  const pagesProps = { displayName: 'Pages', routePrefix: 'pages', inTrash: false, tableComponent: PageTable, actionsComponent: PageActions, singularDisplayName: 'Page', defaultSortField: 'title', defaultSortOrder: 'asc', resourceApi: new PagesApi(), createDropdownComponent: CreatePageDropdown }
43
42
 
44
43
  ui.addAuthenticatedRoutes([
45
- {
46
- path: 'pages/:subpath/edit',
47
- component: LegacyPage,
48
- props: (route) => {
49
- return {
50
- fullPath: route.fullPath,
51
- legacyPrefix: '/oxygen/view',
52
- adminPrefix: '/oxygen'
53
- }
54
- }
55
- },
56
44
  {
57
45
  path: 'partials/trash',
58
46
  name: 'partials.trash',
@@ -1,263 +0,0 @@
1
- <template>
2
- <div class="full-height full-height-container legacy-container">
3
- <transition name="fade">
4
- <iframe v-show="!loading" ref="iframe" class="iframe" />
5
- </transition>
6
-
7
- <MediaInsertModal :active.sync="isInsertMediaItemModalActive" @close="closeInsertMediaItemModal" @select="onFilesSelected" />
8
- </div>
9
- </template>
10
-
11
- <script>
12
- import MediaInsertModal from "./media/MediaInsertModal.vue";
13
-
14
- import {getApiHost, initCsrfCookie, morphToNotification} from "../api";
15
- import MediaApi from "../MediaApi";
16
- import {LOGIN_AGAIN_NOTIFICATION} from "../AuthApi";
17
-
18
- // from https://gist.github.com/hdodov/a87c097216718655ead6cf2969b0dcfa
19
-
20
- const iframeURLChange = (iframe, callback, legacyPage) => {
21
- var unloadHandler = function() {
22
- console.log('[LegacyPage] Starting load');
23
- legacyPage.loadingPath = 'unknown';
24
- };
25
-
26
- function attachUnload() {
27
- // Remove the unloadHandler in case it was already attached.
28
- // Otherwise, the change will be dispatched twice.
29
- iframe.contentWindow.removeEventListener("unload", unloadHandler);
30
- iframe.contentWindow.addEventListener("unload", unloadHandler);
31
- }
32
-
33
- iframe.addEventListener("load", attachUnload);
34
- attachUnload();
35
- }
36
-
37
- // This component manages the tricky/hacky integration of two incompatible GUI systems.
38
- // Legacy pages are rendered inside an iframe, and legacy pages can transition between each other using SmoothState.js
39
- // When the iframe location is updated, so is the url of the parent page using this.$router.push()
40
-
41
- export default {
42
- name: "LegacyPage",
43
- components: { MediaInsertModal },
44
- beforeRouteLeave(to, from, next) {
45
- window.document.body.style.overflowY = 'auto';
46
- window.document.documentElement.style.overflowY = 'auto';
47
- this.$parent.$data.requestedCollapsed = false;
48
- next();
49
- },
50
- props: {
51
- fullPath: { type: String, required: true },
52
- legacyPrefix: { type: String, required: true },
53
- adminPrefix: { type: String, required: true }
54
- },
55
- data() {
56
- return {
57
- loadingPath: null,
58
- currentPath: null,
59
- isInsertMediaItemModalActive: false,
60
- resolveInsertMediaItems: null,
61
- rejectInsertMediaItems: null
62
- }
63
- },
64
- computed: {
65
- loading() { return this.loadingPath !== null; },
66
- userPreferences() { return this.$store.getters.userPreferences; }
67
- },
68
- 'watch': {
69
- 'fullPath': 'onFullPathChanged'
70
- },
71
- async mounted() {
72
- this.loadingPath = 'prefs';
73
-
74
- window.document.body.style.overflowY = 'hidden';
75
- window.document.documentElement.style.overflowY = 'hidden';
76
- let iframe = this.$refs.iframe;
77
-
78
- iframeURLChange(iframe, this.onNavigated.bind(this), this);
79
-
80
- this.loadPath(this.$route.fullPath);
81
-
82
- iframe.addEventListener('load', this.onLoaded.bind(this));
83
- if(iframe.contentDocument.readyState === "complete") {
84
- console.warn('[LegacyPage] mounted: page was already loaded - perhaps this page was cached?');
85
- await this.onLoaded();
86
- }
87
- },
88
- unmounted() {
89
- this.$parent.$data.requestedCollapsed = false;
90
- },
91
- methods: {
92
- onFullPathChanged(newFullPath) {
93
- console.log('Route changed', );
94
- this.loadPath(newFullPath);
95
- },
96
- setupIframeIntegrations() {
97
- console.log('[LegacyPage] Setting up iframe integrations for', this.$refs.iframe.contentWindow.location.href);
98
- let elem = this.$refs.iframe;
99
- elem.contentWindow.Oxygen = elem.contentWindow.Oxygen || {};
100
- elem.contentWindow.Oxygen.user = this.userPreferences.preferences;
101
- elem.contentWindow.Oxygen.onNavigationBegin = this.onNavigated.bind(this);
102
- elem.contentWindow.Oxygen.onNavigationEnd = this.onLoaded.bind(this);
103
- elem.contentWindow.Oxygen.notify = this.showInnerNotification.bind(this);
104
- elem.contentWindow.Oxygen.openAlertDialog = this.openAlertDialog.bind(this);
105
- elem.contentWindow.Oxygen.hardRedirect = this.hardRedirect.bind(this);
106
- elem.contentWindow.Oxygen.insertMediaItem = this.openInsertMediaItemModal.bind(this);
107
- elem.contentWindow.Oxygen.openConfirmDialog = this.openConfirmDialog.bind(this);
108
- elem.contentWindow.Oxygen.popState = this.popState.bind(this);
109
- elem.contentWindow.Oxygen.onToggleFullscreen = this.onToggleFullscreen.bind(this);
110
-
111
- if(elem.contentWindow.Oxygen.onLoadedInsideIFrame) {
112
- elem.contentWindow.Oxygen.onLoadedInsideIFrame();
113
- } else {
114
- console.warn('[LegacyPage] no onLoadedInsideIFrame callback set');
115
- }
116
- },
117
- fullURLToVuePath(url) {
118
- let urlObj = new URL(url);
119
- let urlString = urlObj.toString();
120
- if(urlObj.pathname.startsWith(this.legacyPrefix)) {
121
- let loc = urlString.split(this.legacyPrefix)[1];
122
- return { loadInside: 'iframe', location: this.adminPrefix + loc, locationWithoutPrefix: loc };
123
- } else if(urlObj.pathname.startsWith(this.adminPrefix)) {
124
- return { loadInside: 'vue', location: urlString.split(this.adminPrefix)[1] };
125
- } else {
126
- return { loadInside: false, location: urlString };
127
- }
128
- },
129
- vuePathToURL(path) {
130
- return getApiHost() + this.legacyPrefix.replace(/^\//, '') + path;
131
- },
132
- // We detect when the iframe url changes, and update our window accordingly...
133
- onNavigated(newURL) {
134
- console.log('[LegacyPage] Navigated to ' + newURL);
135
- },
136
- showInnerNotification(data) {
137
- this.$buefy.notification.open(morphToNotification(data));
138
- },
139
- openAlertDialog(message) {
140
- this.$buefy.dialog.alert({
141
- message: message,
142
- size: 'is-small'
143
- });
144
- },
145
- openConfirmDialog(options) {
146
- this.$buefy.dialog.confirm(options);
147
- },
148
- popState() {
149
- this.$router.back();
150
- },
151
- async onLoaded() {
152
- let path = this.$refs.iframe.contentWindow.location.href;
153
- if(path === 'about:blank') { return; }
154
- console.log('[LegacyPage] Loaded', path);
155
-
156
- if(path !== this.currentPath) {
157
- let { loadInside, location } = this.fullURLToVuePath(path);
158
- console.log('[LegacyPage] ', 'loadInside:', loadInside, 'location:', location);
159
- if(loadInside === 'iframe') {
160
- window.history.pushState({}, "", location);
161
- } else if(loadInside === 'vue') {
162
- if(location.startsWith('/auth/login?location=')) {
163
- // If the legacy page is redirecting us to login,
164
- // then that must be because our auth expired/failed.
165
- // So we explicitly log ourselves out, and redirect to the login page.
166
- let redirectTo = this.fullURLToVuePath(this.currentPath);
167
- if(redirectTo.loadInside !== 'iframe') { throw new Error("this.currentPath was not inside iframe"); }
168
- console.log("Requested redirect to /auth/login . Clearing user state first", redirectTo);
169
- this.$store.commit('setUser', null);
170
- this.$buefy.notification.open(LOGIN_AGAIN_NOTIFICATION);
171
- await initCsrfCookie();
172
- location = { name: 'login', query: { redirect: redirectTo.locationWithoutPrefix } };
173
- }
174
-
175
- await this.$router.push(location);
176
- return;
177
- } else {
178
- // load outside of iframe
179
- window.location = location;
180
- return;
181
- }
182
- this.currentPath = path;
183
- }
184
-
185
- document.title = this.$refs.iframe.contentDocument.title;
186
- this.setupIframeIntegrations();
187
-
188
- this.loadingPath = null;
189
- },
190
- loadPath(routePath) {
191
- let path = this.vuePathToURL(routePath);
192
- if(path.endsWith('/oxygen/view/auth/login')) {
193
- console.log('[LegacyPage] Need to login again, redirecting...');
194
- window.location.replace('/oxygen/auth/login');
195
- }
196
-
197
- console.log('[LegacyPage] Loading', path);
198
-
199
- this.loadingPath = path;
200
-
201
- let elem = this.$refs.iframe;
202
- if(elem.src !== path) {
203
- // load the page from scratch
204
- elem.src = path;
205
- }
206
- },
207
- hardRedirect(loc) {
208
- window.location.replace(loc);
209
- },
210
- openInsertMediaItemModal() {
211
- this.isInsertMediaItemModalActive = true;
212
- return new Promise((resolve, reject) => {
213
- this.resolveInsertMediaItems = resolve;
214
- this.rejectInsertMediaItems = reject;
215
- });
216
- },
217
- closeInsertMediaItemModal() {
218
- this.rejectInsertMediaItems({ message: 'modal closed' });
219
- this.isInsertMediaItemModalActive = false;
220
- },
221
- onFilesSelected(files) {
222
- let include = files.map(item => MediaApi.generateIncludeStatement(item)).join("\n") + "\n";
223
- let filenames = files.map(item => item.fullPath).join(",");
224
- this.resolveInsertMediaItems(include);
225
- this.isInsertMediaItemModalActive = false;
226
- this.$buefy.toast.open({
227
- message: 'Inserted ' + filenames,
228
- type: 'is-info',
229
- queue: false
230
- });
231
- },
232
- onToggleFullscreen(mode) {
233
- this.$parent.$data.requestedCollapsed = mode;
234
- }
235
- }
236
- }
237
- </script>
238
-
239
- <style scoped lang="scss">
240
- @import './util.css';
241
- @import '../styles/_variables.scss';
242
-
243
- .iframe {
244
- flex: 1;
245
- width: 100%;
246
- }
247
-
248
- .hidden {
249
- transition: opacity 1s ease, visibility 1s ease;
250
- opacity: 0;
251
- visibility: hidden;
252
- }
253
-
254
- .visible {
255
- opacity: 1;
256
- visibility: visible;
257
- }
258
-
259
- .legacy-container {
260
- position: relative;
261
- background-color: $grey-lightest;
262
- }
263
- </style>