@oxygen-cms/ui 1.5.0 → 1.6.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/.eslintrc.js +1 -0
- package/.github/workflows/node.js.yml +1 -1
- package/package.json +2 -2
- package/src/UserPermissions.js +2 -1
- package/src/UserPreferences.test.js +3 -9
- package/src/api.js +6 -2
- package/src/components/AuthenticationLog.vue +3 -5
- package/src/components/ImportExport.vue +5 -6
- package/src/components/LegacyPage.vue +3 -3
- package/src/components/MainMenu.vue +99 -0
- package/src/components/auth/Login.vue +2 -2
- package/src/components/dashboard/EventsPanel.vue +20 -0
- package/src/components/dashboard/MainDashboard.vue +88 -0
- package/src/components/dashboard/MediaPanel.vue +21 -0
- package/src/components/dashboard/PagesPartialsPanel.vue +24 -0
- package/src/components/dashboard/panel.scss +8 -0
- package/src/components/preferences/Preferences.vue +82 -0
- package/src/icons.js +3 -2
- package/src/main.js +20 -3
- package/src/modules/LegacyPages.js +34 -0
- package/src/modules/Media.js +15 -0
- package/src/styles/_variables.scss +0 -1
- package/src/styles/app.scss +1 -1
package/.eslintrc.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxygen-cms/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Various utilities for UI-building in Vue.js",
|
|
5
5
|
"main": "none",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"eslint-plugin-jest": "^24.4.0",
|
|
49
49
|
"eslint-plugin-vue": "^7.17.0",
|
|
50
50
|
"jest": "^26.6.3",
|
|
51
|
-
"
|
|
51
|
+
"sass-embedded": "^1.49.9",
|
|
52
52
|
"postcss-loader": "^3.0.0",
|
|
53
53
|
"regenerator-runtime": "^0.13.7",
|
|
54
54
|
"sass-loader": "^8.0.2",
|
package/src/UserPermissions.js
CHANGED
|
@@ -23,10 +23,11 @@ export default class UserPermissions {
|
|
|
23
23
|
let keyParts = key.split('.');
|
|
24
24
|
|
|
25
25
|
if(keyParts.length !== 2) {
|
|
26
|
-
throw new Error('TreePermissionsSystem Requires a Dot-Seperated Permissions Key');
|
|
26
|
+
throw new Error('TreePermissionsSystem Requires a Dot-Seperated Permissions Key: ' + key);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// check for the specific key
|
|
30
|
+
// console.log(key + " = " + result);
|
|
30
31
|
return this.hasKey(keyParts[0], keyParts[1]);
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -4,17 +4,11 @@ jest.mock('./AuthApi');
|
|
|
4
4
|
|
|
5
5
|
test('gets and sets preferences', async () => {
|
|
6
6
|
UserPreferences.setBuefy({});
|
|
7
|
-
UserPreferences
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
foo: 'bar',
|
|
11
|
-
baz: { qux: 'fub '}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
7
|
+
let prefs = new UserPreferences({
|
|
8
|
+
foo: 'bar',
|
|
9
|
+
baz: { qux: 'fub '}
|
|
14
10
|
});
|
|
15
11
|
|
|
16
|
-
let prefs = await UserPreferences.load();
|
|
17
|
-
|
|
18
12
|
expect(prefs.get('foo')).toBe('bar');
|
|
19
13
|
expect(prefs.has('fob')).toBe(false);
|
|
20
14
|
expect(prefs.has('baz.qux')).toBe(true);
|
package/src/api.js
CHANGED
|
@@ -57,7 +57,7 @@ export class FetchBuilder {
|
|
|
57
57
|
this.headers.set('X-XSRF-TOKEN', xsrfToken);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async
|
|
60
|
+
async fetchRaw(url) {
|
|
61
61
|
await this.setXsrfTokenHeader()
|
|
62
62
|
|
|
63
63
|
let v = { ... this};
|
|
@@ -72,7 +72,11 @@ export class FetchBuilder {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
return await window.fetch(url.toString(), this);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async fetch(url) {
|
|
79
|
+
let response = await this.fetchRaw(url);
|
|
76
80
|
|
|
77
81
|
let data = {};
|
|
78
82
|
try {
|
|
@@ -132,8 +132,7 @@ export default {
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
async fetchData() {
|
|
135
|
-
this.fetchSessions();
|
|
136
|
-
this.fetchLogins();
|
|
135
|
+
await Promise.all([this.fetchSessions(), this.fetchLogins()]);
|
|
137
136
|
},
|
|
138
137
|
geoIP(ip) {
|
|
139
138
|
if(this.ipInfo.has(ip)) {
|
|
@@ -154,12 +153,12 @@ export default {
|
|
|
154
153
|
},
|
|
155
154
|
updateInfoForIp(ip) {
|
|
156
155
|
let geolocationInfo = this.ipInfo.has(ip) ? this.getGeolocationInfo(this.ipInfo.get(ip)) : '';
|
|
157
|
-
for(let item of this.paginatedItems.items) {
|
|
156
|
+
for(let item of (this.paginatedItems.items || [])) {
|
|
158
157
|
if(item.ipAddress === ip) {
|
|
159
158
|
item.geolocationInfo = geolocationInfo;
|
|
160
159
|
}
|
|
161
160
|
}
|
|
162
|
-
for(let item of this.sessions.items) {
|
|
161
|
+
for(let item of (this.sessions.items || [])) {
|
|
163
162
|
if(item.ipAddress === ip) {
|
|
164
163
|
item.geolocationInfo = geolocationInfo;
|
|
165
164
|
}
|
|
@@ -169,7 +168,6 @@ export default {
|
|
|
169
168
|
let ua = new UAParser(userAgent);
|
|
170
169
|
let browser = ua.getBrowser();
|
|
171
170
|
let device = ua.getDevice();
|
|
172
|
-
// console.log(device);
|
|
173
171
|
return browser.name + ' ' + browser.version + ' on ' + ua.getOS().name + ', ' + (device.vendor ? device.vendor : '(unknown device)');
|
|
174
172
|
},
|
|
175
173
|
getGeolocationInfo(data) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<script>
|
|
16
16
|
import {FetchBuilder} from "../api";
|
|
17
17
|
import {API_ROOT} from "../CrudApi";
|
|
18
|
+
import download from "downloadjs";
|
|
18
19
|
|
|
19
20
|
export default {
|
|
20
21
|
name: "ImportExport",
|
|
@@ -26,15 +27,13 @@ export default {
|
|
|
26
27
|
methods: {
|
|
27
28
|
async processDownload() {
|
|
28
29
|
this.exporting = true;
|
|
29
|
-
let
|
|
30
|
-
.cookies()
|
|
31
|
-
|
|
32
|
-
let response = await window.fetch(API_ROOT + "import-export/export", { ... builder });
|
|
30
|
+
let response = await (new FetchBuilder(this.$buefy, 'post'))
|
|
31
|
+
.cookies()
|
|
32
|
+
.fetchRaw(API_ROOT + "import-export/export");
|
|
33
33
|
this.$buefy.notification.open({ message: 'Export successful', type: 'is-success', queue: false });
|
|
34
34
|
let blob = await response.blob();
|
|
35
|
-
|
|
35
|
+
download(blob, 'database ' + (new Date()).toLocaleString() + '.zip');
|
|
36
36
|
this.exporting = false;
|
|
37
|
-
window.location.assign(file);
|
|
38
37
|
}
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -60,9 +60,6 @@ export default {
|
|
|
60
60
|
legacyPrefix: { type: String, required: true },
|
|
61
61
|
adminPrefix: { type: String, required: true }
|
|
62
62
|
},
|
|
63
|
-
'watch': {
|
|
64
|
-
'fullPath': 'onFullPathChanged'
|
|
65
|
-
},
|
|
66
63
|
data() {
|
|
67
64
|
return {
|
|
68
65
|
loadingPath: null,
|
|
@@ -76,6 +73,9 @@ export default {
|
|
|
76
73
|
loading() { return this.loadingPath !== null; },
|
|
77
74
|
userPreferences() { return this.$store.getters.userPreferences; }
|
|
78
75
|
},
|
|
76
|
+
'watch': {
|
|
77
|
+
'fullPath': 'onFullPathChanged'
|
|
78
|
+
},
|
|
79
79
|
async mounted() {
|
|
80
80
|
this.loadingPath = 'prefs';
|
|
81
81
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="userPermissions">
|
|
3
|
+
<b-menu-list>
|
|
4
|
+
<b-menu-item icon="home" tag="router-link" to="/dashboard" label="Dashboard"></b-menu-item>
|
|
5
|
+
</b-menu-list>
|
|
6
|
+
|
|
7
|
+
<b-menu-list v-for="(category, label) in categoriesWithPermission(items)"
|
|
8
|
+
:key="label"
|
|
9
|
+
:label="label">
|
|
10
|
+
<b-menu-item v-for="(group, groupLabel) in groupsWithPermission(category)"
|
|
11
|
+
:key="groupLabel"
|
|
12
|
+
:icon="group.icon"
|
|
13
|
+
:tag="userPermissions.has(group.listPermission) ? 'router-link' : null"
|
|
14
|
+
:expanded="group.groupPrefix ? $route.fullPath.startsWith(group.groupPrefix) : null"
|
|
15
|
+
:to="group.listAction">
|
|
16
|
+
<template #label>
|
|
17
|
+
{{ groupLabel }}
|
|
18
|
+
<b-button v-if="userPermissions.has(group.addPermission)"
|
|
19
|
+
tag="router-link"
|
|
20
|
+
type="is-text"
|
|
21
|
+
class="is-pulled-right show-if-active"
|
|
22
|
+
:icon-right="group.addIcon"
|
|
23
|
+
:to="group.addAction"></b-button>
|
|
24
|
+
</template>
|
|
25
|
+
<b-menu-item v-for="(item, itemLabel) in itemsWithPermission(group.items)" :key="itemLabel" :label="itemLabel" tag="router-link" :to="item.to"></b-menu-item>
|
|
26
|
+
</b-menu-item>
|
|
27
|
+
</b-menu-list>
|
|
28
|
+
|
|
29
|
+
<b-menu-list v-if="userPermissions && userPermissions.hasOneOf(['preferences.getValue', 'users.getList', 'groups.getList', 'importExport.getList'])" label="System">
|
|
30
|
+
<b-menu-item v-if="userPermissions && userPermissions.has('preferences.getValue')" icon="cogs" tag="router-link" to="/preferences" label="Preferences"></b-menu-item>
|
|
31
|
+
<b-menu-item v-if="userPermissions && userPermissions.has('users.getList')" icon="users" tag="router-link" to="/users" label="Users and Permissions"></b-menu-item>
|
|
32
|
+
</b-menu-list>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
export default {
|
|
39
|
+
name: "MainMenu",
|
|
40
|
+
props: {
|
|
41
|
+
items: {
|
|
42
|
+
type: Object,
|
|
43
|
+
required: true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
computed: {
|
|
47
|
+
userPermissions() { return this.$store.getters.userPermissions; }
|
|
48
|
+
},
|
|
49
|
+
methods: {
|
|
50
|
+
categoriesWithPermission(categories) {
|
|
51
|
+
return Object.fromEntries(Object.entries(categories).filter(([,category]) => this.userPermissions.hasOneOf(Object.values(category).flatMap((group) => this.getPermissionsForGroup(group))) ));
|
|
52
|
+
},
|
|
53
|
+
getPermissionsForGroup(group) {
|
|
54
|
+
let values = Object.values(group.items).map(s => s.permission);
|
|
55
|
+
if(group.addPermission) { values.push(group.addPermission); }
|
|
56
|
+
if(group.listPermission) { values.push(group.listPermission); }
|
|
57
|
+
return values;
|
|
58
|
+
},
|
|
59
|
+
itemsWithPermission(items) {
|
|
60
|
+
return Object.fromEntries(Object.entries(items).filter(([, item]) => this.userPermissions.has(item.permission)));
|
|
61
|
+
},
|
|
62
|
+
groupsWithPermission(groups) {
|
|
63
|
+
return Object.fromEntries(Object.entries(groups).filter(([, group]) => this.userPermissions.hasOneOf(this.getPermissionsForGroup(group))));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<style scoped lang="scss">
|
|
70
|
+
.show-if-active {
|
|
71
|
+
visibility: hidden;
|
|
72
|
+
opacity: 0;
|
|
73
|
+
padding: 0;
|
|
74
|
+
transition: visibility 0.2s ease, opacity 0.2s ease;
|
|
75
|
+
height: auto;
|
|
76
|
+
|
|
77
|
+
.router-link-active & {
|
|
78
|
+
visibility: visible;
|
|
79
|
+
opacity: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.menu-list li:hover & {
|
|
83
|
+
visibility: visible;
|
|
84
|
+
opacity: 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.left-navigation-container.is-collapsed .show-if-active {
|
|
89
|
+
visibility: hidden !important;
|
|
90
|
+
opacity: 0 !important;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
93
|
+
|
|
94
|
+
<style>
|
|
95
|
+
.show-if-active .icon:first-child:last-child {
|
|
96
|
+
margin-left: 0;
|
|
97
|
+
margin-right: 0;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</b-notification>
|
|
9
9
|
|
|
10
10
|
<b-field label="Username" label-position="inside" :type="!submitting && hasFailedLogin ? 'is-danger' : ''">
|
|
11
|
-
<b-input
|
|
11
|
+
<b-input ref="username" v-model="username" name="username"></b-input>
|
|
12
12
|
</b-field>
|
|
13
13
|
|
|
14
14
|
<b-field label="Password" label-position="inside" :type="!submitting && hasFailedLogin ? 'is-danger' : ''">
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
<br>
|
|
32
32
|
|
|
33
33
|
<b-field key="totpCode" label="2FA Code" label-position="inside" :type="!submitting && hasFailedLogin ? 'is-danger' : ''" :message="!submitting && hasFailedLogin ? 'Incorrect code. Try again.' : ''">
|
|
34
|
-
<b-input v-model="totpCode" name="totpCode" type="number" placeholder="e.g.: 123456" minlength="6" required @keyup.enter.native="submitLogin"
|
|
34
|
+
<b-input ref="totpCode" v-model="totpCode" name="totpCode" type="number" placeholder="e.g.: 123456" minlength="6" required @keyup.enter.native="submitLogin" />
|
|
35
35
|
</b-field>
|
|
36
36
|
|
|
37
37
|
<br>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article>
|
|
3
|
+
<p class="title">Events<b-icon icon="calendar-alt" size="is-medium"></b-icon></p>
|
|
4
|
+
<p class="subtitle">Promotion for upcoming concerts and events</p>
|
|
5
|
+
<div class="buttons">
|
|
6
|
+
<b-button tag="router-link" to="/upcoming-events" icon-left="list">Manage Events</b-button>
|
|
7
|
+
<b-button tag="router-link" to="/upcoming-events/create" icon-left="plus" type="is-success">Create New Event</b-button>
|
|
8
|
+
</div>
|
|
9
|
+
</article>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
export default {
|
|
14
|
+
name: "EventsPanel"
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<style scoped lang="scss">
|
|
19
|
+
@import './panel.scss';
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="full-height scroll-container">
|
|
3
|
+
<div class="has-background-white-ter">
|
|
4
|
+
<div class="container hero is-medium">
|
|
5
|
+
<div class="hero-body">
|
|
6
|
+
<h1 class="title" style="font-size: 3rem;">
|
|
7
|
+
<transition name="fade">
|
|
8
|
+
<span v-if="user">Welcome, {{ user.fullName }}</span>
|
|
9
|
+
</transition>
|
|
10
|
+
</h1>
|
|
11
|
+
<br>
|
|
12
|
+
<h2 class="subtitle">The Oxygen CMS administration panel allows you to edit the site content, manage events and more.</h2>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="has-background-white">
|
|
18
|
+
<div class="container hero">
|
|
19
|
+
<div class="hero-body">
|
|
20
|
+
<section class="tile is-ancestor">
|
|
21
|
+
<div class="tile is-parent">
|
|
22
|
+
<div class="tile is-child heading-box">
|
|
23
|
+
<h2 class="title">Manage site</h2>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<div class="grid-loading">
|
|
29
|
+
<b-loading :active="!userPermissions" :is-full-page="false"></b-loading>
|
|
30
|
+
<transition name="fade">
|
|
31
|
+
<section v-if="userPermissions" class="tile is-ancestor">
|
|
32
|
+
<div v-for="(group, i) in panels" :key="i" class="tile is-vertical">
|
|
33
|
+
<div v-if="userPermissions && userPermissions.hasOneOf(group.map(panel => panel.permission))" class="tile is-parent is-vertical">
|
|
34
|
+
<div v-for="panel in group" :key="panel.name" class="tile is-child notification">
|
|
35
|
+
<component :is="panel.component"></component>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</section>
|
|
40
|
+
</transition>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<component :is="item" v-for="(item, i) in extraRows" :key="i"></component>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
export default {
|
|
53
|
+
name: "MainDashboard",
|
|
54
|
+
props: {
|
|
55
|
+
panels: {
|
|
56
|
+
type: Array,
|
|
57
|
+
required: true
|
|
58
|
+
},
|
|
59
|
+
extraRows: {
|
|
60
|
+
type: Array,
|
|
61
|
+
default: () => { return []; }
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
data() {
|
|
65
|
+
return {
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
computed: {
|
|
69
|
+
userPermissions() { return this.$store.getters.userPermissions; },
|
|
70
|
+
user() { return this.$store.state.user; }
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.title .icon {
|
|
77
|
+
margin-left: 1rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.grid-loading {
|
|
81
|
+
position: relative;
|
|
82
|
+
min-height: 30rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.notification {
|
|
86
|
+
text-align: center;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article>
|
|
3
|
+
<p class="title">Photos & Files <b-icon icon="photo-video" size="is-medium"></b-icon></p>
|
|
4
|
+
<p class="subtitle">Photos, videos, audio, PDFs.</p>
|
|
5
|
+
<div class="buttons">
|
|
6
|
+
<b-button tag="router-link" to="/media/list" icon-left="list">Manage Photos & Files</b-button>
|
|
7
|
+
<b-button tag="router-link" type="is-success" to="/media/list?upload=true" icon-left="upload">Upload</b-button>
|
|
8
|
+
<b-button tag="router-link" to="/media/responsive-images" type="is-light" icon-left="mail-bulk">Generate Responsive Images</b-button>
|
|
9
|
+
</div>
|
|
10
|
+
</article>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
export default {
|
|
15
|
+
name: "MediaPanel"
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<style scoped lang="scss">
|
|
20
|
+
@import './panel.scss';
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<article>
|
|
3
|
+
<p class="title">Pages<b-icon icon="file-alt" size="is-medium"></b-icon></p>
|
|
4
|
+
<p class="subtitle">Manage the content of the website</p>
|
|
5
|
+
<div class="buttons">
|
|
6
|
+
<b-button tag="router-link" to="/pages" icon-left="list">Manage Pages</b-button>
|
|
7
|
+
<b-button tag="router-link" to="/partials" icon-left="puzzle-piece">Manage Partials</b-button>
|
|
8
|
+
</div>
|
|
9
|
+
<b-button tag="a" href="/" icon-left="eye" type="is-primary" rounded>View Live Site</b-button>
|
|
10
|
+
</article>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
export default {
|
|
15
|
+
name: "PagesPartialsPanel",
|
|
16
|
+
computed: {
|
|
17
|
+
userPermissions() { return this.$store.getters.userPermissions; },
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<style scoped lang="scss">
|
|
23
|
+
@import './panel.scss';
|
|
24
|
+
</style>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<PreferencesList>
|
|
3
|
+
<template #default="slotProps">
|
|
4
|
+
<b-tab-item v-if="slotProps.canAccessPrefs(['appearance.themes', 'appearance.pages', 'appearance.events'].concat(getExtraPrefsPermissions('appearance')))" label="Website Theme">
|
|
5
|
+
<PreferencesThemeChooser @theme-changed="onThemeChanged" />
|
|
6
|
+
<PreferencesPageTemplates :current-theme="currentTheme" />
|
|
7
|
+
<PreferencesEventTemplates :current-theme="currentTheme" />
|
|
8
|
+
<PreferencesSiteAppearance :current-theme="currentTheme" />
|
|
9
|
+
<component :is="pref.component" v-for="pref in getExtraPrefs('appearance')" :key="pref.key" :current-theme="currentTheme" />
|
|
10
|
+
</b-tab-item>
|
|
11
|
+
<b-tab-item v-if="slotProps.canAccessPrefs(getExtraPrefsPermissions('external'))" label="External Integrations">
|
|
12
|
+
<component :is="pref.component" v-for="pref in getExtraPrefs('external')" :key="pref.key" :current-theme="currentTheme" />
|
|
13
|
+
</b-tab-item>
|
|
14
|
+
<b-tab-item v-if="slotProps.canAccessPrefs(['modules.auth'])" label="Authentication & Security">
|
|
15
|
+
<PreferencesAuthentication :current-theme="currentTheme" />
|
|
16
|
+
</b-tab-item>
|
|
17
|
+
<b-tab-item v-if="slotProps.canAccessPrefs(['appearance.auth'])" label="Admin Look & Feel">
|
|
18
|
+
<PreferencesAdminAppearance :current-theme="currentTheme" />
|
|
19
|
+
</b-tab-item>
|
|
20
|
+
<b-tab-item v-if="userPermissions.has('importExport.getExport')" label="Website Data">
|
|
21
|
+
<ImportExport />
|
|
22
|
+
</b-tab-item>
|
|
23
|
+
</template>
|
|
24
|
+
</PreferencesList>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script>
|
|
28
|
+
|
|
29
|
+
import PreferencesList from './PreferencesList.vue';
|
|
30
|
+
import PreferencesPageTemplates from './PreferencesPageTemplates.vue';
|
|
31
|
+
import PreferencesEventTemplates from './PreferencesEventTemplates.vue';
|
|
32
|
+
import PreferencesThemeChooser from './PreferencesThemeChooser.vue';
|
|
33
|
+
import PreferencesAuthentication from './PreferencesAuthentication.vue';
|
|
34
|
+
import PreferencesAdminAppearance from './PreferencesAdminAppearance.vue';
|
|
35
|
+
import PreferencesSiteAppearance from './PreferencesSiteAppearance.vue';
|
|
36
|
+
import ImportExport from '../ImportExport.vue';
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
name: "Preferences",
|
|
40
|
+
components: {
|
|
41
|
+
PreferencesList,
|
|
42
|
+
PreferencesPageTemplates,
|
|
43
|
+
PreferencesEventTemplates,
|
|
44
|
+
PreferencesThemeChooser,
|
|
45
|
+
PreferencesAuthentication,
|
|
46
|
+
PreferencesAdminAppearance,
|
|
47
|
+
PreferencesSiteAppearance,
|
|
48
|
+
ImportExport
|
|
49
|
+
},
|
|
50
|
+
props: {
|
|
51
|
+
extraPrefs: {
|
|
52
|
+
type: Object,
|
|
53
|
+
default: () => { return {}; }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
data() {
|
|
57
|
+
return {
|
|
58
|
+
currentTheme: null,
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
computed: {
|
|
62
|
+
userPermissions() {
|
|
63
|
+
return this.$store.getters.userPermissions;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
methods: {
|
|
67
|
+
onThemeChanged(currentTheme) {
|
|
68
|
+
this.currentTheme = currentTheme;
|
|
69
|
+
},
|
|
70
|
+
getExtraPrefsPermissions(category) {
|
|
71
|
+
return (this.extraPrefs[category] ?? []).map(pref => pref.key);
|
|
72
|
+
},
|
|
73
|
+
getExtraPrefs(category) {
|
|
74
|
+
return (this.extraPrefs[category] ?? []);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
|
|
82
|
+
</style>
|
package/src/icons.js
CHANGED
|
@@ -73,7 +73,8 @@ import {
|
|
|
73
73
|
faLandmark,
|
|
74
74
|
faFolderOpen,
|
|
75
75
|
faImages,
|
|
76
|
-
faMinusCircle
|
|
76
|
+
faMinusCircle,
|
|
77
|
+
faCalendarPlus, faPaperPlane
|
|
77
78
|
} from "@fortawesome/free-solid-svg-icons";
|
|
78
79
|
|
|
79
80
|
export const addIconsToLibrary = () => {
|
|
@@ -86,5 +87,5 @@ export const addIconsToLibrary = () => {
|
|
|
86
87
|
faFileExcel, faFileCsv, faChevronCircleDown, faChevronCircleUp, faTrash,
|
|
87
88
|
faEye, faEyeSlash, faCaretDown, faCaretUp, faUpload, faUser, faFolder, faHome, faFilePdf, faSignOutAlt, faTag,
|
|
88
89
|
faFolderPlus, faTimes, faQuestionCircle, faFileUpload, faLandmark,
|
|
89
|
-
faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages);
|
|
90
|
+
faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane);
|
|
90
91
|
};
|
package/src/main.js
CHANGED
|
@@ -12,6 +12,7 @@ import { AuthRoutes, makeAuthenticatedRoute } from "./routes";
|
|
|
12
12
|
import createStore from "./store/index";
|
|
13
13
|
import { checkAuthenticated } from "./AuthApi";
|
|
14
14
|
import Error404 from "./components/Error404.vue";
|
|
15
|
+
import MainMenu from "./components/MainMenu.vue";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Creates the Vue.js Oxygen application, allowing for a few points of customization (i.e.: adding modules)
|
|
@@ -21,13 +22,13 @@ import Error404 from "./components/Error404.vue";
|
|
|
21
22
|
* @param beforeMount
|
|
22
23
|
*/
|
|
23
24
|
export default class OxygenUI {
|
|
24
|
-
app;
|
|
25
|
-
|
|
26
25
|
constructor(Vue) {
|
|
26
|
+
this.app = null;
|
|
27
27
|
this.Vue = Vue;
|
|
28
28
|
this.authenticatedRoutes = []
|
|
29
29
|
this.unauthenticatedRoutes = []
|
|
30
|
-
this.
|
|
30
|
+
this.mainMenuItems = {}
|
|
31
|
+
this.rootComponents = { App, MainMenu }
|
|
31
32
|
this.beforeMountHooks = []
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -41,6 +42,17 @@ export default class OxygenUI {
|
|
|
41
42
|
this.authenticatedRoutes.push(route);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
addMainMenuGroup(category, group) {
|
|
46
|
+
if(!this.mainMenuItems[category]) {
|
|
47
|
+
this.mainMenuItems[category] = {};
|
|
48
|
+
}
|
|
49
|
+
if(!group.items) {
|
|
50
|
+
group.items = {};
|
|
51
|
+
}
|
|
52
|
+
this.mainMenuItems[category][group.name] = group;
|
|
53
|
+
return group;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
addUnauthenticatedRoutes(routes) {
|
|
45
57
|
for (let route of routes) {
|
|
46
58
|
this.unauthenticatedRoutes.push(route);
|
|
@@ -92,6 +104,9 @@ export default class OxygenUI {
|
|
|
92
104
|
|
|
93
105
|
this.app = new this.Vue({
|
|
94
106
|
router: router,
|
|
107
|
+
data: {
|
|
108
|
+
mainMenuItems: this.mainMenuItems
|
|
109
|
+
},
|
|
95
110
|
components: this.rootComponents,
|
|
96
111
|
store
|
|
97
112
|
});
|
|
@@ -110,3 +125,5 @@ export default class OxygenUI {
|
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
|
|
128
|
+
export const WEB_CONTENT = 'Web Content';
|
|
129
|
+
|
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import LegacyPage from "../components/LegacyPage.vue";
|
|
2
|
+
import { WEB_CONTENT } from "../main.js";
|
|
2
3
|
|
|
3
4
|
export default function(ui) {
|
|
5
|
+
ui.addMainMenuGroup(WEB_CONTENT, {
|
|
6
|
+
name: 'Pages',
|
|
7
|
+
icon: 'file-alt',
|
|
8
|
+
listAction: '/pages',
|
|
9
|
+
listPermission: 'pages.getList',
|
|
10
|
+
addIcon: 'plus',
|
|
11
|
+
addPermission: 'pages.postCreate',
|
|
12
|
+
addAction: '/pages/create',
|
|
13
|
+
items: {
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
ui.addMainMenuGroup(WEB_CONTENT, {
|
|
17
|
+
name: 'Partials',
|
|
18
|
+
icon: 'puzzle-piece',
|
|
19
|
+
listAction: '/partials',
|
|
20
|
+
listPermission: 'partials.getList',
|
|
21
|
+
addIcon: 'plus',
|
|
22
|
+
addPermission: 'partials.postCreate',
|
|
23
|
+
addAction: '/partials/create',
|
|
24
|
+
items: {
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
ui.addMainMenuGroup(WEB_CONTENT, {
|
|
28
|
+
name: 'Events',
|
|
29
|
+
icon: 'calendar-alt',
|
|
30
|
+
listAction: '/upcoming-events',
|
|
31
|
+
listPermission: 'upcomingEvents.getList',
|
|
32
|
+
addIcon: 'calendar-plus',
|
|
33
|
+
addPermission: 'upcomingEvents.postCreate',
|
|
34
|
+
addAction: '/upcoming-events/create',
|
|
35
|
+
items: {
|
|
36
|
+
}
|
|
37
|
+
});
|
|
4
38
|
ui.addAuthenticatedRoutes([
|
|
5
39
|
{
|
|
6
40
|
// will match everything, try to render a legacy Oxygen page...
|
package/src/modules/Media.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import MediaPage from "../components/media/MediaPage.vue";
|
|
2
2
|
import MediaResponsiveImages from "../components/media/MediaResponsiveImages.vue";
|
|
3
|
+
import {WEB_CONTENT} from "../main";
|
|
3
4
|
|
|
4
5
|
export default function(ui) {
|
|
6
|
+
ui.addMainMenuGroup(WEB_CONTENT, {
|
|
7
|
+
name: 'Photos & Files',
|
|
8
|
+
icon: 'photo-video',
|
|
9
|
+
groupPrefix: '/media',
|
|
10
|
+
addAction: '/media/list/?upload=true',
|
|
11
|
+
addIcon: 'upload',
|
|
12
|
+
addPermission: 'media.postCreate',
|
|
13
|
+
listAction: '/media/list',
|
|
14
|
+
listPermission: 'media.getList',
|
|
15
|
+
items: {
|
|
16
|
+
'Deleted Photos & Files': { to: '/media/trash', permission: 'media.getList' },
|
|
17
|
+
'Responsive Images': { to: '/media/responsive-images', permission: 'media.postMakeResponsive' }
|
|
18
|
+
}
|
|
19
|
+
});
|
|
5
20
|
ui.addAuthenticatedRoutes([
|
|
6
21
|
{
|
|
7
22
|
path: 'media/list/:currentPath(.*)?',
|