@oxygen-cms/ui 1.6.4 → 1.7.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/.idea/workspace.xml +37 -0
- package/package.json +1 -1
- package/src/AuthApi.js +7 -0
- package/src/Internationalize.js +4 -0
- package/src/UsersApi.js +5 -0
- package/src/api.js +24 -26
- package/src/components/CodeEditor.vue +2 -0
- package/src/components/Created.vue +25 -0
- package/src/components/LegacyPage.vue +22 -15
- package/src/components/Updated.vue +25 -0
- package/src/components/UserProfileDescription.vue +41 -0
- package/src/components/UserProfileForm.vue +3 -24
- package/src/components/UserProfilePage.vue +88 -0
- package/src/components/auth/Login.vue +0 -3
- package/src/components/auth/Logout.vue +1 -1
- package/src/icons.js +2 -2
- package/src/main.js +1 -0
- package/src/modules/UserManagement.js +7 -0
- package/src/EventsApi.js +0 -21
- package/src/components/EventsChooser.vue +0 -88
- package/src/components/EventsTable.vue +0 -82
- package/src/components/dashboard/EventsPanel.vue +0 -20
- package/src/modules/Events.js +0 -35
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ChangeListManager">
|
|
4
|
+
<list default="true" id="4ad0dcde-54f6-459b-8ef0-38a17e946358" name="Changes" comment="" />
|
|
5
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
6
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
7
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
8
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
9
|
+
</component>
|
|
10
|
+
<component name="ComposerSettings">
|
|
11
|
+
<execution />
|
|
12
|
+
</component>
|
|
13
|
+
<component name="ProjectId" id="27rS1jBVazc5EdthHlwaXfJm1hC" />
|
|
14
|
+
<component name="ProjectViewState">
|
|
15
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
16
|
+
<option name="showLibraryContents" value="true" />
|
|
17
|
+
</component>
|
|
18
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
19
|
+
"keyToString": {
|
|
20
|
+
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
|
21
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
22
|
+
"WebServerToolWindowFactoryState": "false",
|
|
23
|
+
"last_opened_file_path": "/home/chris/code/oxygen/Components/ui"
|
|
24
|
+
}
|
|
25
|
+
}]]></component>
|
|
26
|
+
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
27
|
+
<component name="TaskManager">
|
|
28
|
+
<task active="true" id="Default" summary="Default task">
|
|
29
|
+
<changelist id="4ad0dcde-54f6-459b-8ef0-38a17e946358" name="Changes" comment="" />
|
|
30
|
+
<created>1650076498224</created>
|
|
31
|
+
<option name="number" value="Default" />
|
|
32
|
+
<option name="presentableId" value="Default" />
|
|
33
|
+
<updated>1650076498224</updated>
|
|
34
|
+
</task>
|
|
35
|
+
<servers />
|
|
36
|
+
</component>
|
|
37
|
+
</project>
|
package/package.json
CHANGED
package/src/AuthApi.js
CHANGED
|
@@ -2,6 +2,13 @@ import {getApiRoot} from "./CrudApi";
|
|
|
2
2
|
import {FetchBuilder, initCsrfCookie} from "./api";
|
|
3
3
|
import UserPermissions from "./UserPermissions";
|
|
4
4
|
|
|
5
|
+
export const LOGIN_AGAIN_NOTIFICATION = {
|
|
6
|
+
message: 'You need to login again.',
|
|
7
|
+
type: 'is-warning',
|
|
8
|
+
duration: 5000,
|
|
9
|
+
queue: false
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
export default class AuthApi {
|
|
6
13
|
|
|
7
14
|
constructor($buefy) {
|
package/src/Internationalize.js
CHANGED
|
@@ -12,6 +12,10 @@ export default class Internationalize {
|
|
|
12
12
|
return format.format(date);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
static formatDateExtended(date) {
|
|
16
|
+
return new Date(date).toDateString();
|
|
17
|
+
}
|
|
18
|
+
|
|
15
19
|
static formatLastUpdated(updatedAt) {
|
|
16
20
|
let d = new Date(updatedAt);
|
|
17
21
|
return d.toDateString() + ' ' + d.toLocaleTimeString();
|
package/src/UsersApi.js
CHANGED
|
@@ -23,6 +23,11 @@ export default class UsersApi extends CrudApi {
|
|
|
23
23
|
.fetch(getApiRoot() + 'users/' + id + '/fullName');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
async getBasic(id) {
|
|
27
|
+
return this.request('get')
|
|
28
|
+
.fetch(this.constructor.getResourceRoot() + '/' + id + '/basic');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
async impersonate(id) {
|
|
27
32
|
return await this.request('post')
|
|
28
33
|
.fetch(getApiRoot() + 'users/' + id + '/impersonate');
|
package/src/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {getApiRoot} from "./CrudApi";
|
|
2
|
+
import {LOGIN_AGAIN_NOTIFICATION} from "./AuthApi";
|
|
2
3
|
|
|
3
4
|
export const getApiHost = () => {
|
|
4
5
|
if (parseInt(window.location.port) >= 3000) {
|
|
@@ -28,6 +29,7 @@ export const initCsrfCookie = async () => {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export class FetchBuilder {
|
|
32
|
+
|
|
31
33
|
constructor($buefy, method) {
|
|
32
34
|
this.$buefy = $buefy;
|
|
33
35
|
this.method = method;
|
|
@@ -120,7 +122,7 @@ export class FetchBuilder {
|
|
|
120
122
|
return data;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
handleAPIError(data, this.$buefy, FetchBuilder.router, response);
|
|
125
|
+
await handleAPIError(data, this.$buefy, FetchBuilder.router, FetchBuilder.store, response);
|
|
124
126
|
let e = new Error('Received an error response from API call');
|
|
125
127
|
e.response = data;
|
|
126
128
|
throw e;
|
|
@@ -135,6 +137,10 @@ export class FetchBuilder {
|
|
|
135
137
|
static setRouter(router) {
|
|
136
138
|
FetchBuilder.router = router;
|
|
137
139
|
}
|
|
140
|
+
|
|
141
|
+
static setStore(store) {
|
|
142
|
+
FetchBuilder.store = store;
|
|
143
|
+
}
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
function statusToBueify(status) {
|
|
@@ -155,35 +161,29 @@ export function morphToNotification(data) {
|
|
|
155
161
|
};
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
const handleAPIError =
|
|
159
|
-
console.error('API error: ', content);
|
|
164
|
+
const handleAPIError = async (content, $buefy, $router, $store, response) => {
|
|
165
|
+
console.error('API error: ', content, $router);
|
|
160
166
|
if(response.status === 401 && content.code === 'unauthenticated') {
|
|
161
167
|
// server is telling us to login again
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return;
|
|
168
|
+
await $store.commit('setUser', null);
|
|
169
|
+
await initCsrfCookie();
|
|
170
|
+
await $buefy.notification.open(LOGIN_AGAIN_NOTIFICATION);
|
|
171
|
+
await $router.push({path: '/auth/login', query: {redirect: $router.currentRoute.fullPath}});
|
|
167
172
|
} else if(response.status === 403 && content.code === 'two_factor_setup_required') {
|
|
168
|
-
$router.push({ path: '/auth/2fa-setup' });
|
|
169
|
-
return;
|
|
173
|
+
await $router.push({ path: '/auth/2fa-setup' });
|
|
170
174
|
} else if(response.status === 403 && content.code === 'email_unverified') {
|
|
171
|
-
$router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
|
|
172
|
-
return;
|
|
175
|
+
await $router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
|
|
173
176
|
} else if(response.status === 404) {
|
|
174
|
-
$router.push({ name: 'error404' });
|
|
177
|
+
await $router.push({ name: 'error404' });
|
|
175
178
|
} else if(response.status === 429) {
|
|
176
|
-
$buefy.notification.open({
|
|
179
|
+
await $buefy.notification.open({
|
|
177
180
|
message: 'Too many requests within a short timeframe. Please wait.',
|
|
178
181
|
type: 'is-warning',
|
|
179
182
|
duration: 10000,
|
|
180
183
|
queue: false
|
|
181
184
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// handle generic validation errors
|
|
186
|
-
if(typeof content.errors === 'object') {
|
|
185
|
+
} else if(typeof content.errors === 'object') {
|
|
186
|
+
// handle generic validation errors
|
|
187
187
|
for(const [, errors ] of Object.entries(content.errors)) {
|
|
188
188
|
for(let error of errors) {
|
|
189
189
|
$buefy.notification.open({
|
|
@@ -194,13 +194,10 @@ const handleAPIError = function(content, $buefy, $router, response) {
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if(content.content && content.status) {
|
|
201
|
-
$buefy.notification.open(morphToNotification(content));
|
|
197
|
+
} else if(content.content && content.status) {
|
|
198
|
+
await $buefy.notification.open(morphToNotification(content));
|
|
202
199
|
} else if(content.exception) {
|
|
203
|
-
$buefy.notification.open({
|
|
200
|
+
await $buefy.notification.open({
|
|
204
201
|
message:
|
|
205
202
|
'PHP Exception of type <pre class="no-pre">' + content.exception +
|
|
206
203
|
'</pre> with message <pre class="no-pre">' + content.message +
|
|
@@ -211,7 +208,7 @@ const handleAPIError = function(content, $buefy, $router, response) {
|
|
|
211
208
|
type: 'is-danger'
|
|
212
209
|
});
|
|
213
210
|
} else if(response.status === 500) {
|
|
214
|
-
$buefy.notification.open({
|
|
211
|
+
await $buefy.notification.open({
|
|
215
212
|
message:'Whoops, looks like something went wrong.',
|
|
216
213
|
type: 'is-danger',
|
|
217
214
|
animation: 'fade',
|
|
@@ -225,3 +222,4 @@ export function getXsrfToken() {
|
|
|
225
222
|
}
|
|
226
223
|
|
|
227
224
|
FetchBuilder.router = null;
|
|
225
|
+
FetchBuilder.store = null;
|
|
@@ -35,8 +35,10 @@ import AceEditor from 'vue2-ace-editor';
|
|
|
35
35
|
// language extension pre-requisite...
|
|
36
36
|
import 'brace/ext/language_tools';
|
|
37
37
|
import 'brace/mode/html';
|
|
38
|
+
import 'brace/mode/json';
|
|
38
39
|
import 'brace/mode/twig';
|
|
39
40
|
import 'brace/snippets/html';
|
|
41
|
+
import 'brace/snippets/json';
|
|
40
42
|
import 'brace/snippets/twig';
|
|
41
43
|
import 'brace/theme/tomorrow_night_eighties';
|
|
42
44
|
import 'brace/theme/tomorrow_night';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span>
|
|
3
|
+
{{ Internationalize.formatLastUpdated(model.createdAt) }}<span v-if="model.createdBy">, by <router-link :to="'/users/' + model.createdBy.id">{{ model.createdBy.fullName }}</router-link></span>
|
|
4
|
+
</span>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import Internationalize from "../Internationalize";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: "Created",
|
|
12
|
+
props: {
|
|
13
|
+
model: { type: Object, required: true }
|
|
14
|
+
},
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
Internationalize
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style scoped>
|
|
24
|
+
|
|
25
|
+
</style>
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
<script>
|
|
12
12
|
import MediaInsertModal from "./media/MediaInsertModal.vue";
|
|
13
13
|
|
|
14
|
-
import {getApiHost, morphToNotification} from "../api";
|
|
14
|
+
import {getApiHost, initCsrfCookie, morphToNotification} from "../api";
|
|
15
15
|
import MediaApi from "../MediaApi";
|
|
16
|
+
import {LOGIN_AGAIN_NOTIFICATION} from "../AuthApi";
|
|
16
17
|
|
|
17
18
|
// from https://gist.github.com/hdodov/a87c097216718655ead6cf2969b0dcfa
|
|
18
19
|
|
|
@@ -20,15 +21,6 @@ const iframeURLChange = (iframe, callback, legacyPage) => {
|
|
|
20
21
|
var unloadHandler = function() {
|
|
21
22
|
console.log('[LegacyPage] Starting load');
|
|
22
23
|
legacyPage.loadingPath = 'unknown';
|
|
23
|
-
// Timeout needed because the URL changes immediately after
|
|
24
|
-
// the `unload` event is dispatched.
|
|
25
|
-
// TODO: this is rather brittle because I believe it relies upon timing
|
|
26
|
-
// setTimeout(function() {
|
|
27
|
-
// console.log(iframe.contentWindow);
|
|
28
|
-
// if(iframe.contentWindow) {
|
|
29
|
-
// callback(iframe.contentWindow.location.href);
|
|
30
|
-
// }
|
|
31
|
-
// }, 0);
|
|
32
24
|
};
|
|
33
25
|
|
|
34
26
|
function attachUnload() {
|
|
@@ -90,7 +82,7 @@ export default {
|
|
|
90
82
|
iframe.addEventListener('load', this.onLoaded.bind(this));
|
|
91
83
|
if(iframe.contentDocument.readyState === "complete") {
|
|
92
84
|
console.warn('[LegacyPage] mounted: page was already loaded - perhaps this page was cached?');
|
|
93
|
-
this.onLoaded();
|
|
85
|
+
await this.onLoaded();
|
|
94
86
|
}
|
|
95
87
|
},
|
|
96
88
|
unmounted() {
|
|
@@ -126,7 +118,8 @@ export default {
|
|
|
126
118
|
let urlObj = new URL(url);
|
|
127
119
|
let urlString = urlObj.toString();
|
|
128
120
|
if(urlObj.pathname.startsWith(this.legacyPrefix)) {
|
|
129
|
-
|
|
121
|
+
let loc = urlString.split(this.legacyPrefix)[1];
|
|
122
|
+
return { loadInside: 'iframe', location: this.adminPrefix + loc, locationWithoutPrefix: loc };
|
|
130
123
|
} else if(urlObj.pathname.startsWith(this.adminPrefix)) {
|
|
131
124
|
return { loadInside: 'vue', location: urlString.split(this.adminPrefix)[1] };
|
|
132
125
|
} else {
|
|
@@ -155,7 +148,7 @@ export default {
|
|
|
155
148
|
popState() {
|
|
156
149
|
this.$router.back();
|
|
157
150
|
},
|
|
158
|
-
onLoaded() {
|
|
151
|
+
async onLoaded() {
|
|
159
152
|
let path = this.$refs.iframe.contentWindow.location.href;
|
|
160
153
|
if(path === 'about:blank') { return; }
|
|
161
154
|
console.log('[LegacyPage] Loaded', path);
|
|
@@ -166,12 +159,26 @@ export default {
|
|
|
166
159
|
if(loadInside === 'iframe') {
|
|
167
160
|
window.history.pushState({}, "", location);
|
|
168
161
|
} else if(loadInside === 'vue') {
|
|
169
|
-
|
|
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;
|
|
170
177
|
} else {
|
|
171
178
|
// load outside of iframe
|
|
172
179
|
window.location = location;
|
|
180
|
+
return;
|
|
173
181
|
}
|
|
174
|
-
|
|
175
182
|
this.currentPath = path;
|
|
176
183
|
}
|
|
177
184
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span>
|
|
3
|
+
{{ Internationalize.formatLastUpdated(model.updatedAt) }}<span v-if="model.updatedBy">, by <router-link :to="'/users/' + model.updatedBy.id">{{ model.updatedBy.fullName }}</router-link></span>
|
|
4
|
+
</span>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import Internationalize from "../Internationalize";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: "Created",
|
|
12
|
+
props: {
|
|
13
|
+
model: { type: Object, required: true }
|
|
14
|
+
},
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
Internationalize
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style scoped>
|
|
24
|
+
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="content">
|
|
3
|
+
<div class="level level-left">
|
|
4
|
+
<strong class="mr-2">Username:</strong>
|
|
5
|
+
<span v-if="user">{{ user.username }}
|
|
6
|
+
<b-tooltip v-if="editable" label="To change username, please contact your administrator." position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="level level-left"><strong class="mr-2">Group: </strong>
|
|
10
|
+
<transition name="fade" mode="out-in">
|
|
11
|
+
<span v-if="user">{{ user.group.name }}
|
|
12
|
+
<b-tooltip :label="user.group.description" position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
|
|
13
|
+
</span>
|
|
14
|
+
<b-skeleton v-else width="20%" :animated="true" />
|
|
15
|
+
</transition>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="level level-left"><strong class="mr-2">Joined: </strong>
|
|
18
|
+
<transition name="fade" mode="out-in">
|
|
19
|
+
<UserJoined v-if="user" :user="user" />
|
|
20
|
+
<b-skeleton v-else width="20%" :animated="true" />
|
|
21
|
+
</transition>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script>
|
|
27
|
+
import UserJoined from "./UserJoined.vue";
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
name: "UserProfileDescription",
|
|
31
|
+
components: { UserJoined },
|
|
32
|
+
props: {
|
|
33
|
+
user: { type: Object, default: null },
|
|
34
|
+
editable: { type: Boolean, required: true }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<style scoped>
|
|
40
|
+
|
|
41
|
+
</style>
|
|
@@ -19,28 +19,7 @@
|
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
-
<
|
|
23
|
-
<div class="level level-left">
|
|
24
|
-
<strong class="mr-2">Username:</strong>
|
|
25
|
-
<span>{{ user.username }}
|
|
26
|
-
<b-tooltip label="To change username, please contact your administrator." position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
|
|
27
|
-
</span>
|
|
28
|
-
</div>
|
|
29
|
-
<div class="level level-left"><strong class="mr-2">Group: </strong>
|
|
30
|
-
<transition name="fade" mode="out-in">
|
|
31
|
-
<span v-if="user">{{ user.group.name }}
|
|
32
|
-
<b-tooltip :label="user.group.description" position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
|
|
33
|
-
</span>
|
|
34
|
-
<b-skeleton v-else width="20%" :animated="true" />
|
|
35
|
-
</transition>
|
|
36
|
-
</div>
|
|
37
|
-
<div class="level level-left"><strong class="mr-2">Joined: </strong>
|
|
38
|
-
<transition name="fade" mode="out-in">
|
|
39
|
-
<UserJoined v-if="user" :user="user" />
|
|
40
|
-
<b-skeleton v-else width="20%" :animated="true" />
|
|
41
|
-
</transition>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
22
|
+
<UserProfileDescription :user="user" :editable="true"></UserProfileDescription>
|
|
44
23
|
|
|
45
24
|
<b-modal :closable="false" :active.sync="isUserPreferencesModalActive" has-modal-card trap-focus aria-role="dialog" aria-modal>
|
|
46
25
|
<div class="modal-card">
|
|
@@ -136,16 +115,16 @@
|
|
|
136
115
|
|
|
137
116
|
<script>
|
|
138
117
|
import {morphToNotification} from "../api";
|
|
139
|
-
import UserJoined from "./UserJoined.vue";
|
|
140
118
|
import ShowIfPermitted from "./preferences/ShowIfPermitted.vue";
|
|
141
119
|
import UserPreferences from "./preferences/UserPreferences.vue";
|
|
142
120
|
import UsersApi from "../UsersApi";
|
|
143
121
|
import AuthApi from "../AuthApi";
|
|
144
122
|
import GenericEditableField from "./GenericEditableField.vue";
|
|
123
|
+
import UserProfileDescription from "./UserProfileDescription.vue";
|
|
145
124
|
|
|
146
125
|
export default {
|
|
147
126
|
name: "UserProfileForm",
|
|
148
|
-
components: {GenericEditableField, ShowIfPermitted,
|
|
127
|
+
components: {UserProfileDescription, GenericEditableField, ShowIfPermitted, UserPreferences },
|
|
149
128
|
props: {
|
|
150
129
|
user: {
|
|
151
130
|
type: Object,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="full-height-container pad">
|
|
3
|
+
|
|
4
|
+
<div class="box middle-of-page">
|
|
5
|
+
|
|
6
|
+
<div>
|
|
7
|
+
<div class="has-background-grey-light huge-icon-container" style="display: inline-block;">
|
|
8
|
+
<b-icon icon="user" size="is-large" class="has-text-grey-lighter huge-icon"></b-icon>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<h2 class="title">
|
|
13
|
+
<transition name="fade" mode="out-in">
|
|
14
|
+
<span v-if="model">{{model.fullName}}</span>
|
|
15
|
+
<b-skeleton :active="!model" size="is-medium"></b-skeleton>
|
|
16
|
+
</transition>
|
|
17
|
+
</h2>
|
|
18
|
+
<h3 class="subtitle">
|
|
19
|
+
<transition name="fade" mode="out-in">
|
|
20
|
+
<a v-if="model" :href="'mailto:' + model.email">{{ model.email }}</a>
|
|
21
|
+
<b-skeleton :active="!model"></b-skeleton>
|
|
22
|
+
</transition>
|
|
23
|
+
</h3>
|
|
24
|
+
|
|
25
|
+
<div v-if="model && model.id == currentUserId">
|
|
26
|
+
<b-button type="is-primary" tag="router-link" to="/user/profile">Edit Your Profile</b-button>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<hr>
|
|
30
|
+
|
|
31
|
+
<UserProfileDescription :user="model" :editable="false"></UserProfileDescription>
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<script>
|
|
38
|
+
import UsersApi from "../UsersApi";
|
|
39
|
+
import UserProfileDescription from "./UserProfileDescription.vue";
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: "UserProfilePage",
|
|
43
|
+
components: {UserProfileDescription},
|
|
44
|
+
data() {
|
|
45
|
+
return {
|
|
46
|
+
model: null,
|
|
47
|
+
loading: true,
|
|
48
|
+
usersApi: new UsersApi(this.$buefy)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
computed: {
|
|
52
|
+
currentUserId() {
|
|
53
|
+
return this.$store.state.user.id;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
created() {
|
|
57
|
+
this.fetchData();
|
|
58
|
+
},
|
|
59
|
+
methods: {
|
|
60
|
+
async fetchData() {
|
|
61
|
+
this.model = (await this.usersApi.getBasic(this.$route.params.id)).item;
|
|
62
|
+
this.loading = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
@import '~@oxygen-cms/ui/src/components/util.css';
|
|
70
|
+
|
|
71
|
+
.middle-of-page {
|
|
72
|
+
width: 40rem;
|
|
73
|
+
max-width: 100%;
|
|
74
|
+
margin-left: auto;
|
|
75
|
+
margin-right: auto;
|
|
76
|
+
text-align: center;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.huge-icon {
|
|
80
|
+
width: 10rem;
|
|
81
|
+
height: 10rem;
|
|
82
|
+
font-size: 3rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.huge-icon-container {
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -89,9 +89,6 @@ export default {
|
|
|
89
89
|
this.submitting = false;
|
|
90
90
|
this.hasFailedLogin = false;
|
|
91
91
|
this.$store.commit('setUser', response.user);
|
|
92
|
-
if(this.$route.query.location) {
|
|
93
|
-
window.location = this.$route.query.location;
|
|
94
|
-
}
|
|
95
92
|
this.$buefy.notification.open({
|
|
96
93
|
message: "You're now logged in.",
|
|
97
94
|
type: 'is-info',
|
package/src/icons.js
CHANGED
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
faFolderOpen,
|
|
75
75
|
faImages,
|
|
76
76
|
faMinusCircle,
|
|
77
|
-
faCalendarPlus, faPaperPlane, faHandshakeSlash, faHandshake
|
|
77
|
+
faCalendarPlus, faPaperPlane, faHandshakeSlash, faHandshake, faExclamation, faMousePointer, faUnlink, faAngry
|
|
78
78
|
} from "@fortawesome/free-solid-svg-icons";
|
|
79
79
|
|
|
80
80
|
export const addIconsToLibrary = () => {
|
|
@@ -87,5 +87,5 @@ export const addIconsToLibrary = () => {
|
|
|
87
87
|
faFileExcel, faFileCsv, faChevronCircleDown, faChevronCircleUp, faTrash,
|
|
88
88
|
faEye, faEyeSlash, faCaretDown, faCaretUp, faUpload, faUser, faFolder, faHome, faFilePdf, faSignOutAlt, faTag,
|
|
89
89
|
faFolderPlus, faTimes, faQuestionCircle, faFileUpload, faLandmark,
|
|
90
|
-
faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane, faHandshake, faHandshakeSlash);
|
|
90
|
+
faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane, faHandshake, faHandshakeSlash, faExclamation, faMousePointer, faUnlink, faAngry);
|
|
91
91
|
};
|
package/src/main.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import ViewProfile from "../components/ViewProfile.vue";
|
|
2
2
|
import AuthenticationLog from "../components/AuthenticationLog.vue";
|
|
3
3
|
import UserManagement from "../components/UserManagement.vue";
|
|
4
|
+
import UserProfilePage from "../components/UserProfilePage.vue";
|
|
4
5
|
|
|
5
6
|
export default function(ui) {
|
|
6
7
|
ui.addAuthenticatedRoutes([
|
|
8
|
+
{
|
|
9
|
+
path: 'users/:id',
|
|
10
|
+
name: 'users.viewProfile',
|
|
11
|
+
component: UserProfilePage,
|
|
12
|
+
meta: { title: 'User Profile' }
|
|
13
|
+
},
|
|
7
14
|
{
|
|
8
15
|
path: 'user/profile',
|
|
9
16
|
name: 'auth.viewProfile',
|
package/src/EventsApi.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { CrudApi } from './CrudApi';
|
|
2
|
-
|
|
3
|
-
export default class EventsApi extends CrudApi {
|
|
4
|
-
|
|
5
|
-
static prepareModelForAPI(data) {
|
|
6
|
-
let m = { ...data };
|
|
7
|
-
delete m.id;
|
|
8
|
-
delete m.bookings;
|
|
9
|
-
return m;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
static getResourceName() {
|
|
13
|
-
return 'upcoming-events';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async listTrybookingSessions() {
|
|
17
|
-
return this.request('get')
|
|
18
|
-
.fetch(this.constructor.getResourceRoot() + '/trybooking-sessions');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<b-modal :active="active" trap-focus
|
|
3
|
-
aria-role="dialog"
|
|
4
|
-
aria-modal
|
|
5
|
-
@update:active="arg => $emit('update:active', arg)">
|
|
6
|
-
<div class="modal-card" style="width: auto">
|
|
7
|
-
<header class="modal-card-head">
|
|
8
|
-
<p class="modal-card-title">
|
|
9
|
-
<slot name="title">Choose an event</slot>
|
|
10
|
-
</p>
|
|
11
|
-
<button
|
|
12
|
-
type="button"
|
|
13
|
-
class="delete"
|
|
14
|
-
@click="$emit('update:active', false)"/>
|
|
15
|
-
</header>
|
|
16
|
-
<section class="modal-card-body full-height-container">
|
|
17
|
-
<slot name="explanation"></slot>
|
|
18
|
-
<b-field>
|
|
19
|
-
<b-input v-model="searchQuery"
|
|
20
|
-
placeholder="Search..."
|
|
21
|
-
type="search" icon="search" rounded>
|
|
22
|
-
</b-input>
|
|
23
|
-
</b-field>
|
|
24
|
-
<EventsTable :paginated-items="eventsPaginatedItems" :on-page-change="(page) => eventsPaginatedItems.currentPage = page">
|
|
25
|
-
<template #actions="slotProps">
|
|
26
|
-
<div class="buttons" style="flex-wrap: nowrap;">
|
|
27
|
-
<b-button icon-left="calendar-alt" size="is-small" tag="a" :href="'/oxygen/upcoming-events/' + slotProps.row.id + '/edit'" style="margin-left: 0.5rem;" rounded>Go to event</b-button>
|
|
28
|
-
<b-button rounded :type="disableEvent(slotProps.row) ? '' : 'is-success'" :disabled="disableEvent(slotProps.row)" @click="$emit('selected', slotProps.row)">Choose</b-button>
|
|
29
|
-
</div>
|
|
30
|
-
</template>
|
|
31
|
-
</EventsTable>
|
|
32
|
-
</section>
|
|
33
|
-
</div>
|
|
34
|
-
</b-modal>
|
|
35
|
-
</template>
|
|
36
|
-
|
|
37
|
-
<script>
|
|
38
|
-
import EventsApi from "../EventsApi";
|
|
39
|
-
import EventsTable from './EventsTable.vue';
|
|
40
|
-
|
|
41
|
-
export default {
|
|
42
|
-
name: "EventsChooser",
|
|
43
|
-
components: { EventsTable },
|
|
44
|
-
props: {
|
|
45
|
-
active: Boolean,
|
|
46
|
-
disableEvent: {
|
|
47
|
-
type: Function,
|
|
48
|
-
default: () => { return false; }
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
data() {
|
|
52
|
-
return {
|
|
53
|
-
eventsPaginatedItems: { items: null, itemsPerPage: null, totalItems: null, loading: false, currentPage: null },
|
|
54
|
-
eventsApi: new EventsApi(this.$buefy),
|
|
55
|
-
searchQuery: '',
|
|
56
|
-
searchDebounce: null
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
watch: {
|
|
60
|
-
'searchQuery': 'debounceFetchData',
|
|
61
|
-
'eventsPaginatedItems.currentPage': 'fetchData'
|
|
62
|
-
},
|
|
63
|
-
async created() {
|
|
64
|
-
await this.fetchData()
|
|
65
|
-
},
|
|
66
|
-
methods: {
|
|
67
|
-
async fetchData() {
|
|
68
|
-
this.eventsPaginatedItems.loading = true;
|
|
69
|
-
|
|
70
|
-
let data = await this.eventsApi.list(false, this.eventsPaginatedItems.currentPage, this.searchQuery !== '' ? this.searchQuery : null);
|
|
71
|
-
this.eventsPaginatedItems.items = data.items;
|
|
72
|
-
this.eventsPaginatedItems.itemsPerPage = data.itemsPerPage;
|
|
73
|
-
this.eventsPaginatedItems.totalItems = data.totalItems;
|
|
74
|
-
this.eventsPaginatedItems.loading = false;
|
|
75
|
-
},
|
|
76
|
-
debounceFetchData() {
|
|
77
|
-
clearTimeout(this.searchDebounce)
|
|
78
|
-
this.searchDebounce = setTimeout(() => {
|
|
79
|
-
this.fetchData();
|
|
80
|
-
}, 400);
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
</script>
|
|
85
|
-
|
|
86
|
-
<style scoped>
|
|
87
|
-
|
|
88
|
-
</style>
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<b-table
|
|
4
|
-
:data="paginatedItems === null || paginatedItems.items === null ? [] : paginatedItems.items"
|
|
5
|
-
:checked-rows="checkedRows"
|
|
6
|
-
:loading="paginatedItems.items === null || paginatedItems.loading"
|
|
7
|
-
:checkable="checkable"
|
|
8
|
-
custom-row-key="id"
|
|
9
|
-
:paginated="paginatedItems.totalItems > paginatedItems.itemsPerPage"
|
|
10
|
-
backend-pagination
|
|
11
|
-
:total="paginatedItems.totalItems"
|
|
12
|
-
:per-page="paginatedItems.itemsPerPage"
|
|
13
|
-
:current-page="paginatedItems.currentPage"
|
|
14
|
-
aria-next-label="Next page"
|
|
15
|
-
aria-previous-label="Previous page"
|
|
16
|
-
aria-page-label="Page"
|
|
17
|
-
aria-current-label="Current page"
|
|
18
|
-
class="full-height-flex full-height-container"
|
|
19
|
-
@update:checkedRows="$emit('update:checkedRows', $event)"
|
|
20
|
-
@page-change="onPageChange">
|
|
21
|
-
<b-table-column v-slot="props" label="Title">
|
|
22
|
-
{{ props.row.title }}
|
|
23
|
-
</b-table-column>
|
|
24
|
-
|
|
25
|
-
<b-table-column v-slot="props" label="Display on website">
|
|
26
|
-
<em v-if="!props.row.active">No</em>
|
|
27
|
-
<span v-else>{{ props.row.startDate ? new Date(props.row.startDate).toDateString() : '?'}} - {{ props.row.endDate ? new Date(props.row.endDate).toDateString() : '?'}}</span>
|
|
28
|
-
</b-table-column>
|
|
29
|
-
|
|
30
|
-
<b-table-column v-slot="props">
|
|
31
|
-
<slot name="actions" :row="props.row"></slot>
|
|
32
|
-
</b-table-column>
|
|
33
|
-
|
|
34
|
-
<template slot="empty">
|
|
35
|
-
<section class="section">
|
|
36
|
-
<div class="content has-text-grey has-text-centered">
|
|
37
|
-
<p>
|
|
38
|
-
<slot name="empty">
|
|
39
|
-
No events found.
|
|
40
|
-
</slot>
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
</section>
|
|
44
|
-
</template>
|
|
45
|
-
</b-table>
|
|
46
|
-
</div>
|
|
47
|
-
</template>
|
|
48
|
-
|
|
49
|
-
<script>
|
|
50
|
-
export default {
|
|
51
|
-
name: "EventsTable",
|
|
52
|
-
props: {
|
|
53
|
-
paginatedItems: {
|
|
54
|
-
type: Object,
|
|
55
|
-
required: true
|
|
56
|
-
},
|
|
57
|
-
onPageChange: {
|
|
58
|
-
type: Function,
|
|
59
|
-
required: true
|
|
60
|
-
},
|
|
61
|
-
checkedRows: {
|
|
62
|
-
type: Array,
|
|
63
|
-
default: () => { return []; }
|
|
64
|
-
},
|
|
65
|
-
checkable: Boolean
|
|
66
|
-
},
|
|
67
|
-
data() {
|
|
68
|
-
return {
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
</script>
|
|
73
|
-
|
|
74
|
-
<style scoped>
|
|
75
|
-
.b-table {
|
|
76
|
-
min-height: 10rem;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.b-table.is-loading {
|
|
80
|
-
margin-bottom: 5rem;
|
|
81
|
-
}
|
|
82
|
-
</style>
|
|
@@ -1,20 +0,0 @@
|
|
|
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>
|
package/src/modules/Events.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import LegacyPage from "../components/LegacyPage.vue";
|
|
2
|
-
import { WEB_CONTENT } from "../main.js";
|
|
3
|
-
import PreferencesEventTemplates from "../components/preferences/PreferencesEventTemplates.vue";
|
|
4
|
-
|
|
5
|
-
export default function(ui) {
|
|
6
|
-
ui.addMainMenuGroup(WEB_CONTENT, {
|
|
7
|
-
name: 'Events',
|
|
8
|
-
icon: 'calendar-alt',
|
|
9
|
-
listAction: '/upcoming-events',
|
|
10
|
-
listPermission: 'upcomingEvents.getList',
|
|
11
|
-
addIcon: 'calendar-plus',
|
|
12
|
-
addPermission: 'upcomingEvents.postCreate',
|
|
13
|
-
addAction: '/upcoming-events/create',
|
|
14
|
-
items: {
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
ui.extraPrefs['appearance'].push({
|
|
18
|
-
key: 'appearance.events',
|
|
19
|
-
component: PreferencesEventTemplates
|
|
20
|
-
});
|
|
21
|
-
ui.addAuthenticatedRoutes([
|
|
22
|
-
{
|
|
23
|
-
// will match everything, try to render a legacy Oxygen page...
|
|
24
|
-
path: 'upcoming-events/:subpath*',
|
|
25
|
-
component: LegacyPage,
|
|
26
|
-
props: (route) => {
|
|
27
|
-
return {
|
|
28
|
-
fullPath: route.fullPath,
|
|
29
|
-
legacyPrefix: '/oxygen/view',
|
|
30
|
-
adminPrefix: '/oxygen'
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
]);
|
|
35
|
-
}
|