@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.
- package/package.json +1 -1
- package/src/CrudApi.js +39 -0
- package/src/PagesApi.js +2 -0
- package/src/PartialsApi.js +7 -0
- package/src/components/ContentResourceEdit.vue +980 -0
- package/src/components/{PageEdit.vue → PageEditWysiwyg.vue} +1 -1
- package/src/components/ResourceList.vue +8 -3
- package/src/components/VersionsDrawer.vue +386 -0
- package/src/components/content/PartialNodeView.vue +1 -1
- package/src/components/pages/CreatePageDropdown.vue +5 -5
- package/src/components/pages/PageActions.vue +67 -0
- package/src/components/pages/PageChooseParent.vue +170 -0
- package/src/components/pages/PageEdit.vue +149 -0
- package/src/components/pages/PageList.vue +1 -1
- package/src/components/{PageNestedRow.vue → pages/PageNestedRow.vue} +3 -3
- package/src/components/{PageStatusIcon.vue → pages/PageStatusIcon.vue} +1 -1
- package/src/components/{PageTable.vue → pages/PageTable.vue} +5 -5
- package/src/components/partials/CreatePartialDropdown.vue +1 -1
- package/src/components/{PartialActions.vue → partials/PartialActions.vue} +1 -1
- package/src/components/partials/PartialEdit.vue +49 -0
- package/src/components/{PartialList.vue → partials/PartialList.vue} +3 -3
- package/src/components/{PartialStatusIcon.vue → partials/PartialStatusIcon.vue} +1 -1
- package/src/components/{PartialTable.vue → partials/PartialTable.vue} +1 -1
- package/src/icons.js +11 -2
- package/src/modules/PagesPartials.js +10 -27
- package/src/components/LegacyPage.vue +0 -263
- package/src/components/PageActions.vue +0 -151
- /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>
|
|
File without changes
|