@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
@@ -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>
@@ -1,151 +0,0 @@
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-dropdown
6
- ref="moveDropdown"
7
- position="is-top-left"
8
- append-to-body
9
- aria-role="menu"
10
- trap-focus
11
- class="move-page-dropdown"
12
- >
13
- <template #trigger>
14
- <b-button rounded size="is-small" icon-left="folder-open" class="mr-2">Move</b-button>
15
- </template>
16
-
17
- <b-dropdown-item
18
- aria-role="menu-item"
19
- custom
20
- paddingless>
21
- <div class="modal-card" style="width: auto; overflow: visible">
22
- <header class="modal-card-head">
23
- <p class="modal-card-title">Update the parent for "{{ item.title }}"</p>
24
- </header>
25
- <section class="modal-card-body" style="overflow: visible;">
26
- <b-field>
27
- <b-autocomplete
28
- v-model.lazy="movePageSearchQuery"
29
- :disabled="isLoading"
30
- open-on-focus
31
- :data="sortedPagesList"
32
- placeholder="Search for pages..."
33
- clearable
34
- @select="setParentPage">
35
- <template #default="props">
36
- <span :style="getOptionStyle(props.option)">
37
- {{ props.option.title }} - {{ props.option.slug }}
38
- <span v-if="isCurrentParent(props.option)" style="opacity: 0.6;"> (current parent)</span>
39
- </span>
40
- </template>
41
- <template #empty>No results found</template>
42
- </b-autocomplete>
43
- </b-field>
44
- </section>
45
- <footer class="modal-card-foot is-flex">
46
- <div class="is-flex-grow-1"></div>
47
- <b-button
48
- label="Close"
49
- @click="close"/>
50
- </footer>
51
- </div>
52
- </b-dropdown-item>
53
- </b-dropdown>
54
- </div>
55
- </template>
56
-
57
- <script>
58
- import PagesApi from "../PagesApi.js";
59
- import {morphToNotification} from "../api.js";
60
-
61
- export default {
62
- name: "PageActions",
63
- props: {
64
- item: { type: Object, required: true }
65
- },
66
- created() {
67
- this.isLoading = true;
68
- this.fetchData()
69
- },
70
- data() {
71
- return {
72
- STAGE_PUBLISHED: PagesApi.STAGE_PUBLISHED,
73
- pagesApi: new PagesApi(),
74
- movePageSearchQuery: '',
75
- isLoading: false,
76
- pagesList: []
77
- }
78
- },
79
- computed: {
80
- sortedPagesList() {
81
- // Sort with Home page (slug '/') at the top, then alphabetically by title
82
- return [...this.pagesList].sort((a, b) => {
83
- if (a.slug === '/') return -1;
84
- if (b.slug === '/') return 1;
85
- return a.title.localeCompare(b.title);
86
- });
87
- }
88
- },
89
- watch: {
90
- 'movePageSearchQuery': 'fetchData'
91
- },
92
- methods: {
93
- async fetchData() {
94
- let data = await this.pagesApi.list({ inTrash: false, page: 1, q: this.movePageSearchQuery });
95
- this.pagesList = data.items;
96
- this.isLoading = false;
97
- },
98
- async publish() {
99
- let item = await this.pagesApi.publish(this.item.id);
100
- this.$emit('update', item);
101
- },
102
- async setParentPage(parentPage) {
103
- // Don't allow moving to current parent
104
- if (this.isCurrentParent(parentPage)) {
105
- return;
106
- }
107
- let data = await this.pagesApi.update({id: this.item.id, parent: parentPage.id, autoConvertToDraft: 'no', version: false});
108
- this.$buefy.toast.open(morphToNotification(data));
109
- this.$emit('reload');
110
- },
111
- close() {
112
- this.$refs.moveDropdown.toggle();
113
- },
114
- isCurrentParent(page) {
115
- if (!this.item.parent) return false;
116
- // Handle both cases: parent as object or parent as ID
117
- const parentId = typeof this.item.parent === 'object' ? this.item.parent.id : this.item.parent;
118
- return page.id === parentId;
119
- },
120
- getOptionStyle(option) {
121
- let style = '';
122
- if (option.slug === '/') {
123
- style += 'font-weight: bold;';
124
- }
125
- if (this.isCurrentParent(option)) {
126
- style += ' opacity: 0.5; cursor: not-allowed;';
127
- }
128
- return style;
129
- }
130
- }
131
- }
132
- </script>
133
-
134
- <style scoped>
135
- .modal-card-title {
136
- flex-shrink: unset;
137
- flex-grow: unset;
138
- }
139
- </style>
140
-
141
- <style>
142
- .move-page-dropdown .dropdown-content {
143
- padding-top: 0;
144
- padding-bottom: 0;
145
- }
146
-
147
- .move-page-dropdown .dropdown-menu {
148
- overflow: visible !important;
149
- min-width: 20rem;
150
- }
151
- </style>