@oxygen-cms/ui 1.5.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/.babelrc +1 -0
- package/.eslintrc.js +22 -0
- package/.github/workflows/node.js.yml +29 -0
- package/.idea/modules.xml +8 -0
- package/.idea/ui.iml +10 -0
- package/.jshintrc +3 -0
- package/README.md +7 -0
- package/assets/oxygen-icon.png +0 -0
- package/jest.init.js +1 -0
- package/package.json +72 -0
- package/src/AuthApi.js +116 -0
- package/src/CrudApi.js +112 -0
- package/src/EventsApi.js +16 -0
- package/src/GroupsApi.js +9 -0
- package/src/Internationalize.js +31 -0
- package/src/MediaApi.js +52 -0
- package/src/MediaDirectoryApi.js +62 -0
- package/src/PreferencesApi.js +47 -0
- package/src/UserPermissions.js +66 -0
- package/src/UserPreferences.js +69 -0
- package/src/UserPreferences.test.js +23 -0
- package/src/UsersApi.js +41 -0
- package/src/api.js +209 -0
- package/src/components/App.vue +61 -0
- package/src/components/AuthenticatedLayout.vue +254 -0
- package/src/components/AuthenticationLog.vue +196 -0
- package/src/components/CodeEditor.vue +90 -0
- package/src/components/EditButtonOnRowHover.vue +21 -0
- package/src/components/Error404.vue +25 -0
- package/src/components/EventsChooser.vue +88 -0
- package/src/components/EventsTable.vue +82 -0
- package/src/components/GenericEditableField.vue +74 -0
- package/src/components/GroupsChooser.vue +58 -0
- package/src/components/GroupsList.vue +129 -0
- package/src/components/ImportExport.vue +45 -0
- package/src/components/LegacyPage.vue +256 -0
- package/src/components/UserJoined.vue +35 -0
- package/src/components/UserManagement.vue +168 -0
- package/src/components/UserProfileForm.vue +214 -0
- package/src/components/ViewProfile.vue +32 -0
- package/src/components/auth/Auth404.vue +16 -0
- package/src/components/auth/Login.vue +135 -0
- package/src/components/auth/LoginLogo.vue +30 -0
- package/src/components/auth/Logout.vue +26 -0
- package/src/components/auth/PasswordRemind.vue +71 -0
- package/src/components/auth/PasswordReset.vue +97 -0
- package/src/components/auth/TwoFactorSetup.vue +115 -0
- package/src/components/auth/VerifyEmail.vue +71 -0
- package/src/components/auth/WelcomeFloat.vue +87 -0
- package/src/components/auth/login.scss +17 -0
- package/src/components/media/MediaChooseDirectory.vue +129 -0
- package/src/components/media/MediaDirectory.vue +109 -0
- package/src/components/media/MediaInsertModal.vue +88 -0
- package/src/components/media/MediaItem.vue +282 -0
- package/src/components/media/MediaItemPreview.vue +45 -0
- package/src/components/media/MediaList.vue +305 -0
- package/src/components/media/MediaPage.vue +44 -0
- package/src/components/media/MediaResponsiveImages.vue +51 -0
- package/src/components/media/MediaUpload.vue +133 -0
- package/src/components/media/media.scss +51 -0
- package/src/components/preferences/PreferencesAdminAppearance.vue +22 -0
- package/src/components/preferences/PreferencesAuthentication.vue +27 -0
- package/src/components/preferences/PreferencesEventTemplates.vue +22 -0
- package/src/components/preferences/PreferencesField.vue +215 -0
- package/src/components/preferences/PreferencesList.vue +50 -0
- package/src/components/preferences/PreferencesPageTemplates.vue +23 -0
- package/src/components/preferences/PreferencesSiteAppearance.vue +22 -0
- package/src/components/preferences/PreferencesThemeChooser.vue +73 -0
- package/src/components/preferences/ShowIfPermitted.vue +37 -0
- package/src/components/preferences/UserPreferences.vue +30 -0
- package/src/components/users/CreateUserModal.vue +73 -0
- package/src/components/util.css +47 -0
- package/src/icons.js +90 -0
- package/src/main.js +112 -0
- package/src/modules/LegacyPages.js +18 -0
- package/src/modules/Media.js +45 -0
- package/src/modules/UserManagement.js +24 -0
- package/src/routes/index.js +92 -0
- package/src/store/index.js +70 -0
- package/src/styles/_variables.scss +23 -0
- package/src/styles/app.scss +76 -0
- package/src/unsavedChanges.js +16 -0
- package/src/util.js +65 -0
- package/src/util.test.js +39 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ================================
|
|
2
|
+
// Notification
|
|
3
|
+
// ================================
|
|
4
|
+
|
|
5
|
+
import AuthApi from "./AuthApi";
|
|
6
|
+
|
|
7
|
+
const isDefined = (o) => {
|
|
8
|
+
return typeof o !== 'undefined' && o !== null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
class UserPreferences {
|
|
12
|
+
|
|
13
|
+
constructor(preferences) {
|
|
14
|
+
this.preferences = preferences;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static setBuefy($buefy) {
|
|
18
|
+
this.$buefy = $buefy;
|
|
19
|
+
this.authApi = new AuthApi(this.$buefy);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(key, fallback = null) {
|
|
23
|
+
let o = this.preferences;
|
|
24
|
+
|
|
25
|
+
if(!isDefined(o)) {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var parts = key.split('.');
|
|
30
|
+
//var last = parts.pop();
|
|
31
|
+
var l = parts.length;
|
|
32
|
+
var i = 0;
|
|
33
|
+
|
|
34
|
+
while(isDefined(o) && i < l) {
|
|
35
|
+
var idx = parts[i];
|
|
36
|
+
o = o[idx];
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isDefined(o)) {
|
|
41
|
+
return o;
|
|
42
|
+
} else {
|
|
43
|
+
console.log('Preferences key ', key, 'was not defined, using default ', fallback);
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
has(key) {
|
|
49
|
+
let o = this.preferences;
|
|
50
|
+
if(!o) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var parts = key.split('.');
|
|
55
|
+
var l = parts.length;
|
|
56
|
+
var i = 1;
|
|
57
|
+
var current = parts[0];
|
|
58
|
+
|
|
59
|
+
while((o = o[current]) && i < l) {
|
|
60
|
+
current = parts[i];
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return !!o;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default UserPreferences;
|
|
69
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import UserPreferences from './UserPreferences';
|
|
2
|
+
|
|
3
|
+
jest.mock('./AuthApi');
|
|
4
|
+
|
|
5
|
+
test('gets and sets preferences', async () => {
|
|
6
|
+
UserPreferences.setBuefy({});
|
|
7
|
+
UserPreferences.authApi.userDetails.mockResolvedValue({
|
|
8
|
+
user: {
|
|
9
|
+
preferences: {
|
|
10
|
+
foo: 'bar',
|
|
11
|
+
baz: { qux: 'fub '}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let prefs = await UserPreferences.load();
|
|
17
|
+
|
|
18
|
+
expect(prefs.get('foo')).toBe('bar');
|
|
19
|
+
expect(prefs.has('fob')).toBe(false);
|
|
20
|
+
expect(prefs.has('baz.qux')).toBe(true);
|
|
21
|
+
expect(prefs.get('bar.qux2', 'fallback')).toBe('fallback');
|
|
22
|
+
|
|
23
|
+
});
|
package/src/UsersApi.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {API_ROOT, CrudApi} from './CrudApi';
|
|
2
|
+
|
|
3
|
+
export default class UsersApi extends CrudApi {
|
|
4
|
+
|
|
5
|
+
static prepareModelForAPI(data) {
|
|
6
|
+
let m = { ...data };
|
|
7
|
+
delete m.id;
|
|
8
|
+
if(m.group) {
|
|
9
|
+
m.group = m.group.id;
|
|
10
|
+
}
|
|
11
|
+
return m;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static getResourceName() {
|
|
15
|
+
return 'users';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async updateFullName(id, name) {
|
|
19
|
+
return this.request('put')
|
|
20
|
+
.withJson({
|
|
21
|
+
fullName: name
|
|
22
|
+
})
|
|
23
|
+
.fetch(API_ROOT + 'users/' + id + '/fullName');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async impersonate(id) {
|
|
27
|
+
return await this.request('post')
|
|
28
|
+
.fetch(API_ROOT + 'users/' + id + '/impersonate');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async forceDelete(id) {
|
|
32
|
+
return this.request('delete')
|
|
33
|
+
.fetch(this.constructor.getResourceRoot() + '/' + id + '/force');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async stopImpersonating() {
|
|
37
|
+
return await this.request('post')
|
|
38
|
+
.fetch(API_ROOT + 'users/stop-impersonating');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
var xsrfToken = null;
|
|
2
|
+
|
|
3
|
+
export const initCsrfCookie = async () => {
|
|
4
|
+
await window.fetch(
|
|
5
|
+
'/sanctum/csrf-cookie',
|
|
6
|
+
{
|
|
7
|
+
credentials: 'same-origin'
|
|
8
|
+
});
|
|
9
|
+
let cookiesObj = document.cookie
|
|
10
|
+
.split(';')
|
|
11
|
+
.reduce((res, c) => {
|
|
12
|
+
const [key, val] = c.trim().split('=').map(decodeURIComponent)
|
|
13
|
+
return Object.assign(res, { [key]: val });
|
|
14
|
+
}, {});
|
|
15
|
+
xsrfToken = cookiesObj['XSRF-TOKEN'];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class FetchBuilder {
|
|
19
|
+
constructor($buefy, method) {
|
|
20
|
+
this.$buefy = $buefy;
|
|
21
|
+
this.method = method;
|
|
22
|
+
this.headers = new Headers();
|
|
23
|
+
this.body = undefined;
|
|
24
|
+
}
|
|
25
|
+
wantJson() {
|
|
26
|
+
this.headers.set('Accept', 'application/json');
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
setContentType(type) {
|
|
30
|
+
this.headers.set('Content-Type', type);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
setBody(body) {
|
|
34
|
+
this.body = body;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
withJson(json) {
|
|
38
|
+
this.body = JSON.stringify(json);
|
|
39
|
+
this.setContentType('application/json');
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
withQueryParams(queryParams) {
|
|
44
|
+
this.queryParams = queryParams;
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
cookies() {
|
|
49
|
+
this.credentials = 'same-origin';
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async setXsrfTokenHeader() {
|
|
54
|
+
if(xsrfToken === null) {
|
|
55
|
+
await initCsrfCookie();
|
|
56
|
+
}
|
|
57
|
+
this.headers.set('X-XSRF-TOKEN', xsrfToken);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async fetch(url) {
|
|
61
|
+
await this.setXsrfTokenHeader()
|
|
62
|
+
|
|
63
|
+
let v = { ... this};
|
|
64
|
+
v.queryParams = undefined;
|
|
65
|
+
|
|
66
|
+
if(this.queryParams) {
|
|
67
|
+
url = new URL(url, window.location);
|
|
68
|
+
for(let name in this.queryParams) {
|
|
69
|
+
if(Object.prototype.hasOwnProperty.call(this.queryParams, name) && this.queryParams[name] !== null) {
|
|
70
|
+
url.searchParams.append(name, this.queryParams[name]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let response = await window.fetch(url.toString(), this);
|
|
76
|
+
|
|
77
|
+
let data = {};
|
|
78
|
+
try {
|
|
79
|
+
data = await response.json();
|
|
80
|
+
} catch(e) {
|
|
81
|
+
if(response.status === 413) {
|
|
82
|
+
this.$buefy.notification.open({
|
|
83
|
+
message: 'Upload failed: file(s) too large',
|
|
84
|
+
type: 'is-danger',
|
|
85
|
+
animation: 'fade',
|
|
86
|
+
queue: false
|
|
87
|
+
});
|
|
88
|
+
return {};
|
|
89
|
+
} else if(response.status === 204) {
|
|
90
|
+
// no content, we're okay
|
|
91
|
+
} else {
|
|
92
|
+
console.error('Response did not contain valid JSON: ', e);
|
|
93
|
+
this.$buefy.notification.open({
|
|
94
|
+
message: 'Whoops, looks like something went wrong.',
|
|
95
|
+
type: 'is-warning',
|
|
96
|
+
queue: false
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if(response.ok && data.status !== 'failed') {
|
|
104
|
+
return data;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
handleAPIError(data, this.$buefy, FetchBuilder.router, response);
|
|
108
|
+
let e = new Error('Received an error response from API call');
|
|
109
|
+
e.response = data;
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static default($buefy, method) {
|
|
114
|
+
return (new FetchBuilder($buefy, method))
|
|
115
|
+
.cookies()
|
|
116
|
+
.wantJson();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static setRouter(router) {
|
|
120
|
+
FetchBuilder.router = router;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function statusToBueify(status) {
|
|
125
|
+
if(status === 'failed') {
|
|
126
|
+
return 'is-danger';
|
|
127
|
+
} else {
|
|
128
|
+
return `is-${status}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function morphToNotification(data) {
|
|
133
|
+
return {
|
|
134
|
+
message: data.content,
|
|
135
|
+
type: statusToBueify(data.status),
|
|
136
|
+
indefinite: data.duration === 'indefinite',
|
|
137
|
+
duration: data.duration ? data.duration : 4000,
|
|
138
|
+
queue: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleAPIError = function(content, $buefy, $router, response) {
|
|
143
|
+
console.error('API error: ', content);
|
|
144
|
+
if(response.status === 401 && content.code === 'unauthenticated') {
|
|
145
|
+
// server is telling us to login again
|
|
146
|
+
initCsrfCookie()
|
|
147
|
+
.then(() => {
|
|
148
|
+
$router.push({path: '/auth/login', query: {redirect: $router.currentRoute.fullPath}});
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
} else if(response.status === 403 && content.code === 'two_factor_setup_required') {
|
|
152
|
+
$router.push({ path: '/auth/2fa-setup' });
|
|
153
|
+
return;
|
|
154
|
+
} else if(response.status === 403 && content.code === 'email_unverified') {
|
|
155
|
+
$router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
|
|
156
|
+
return;
|
|
157
|
+
} else if(response.status === 429) {
|
|
158
|
+
$buefy.notification.open({
|
|
159
|
+
message: 'Too many requests within a short timeframe. Please wait.',
|
|
160
|
+
type: 'is-warning',
|
|
161
|
+
duration: 10000,
|
|
162
|
+
queue: false
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// handle generic validation errors
|
|
168
|
+
if(typeof content.errors === 'object') {
|
|
169
|
+
for(const [, errors ] of Object.entries(content.errors)) {
|
|
170
|
+
for(let error of errors) {
|
|
171
|
+
$buefy.notification.open({
|
|
172
|
+
message: error,
|
|
173
|
+
duration: 4000,
|
|
174
|
+
queue: false,
|
|
175
|
+
type: 'is-warning'
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if(content.content && content.status) {
|
|
183
|
+
$buefy.notification.open(morphToNotification(content));
|
|
184
|
+
} else if(content.exception) {
|
|
185
|
+
$buefy.notification.open({
|
|
186
|
+
message:
|
|
187
|
+
'PHP Exception of type <pre class="no-pre">' + content.exception +
|
|
188
|
+
'</pre> with message <pre class="no-pre">' + content.message +
|
|
189
|
+
'</pre> thrown at <pre class="no-pre">' + content.file + ':' + content.line +
|
|
190
|
+
'</pre>',
|
|
191
|
+
duration: 20000,
|
|
192
|
+
animation: 'fade',
|
|
193
|
+
type: 'is-danger'
|
|
194
|
+
});
|
|
195
|
+
} else if(response.status === 500) {
|
|
196
|
+
$buefy.notification.open({
|
|
197
|
+
message:'Whoops, looks like something went wrong.',
|
|
198
|
+
type: 'is-danger',
|
|
199
|
+
animation: 'fade',
|
|
200
|
+
queue: false
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export function getXsrfToken() {
|
|
206
|
+
return xsrfToken;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
FetchBuilder.router = null;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div style="height: 100%;">
|
|
3
|
+
<!-- <transition name="slide-left" mode="out-in">-->
|
|
4
|
+
<router-view @logout="signOut">
|
|
5
|
+
<template #main-navigation>
|
|
6
|
+
<slot name="app-navigation"></slot>
|
|
7
|
+
</template>
|
|
8
|
+
</router-view>
|
|
9
|
+
<!-- </transition>-->
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import AuthApi from "../AuthApi";
|
|
15
|
+
export default {
|
|
16
|
+
name: "App",
|
|
17
|
+
props: {
|
|
18
|
+
appTitle: { type: String, required: true },
|
|
19
|
+
defaultRouteTitle: { type: String, required: true },
|
|
20
|
+
impersonating: {
|
|
21
|
+
type: Boolean,
|
|
22
|
+
default: false
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
data() {
|
|
26
|
+
return {
|
|
27
|
+
user: null,
|
|
28
|
+
authApi: new AuthApi(this.$buefy),
|
|
29
|
+
userPermissions: null
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
watch: {
|
|
33
|
+
'$route' (to) {
|
|
34
|
+
this.setTitle(to.meta.title);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
created() {
|
|
38
|
+
this.setTitle(this.$route.meta.title);
|
|
39
|
+
this.$root.$on('update-route-title', (title) => {
|
|
40
|
+
this.setTitle(title);
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
methods: {
|
|
44
|
+
setTitle(title) {
|
|
45
|
+
document.title = (title || this.defaultRouteTitle) + ' - ' + this.appTitle;
|
|
46
|
+
},
|
|
47
|
+
async signOut() {
|
|
48
|
+
await this.authApi.logout();
|
|
49
|
+
this.$store.commit('setUser', null);
|
|
50
|
+
await this.$router.push('/auth/logout');
|
|
51
|
+
},
|
|
52
|
+
stopImpersonating() {
|
|
53
|
+
this.authApi.stopImpersonating();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<style lang="scss">
|
|
60
|
+
@import '../styles/app.scss';
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="is-flex is-flex-direction-row" style="height: 100%;">
|
|
3
|
+
<div :class="'left-navigation-container' + (collapsed ? ' is-collapsed' : '')">
|
|
4
|
+
|
|
5
|
+
<div class="app-logo-title">
|
|
6
|
+
<router-link to="/" class="app-logo-title-link">
|
|
7
|
+
<img src="../../assets/oxygen-icon.png" alt="Oxygen CMS" class="app-logo">
|
|
8
|
+
<router-link v-if="!collapsed" to="/" class="app-title">Oxygen CMS</router-link>
|
|
9
|
+
</router-link>
|
|
10
|
+
<span class="is-flex-grow-1"></span>
|
|
11
|
+
<b-button v-if="!requestedCollapsed" type="is-light" :icon-left="collapsed ? 'angle-right' : 'angle-left'" class="collapse-menu-button" @click="setCollapsed = !setCollapsed"></b-button>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<b-menu class="left-navigation">
|
|
15
|
+
<slot name="main-navigation" :collapsed="collapsed"></slot>
|
|
16
|
+
</b-menu>
|
|
17
|
+
|
|
18
|
+
<div :class="'user-info' + (impersonating ? ' has-background-warning' : '')">
|
|
19
|
+
<b-dropdown aria-role="list" :position="collapsed ? 'is-top-right' : 'is-top-left'" expanded>
|
|
20
|
+
<template #trigger>
|
|
21
|
+
<div class="user-dropdown">
|
|
22
|
+
<div v-if="!collapsed" class="user-dropdown-text">
|
|
23
|
+
<strong v-if="impersonating">Temporarily logged-in as<br/></strong>
|
|
24
|
+
<transition name="fade" mode="out-in">
|
|
25
|
+
<span v-if="user">{{ user.fullName }}</span>
|
|
26
|
+
<b-skeleton v-else size="is-medium" width="10em" :animated="true"></b-skeleton>
|
|
27
|
+
</transition>
|
|
28
|
+
<transition name="fade" mode="out-in">
|
|
29
|
+
<p v-if="user" class="is-size-7">{{ user.email }}</p>
|
|
30
|
+
<b-skeleton v-else width="8em" :animated="true"></b-skeleton>
|
|
31
|
+
</transition>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="is-flex-grow-1"></div>
|
|
34
|
+
<div class="has-background-grey-light centered-icon" style="display: inline-block;">
|
|
35
|
+
<b-icon icon="user" size="is-large" class="has-text-grey-lighter"></b-icon>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
<b-dropdown-item aria-role="listitem" has-link><router-link to="/user/profile"><b-icon icon="user-edit"></b-icon>Profile</router-link></b-dropdown-item>
|
|
40
|
+
<b-dropdown-item aria-role="listitem" has-link><router-link to="/user/login-log"><b-icon icon="lock"></b-icon>Account Security</router-link></b-dropdown-item>
|
|
41
|
+
<b-dropdown-item separator></b-dropdown-item>
|
|
42
|
+
<b-dropdown-item aria-role="listitem" @click="$emit('logout')"><b-icon icon="sign-out-alt"></b-icon>Sign Out</b-dropdown-item>
|
|
43
|
+
<b-dropdown-item v-if="impersonating" aria-role="listitem" @click="stopImpersonating"><b-icon icon="times"></b-icon>Stop impersonating</b-dropdown-item>
|
|
44
|
+
</b-dropdown>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="no-pad full-height-container content-column">
|
|
49
|
+
|
|
50
|
+
<transition name="slide-up" mode="out-in">
|
|
51
|
+
<router-view></router-view>
|
|
52
|
+
</transition>
|
|
53
|
+
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
import UsersApi from "../UsersApi";
|
|
60
|
+
import {morphToNotification} from "../api";
|
|
61
|
+
import {isNavigationFailure, NavigationFailureType} from "vue-router/src/util/errors";
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
name: "AuthenticatedLayout",
|
|
65
|
+
emits: ["logout"],
|
|
66
|
+
data() {
|
|
67
|
+
return {
|
|
68
|
+
usersApi: new UsersApi(this.$buefy),
|
|
69
|
+
setCollapsed: false,
|
|
70
|
+
requestedCollapsed: false
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
computed: {
|
|
74
|
+
impersonating() { return this.$store.state.impersonating; },
|
|
75
|
+
user() { return this.$store.state.user; },
|
|
76
|
+
userPreferences() { return this.$store.getters.userPreferences; },
|
|
77
|
+
collapsed() {
|
|
78
|
+
return this.setCollapsed || this.requestedCollapsed;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
mounted() {
|
|
82
|
+
console.log('mounted', this.$store);
|
|
83
|
+
this.setGlobalFontSize()
|
|
84
|
+
},
|
|
85
|
+
methods: {
|
|
86
|
+
setGlobalFontSize() {
|
|
87
|
+
let fontSize = this.userPreferences.get('fontSize', '100%');
|
|
88
|
+
console.log('Setting global font size to ', fontSize);
|
|
89
|
+
if(fontSize !== '100%') {
|
|
90
|
+
window.document.documentElement.style.fontSize = fontSize;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async stopImpersonating() {
|
|
94
|
+
let response = await this.usersApi.stopImpersonating();
|
|
95
|
+
this.$buefy.notification.open(morphToNotification(response));
|
|
96
|
+
this.$store.commit('stopImpersonating', response.user);
|
|
97
|
+
// ignore duplicated navigation failure
|
|
98
|
+
await this.$router.push({ path: '/' }).catch(failure => {
|
|
99
|
+
if(!isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
|
100
|
+
throw failure;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<style scoped lang="scss">
|
|
109
|
+
@import 'util.css';
|
|
110
|
+
@import '../styles/_variables.scss';
|
|
111
|
+
|
|
112
|
+
.left-navigation-container {
|
|
113
|
+
padding: 0 !important;
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
flex: 1;
|
|
117
|
+
max-width: 550px;
|
|
118
|
+
border-right: 1px solid $grey-lighter;
|
|
119
|
+
transition: max-width 0.5s ease;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.left-navigation-container.is-collapsed {
|
|
123
|
+
max-width: 5rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.app-logo-title {
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
padding: 1rem 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.app-logo {
|
|
133
|
+
width: 3rem;
|
|
134
|
+
margin-left: 1rem;
|
|
135
|
+
margin-right: 1rem;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.app-logo-title-link {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.app-title {
|
|
144
|
+
color: $text;
|
|
145
|
+
font-size: 1.1rem;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.app-container {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: row;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.left-navigation {
|
|
154
|
+
flex: 1;
|
|
155
|
+
padding: 1rem 1rem 1rem 2rem;
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
overflow-x: hidden;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.content-column {
|
|
161
|
+
flex: 4;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
::v-deep .router-link-exact-active {
|
|
165
|
+
// background-color: $link-invert !important;
|
|
166
|
+
color: $black-bis !important;
|
|
167
|
+
font-weight: 700;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
::v-deep .menu-list .icon {
|
|
171
|
+
margin-right: 1rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.user-dropdown {
|
|
175
|
+
padding: 1rem;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.columns {
|
|
182
|
+
margin: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.no-pad {
|
|
186
|
+
padding: 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.content-column {
|
|
190
|
+
background-color: $white-ter;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.dropdown-content .icon {
|
|
194
|
+
margin-right: 0.5rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.app-logo-title:hover .collapse-menu-button {
|
|
198
|
+
opacity: 1.0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.collapse-menu-button {
|
|
202
|
+
opacity: 0;
|
|
203
|
+
transition: opacity 0.2s ease;
|
|
204
|
+
margin-right: 1rem;
|
|
205
|
+
z-index: 10;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.is-collapsed {
|
|
209
|
+
.collapse-menu-button {
|
|
210
|
+
margin-left: 1rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.left-navigation {
|
|
214
|
+
padding: 0;
|
|
215
|
+
text-align: center;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
::v-deep .menu-label {
|
|
219
|
+
text-indent: -9999px;
|
|
220
|
+
height: 0;
|
|
221
|
+
border-bottom: 1px solid $grey-lighter;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
::v-deep .icon-text > span:not(.icon) {
|
|
225
|
+
display: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
::v-deep .menu-list .icon {
|
|
229
|
+
margin-right: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
::v-deep .menu-list li ul {
|
|
233
|
+
border-left: 0;
|
|
234
|
+
margin: 0;
|
|
235
|
+
padding-left: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.app-logo-title-link {
|
|
239
|
+
width: 5rem;
|
|
240
|
+
flex-direction: column;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.app-logo-title {
|
|
244
|
+
display: block;
|
|
245
|
+
position: relative;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.collapse-menu-button {
|
|
249
|
+
position: absolute;
|
|
250
|
+
top: 1rem;
|
|
251
|
+
left: 4.5rem;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
</style>
|