@oxygen-cms/ui 1.4.0 → 1.5.2
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 +23 -0
- package/.github/workflows/node.js.yml +4 -4
- package/.idea/modules.xml +8 -0
- package/.idea/ui.iml +10 -0
- package/package.json +13 -5
- package/src/AuthApi.js +77 -42
- package/src/CrudApi.js +3 -3
- package/src/GroupsApi.js +9 -0
- package/src/MediaDirectoryApi.js +1 -1
- package/src/PreferencesApi.js +2 -0
- package/src/UserPermissions.js +2 -9
- package/src/UserPreferences.js +0 -4
- package/src/UserPreferences.test.js +0 -2
- package/src/UsersApi.js +41 -0
- package/src/api.js +96 -38
- package/src/components/App.vue +19 -240
- package/src/components/AuthenticatedLayout.vue +254 -0
- package/src/components/AuthenticationLog.vue +86 -30
- package/src/components/CodeEditor.vue +16 -32
- package/src/components/EditButtonOnRowHover.vue +21 -0
- package/src/components/Error404.vue +15 -5
- package/src/components/EventsChooser.vue +11 -11
- package/src/components/EventsTable.vue +14 -8
- 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 +32 -1
- package/src/components/LegacyPage.vue +22 -23
- 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 +7 -219
- 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/{MediaChooseDirectory.vue → media/MediaChooseDirectory.vue} +12 -12
- package/src/components/{MediaDirectory.vue → media/MediaDirectory.vue} +8 -8
- package/src/components/{MediaInsertModal.vue → media/MediaInsertModal.vue} +2 -2
- package/src/components/{MediaItem.vue → media/MediaItem.vue} +24 -23
- package/src/components/{MediaItemPreview.vue → media/MediaItemPreview.vue} +5 -5
- package/src/components/{MediaList.vue → media/MediaList.vue} +42 -38
- package/src/components/{MediaPage.vue → media/MediaPage.vue} +1 -1
- package/src/components/{MediaResponsiveImages.vue → media/MediaResponsiveImages.vue} +5 -5
- package/src/components/{MediaUpload.vue → media/MediaUpload.vue} +10 -10
- package/src/components/{media.scss → media/media.scss} +1 -1
- package/src/components/preferences/PreferencesField.vue +10 -10
- package/src/components/preferences/PreferencesList.vue +13 -20
- package/src/components/preferences/PreferencesThemeChooser.vue +9 -9
- package/src/components/preferences/ShowIfPermitted.vue +9 -14
- package/src/components/users/CreateUserModal.vue +73 -0
- package/src/icons.js +90 -0
- package/src/main.js +111 -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 +1 -0
- package/src/styles/app.scss +15 -2
- package/src/login.js +0 -17
- package/src/routes.js +0 -61
- package/src/styles/login.scss +0 -86
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="full-height scroll-container pad">
|
|
3
3
|
<div class="box">
|
|
4
|
-
|
|
4
|
+
<h1 class="title">Active Sessions <b-button icon-left="redo-alt" rounded @click="fetchSessions"></b-button>
|
|
5
|
+
</h1>
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
<b-table :data="sessions.items === null ? [] : sessions.items"
|
|
9
|
+
:loading="sessions.loading">
|
|
10
|
+
<b-table-column v-slot="props" label="Browser / Device">
|
|
11
|
+
<b-tooltip :label="props.row.userAgent" animated>{{ parseUserAgent(props.row.userAgent) }}</b-tooltip>
|
|
12
|
+
</b-table-column>
|
|
13
|
+
|
|
14
|
+
<b-table-column v-slot="props" label="IP Address">
|
|
15
|
+
{{ props.row.ipAddress }}
|
|
16
|
+
</b-table-column>
|
|
17
|
+
|
|
18
|
+
<b-table-column v-slot="props" label="Location">
|
|
19
|
+
<div v-if="props.row.geolocationInfo !== null">
|
|
20
|
+
{{ props.row.geolocationInfo }}
|
|
21
|
+
</div>
|
|
22
|
+
<b-progress v-else></b-progress>
|
|
23
|
+
</b-table-column>
|
|
24
|
+
|
|
25
|
+
<b-table-column v-slot="props" label="Last Activity">
|
|
26
|
+
{{ Internationalize.formatDateTime(props.row.lastActivity) }}
|
|
27
|
+
</b-table-column>
|
|
28
|
+
|
|
29
|
+
<b-table-column v-slot="props" label="">
|
|
30
|
+
<b-tag v-if="props.row.current" type="is-success" style="text-transform: uppercase">Current Session</b-tag>
|
|
31
|
+
<b-button v-else size="is-small" type="is-warning" icon-left="sign-out-alt" @click="deleteSession(props.row.id)">Remove session</b-button>
|
|
32
|
+
</b-table-column>
|
|
33
|
+
</b-table>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="box">
|
|
37
|
+
<h1 class="title">Logins, Logouts & Login Attempts <b-button icon-left="redo-alt" rounded @click="paginatedItems.currentPage = 1; fetchLogins"></b-button></h1>
|
|
5
38
|
|
|
6
39
|
<b-table :data="paginatedItems.items === null ? [] : paginatedItems.items"
|
|
7
40
|
:loading="paginatedItems.loading"
|
|
@@ -11,11 +44,11 @@
|
|
|
11
44
|
:total="paginatedItems.totalItems"
|
|
12
45
|
:row-class="(row) => row.type === 1 ? 'is-danger' : ''"
|
|
13
46
|
@page-change="paginatedItems.currentPage = $event">
|
|
14
|
-
<b-table-column label="IP Address"
|
|
47
|
+
<b-table-column v-slot="props" label="IP Address">
|
|
15
48
|
{{ props.row.ipAddress }}
|
|
16
49
|
</b-table-column>
|
|
17
50
|
|
|
18
|
-
<b-table-column
|
|
51
|
+
<b-table-column v-slot="props" label="Location">
|
|
19
52
|
<div v-if="props.row.geolocationInfo !== null">
|
|
20
53
|
{{ props.row.geolocationInfo }}
|
|
21
54
|
</div>
|
|
@@ -23,17 +56,25 @@
|
|
|
23
56
|
|
|
24
57
|
</b-table-column>
|
|
25
58
|
|
|
26
|
-
<b-table-column label="Browser / Device"
|
|
59
|
+
<b-table-column v-slot="props" label="Browser / Device">
|
|
27
60
|
<b-tooltip :label="props.row.userAgent" animated>{{ parseUserAgent(props.row.userAgent) }}</b-tooltip>
|
|
28
61
|
|
|
29
62
|
</b-table-column>
|
|
30
63
|
|
|
31
|
-
<b-table-column
|
|
64
|
+
<b-table-column v-slot="props" label="Time">
|
|
32
65
|
{{ Internationalize.formatDateTime(props.row.timestamp) }}
|
|
33
66
|
</b-table-column>
|
|
34
67
|
|
|
35
|
-
<b-table-column
|
|
36
|
-
|
|
68
|
+
<b-table-column v-slot="props" label="Type">
|
|
69
|
+
<span v-if="props.row.type === 0">
|
|
70
|
+
<b-tag type="is-success is-light"><b-icon icon="sign-in-alt" /> Login</b-tag>
|
|
71
|
+
</span>
|
|
72
|
+
<span v-else-if="props.row.type === 1">
|
|
73
|
+
<b-tag type="is-danger is-light"><b-icon icon="exclamation-triangle" /> Failed Login</b-tag>
|
|
74
|
+
</span>
|
|
75
|
+
<span v-else-if="props.row.type === 2">
|
|
76
|
+
<b-tag><b-icon icon="sign-out-alt" /> Logout</b-tag>
|
|
77
|
+
</span>
|
|
37
78
|
</b-table-column>
|
|
38
79
|
</b-table>
|
|
39
80
|
</div>
|
|
@@ -44,6 +85,7 @@
|
|
|
44
85
|
import {FetchBuilder} from "../api";
|
|
45
86
|
import Internationalize from "../Internationalize";
|
|
46
87
|
import UAParser from 'ua-parser-js';
|
|
88
|
+
import AuthApi from "../AuthApi";
|
|
47
89
|
|
|
48
90
|
const IP_INFO_LOADING = 'loading';
|
|
49
91
|
|
|
@@ -51,7 +93,9 @@ export default {
|
|
|
51
93
|
name: "AuthenticationLog",
|
|
52
94
|
data() {
|
|
53
95
|
return {
|
|
96
|
+
authApi: new AuthApi(this.$buefy),
|
|
54
97
|
paginatedItems: { items: null, currentPage: 1, loading: true, totalItems: null, itemsPerPage: null },
|
|
98
|
+
sessions: { items: null, loading: true },
|
|
55
99
|
ipInfo: new Map(),
|
|
56
100
|
Internationalize
|
|
57
101
|
}
|
|
@@ -63,20 +107,32 @@ export default {
|
|
|
63
107
|
this.fetchData()
|
|
64
108
|
},
|
|
65
109
|
methods: {
|
|
110
|
+
async fetchSessions() {
|
|
111
|
+
this.sessions.loading = true;
|
|
112
|
+
let response = await this.authApi.listUserSessions();
|
|
113
|
+
this.sessions.items = response.sessions.map((item) => { return { geolocationInfo: this.ipInfo.get(item.ipAddress), ...item } });
|
|
114
|
+
this.sessions.loading = false;
|
|
115
|
+
for(let item of this.sessions.items) {
|
|
116
|
+
this.geoIP(item.ipAddress);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
async fetchLogins() {
|
|
120
|
+
this.paginatedItems.loading = true;
|
|
121
|
+
let data = await FetchBuilder
|
|
122
|
+
.default(this.$buefy, 'post')
|
|
123
|
+
.withQueryParams({ page: this.paginatedItems.currentPage })
|
|
124
|
+
.fetch('/oxygen/api/auth/login-log-entries');
|
|
125
|
+
|
|
126
|
+
this.paginatedItems.items = data.items.map((item) => { return { geolocationInfo: this.ipInfo.get(item.ipAddress), ...item } });
|
|
127
|
+
this.paginatedItems.totalItems = data.totalItems;
|
|
128
|
+
this.paginatedItems.loading = false;
|
|
129
|
+
this.paginatedItems.itemsPerPage = data.itemsPerPage;
|
|
130
|
+
for(let item of this.paginatedItems.items) {
|
|
131
|
+
this.geoIP(item.ipAddress);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
66
134
|
async fetchData() {
|
|
67
|
-
this.
|
|
68
|
-
let data = await FetchBuilder
|
|
69
|
-
.default(this.$buefy, 'post')
|
|
70
|
-
.withQueryParams({ page: this.paginatedItems.currentPage })
|
|
71
|
-
.fetch('/oxygen/api/auth/login-log-entries');
|
|
72
|
-
|
|
73
|
-
this.paginatedItems.items = data.items.map((item) => { return { geolocationInfo: this.ipInfo.get(item.ip), ...item } });
|
|
74
|
-
this.paginatedItems.totalItems = data.totalItems;
|
|
75
|
-
this.paginatedItems.loading = false;
|
|
76
|
-
this.paginatedItems.itemsPerPage = data.itemsPerPage;
|
|
77
|
-
for(let item of this.paginatedItems.items) {
|
|
78
|
-
this.geoIP(item.ipAddress);
|
|
79
|
-
}
|
|
135
|
+
await Promise.all([this.fetchSessions(), this.fetchLogins()]);
|
|
80
136
|
},
|
|
81
137
|
geoIP(ip) {
|
|
82
138
|
if(this.ipInfo.has(ip)) {
|
|
@@ -97,17 +153,21 @@ export default {
|
|
|
97
153
|
},
|
|
98
154
|
updateInfoForIp(ip) {
|
|
99
155
|
let geolocationInfo = this.ipInfo.has(ip) ? this.getGeolocationInfo(this.ipInfo.get(ip)) : '';
|
|
100
|
-
for(let item of this.paginatedItems.items) {
|
|
156
|
+
for(let item of (this.paginatedItems.items || [])) {
|
|
101
157
|
if(item.ipAddress === ip) {
|
|
102
158
|
item.geolocationInfo = geolocationInfo;
|
|
103
159
|
}
|
|
104
160
|
}
|
|
161
|
+
for(let item of (this.sessions.items || [])) {
|
|
162
|
+
if(item.ipAddress === ip) {
|
|
163
|
+
item.geolocationInfo = geolocationInfo;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
105
166
|
},
|
|
106
167
|
parseUserAgent(userAgent) {
|
|
107
168
|
let ua = new UAParser(userAgent);
|
|
108
169
|
let browser = ua.getBrowser();
|
|
109
170
|
let device = ua.getDevice();
|
|
110
|
-
// console.log(device);
|
|
111
171
|
return browser.name + ' ' + browser.version + ' on ' + ua.getOS().name + ', ' + (device.vendor ? device.vendor : '(unknown device)');
|
|
112
172
|
},
|
|
113
173
|
getGeolocationInfo(data) {
|
|
@@ -116,14 +176,10 @@ export default {
|
|
|
116
176
|
}
|
|
117
177
|
return data.city + ', ' + data.country_name;
|
|
118
178
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return 'Login Failed';
|
|
124
|
-
} else if(data.type === 2) {
|
|
125
|
-
return 'Logout';
|
|
126
|
-
}
|
|
179
|
+
async deleteSession(id) {
|
|
180
|
+
await this.authApi.deleteUserSession(id);
|
|
181
|
+
this.$buefy.notification.open({ message: 'User session removed', type: 'is-warning' });
|
|
182
|
+
await this.fetchData();
|
|
127
183
|
}
|
|
128
184
|
}
|
|
129
185
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="editor-container has-background-grey-darker" :style="'height: ' + height + ';'">
|
|
3
|
-
|
|
4
|
-
<b-loading :is-full-page="false" v-model="loading" class="load-screen"></b-loading>
|
|
5
|
-
|
|
6
3
|
<transition name="fade">
|
|
7
4
|
<AceEditor
|
|
8
|
-
v-if="!loading"
|
|
9
5
|
key="editor"
|
|
6
|
+
ref="ace"
|
|
10
7
|
:value="value"
|
|
11
|
-
@input="$emit('input', $event)"
|
|
12
|
-
@init="editorInit"
|
|
13
8
|
:lang="lang"
|
|
14
9
|
:theme="theme"
|
|
15
10
|
width="100%"
|
|
16
11
|
height="100%"
|
|
17
|
-
ref="ace"
|
|
18
12
|
:options="{
|
|
19
13
|
enableBasicAutocompletion: true,
|
|
20
14
|
enableLiveAutocompletion: true,
|
|
@@ -28,59 +22,53 @@
|
|
|
28
22
|
showInvisibles: showInvisibles,
|
|
29
23
|
showGutter: true,
|
|
30
24
|
}"
|
|
25
|
+
@input="$emit('input', $event)"
|
|
26
|
+
@init="editorInit"
|
|
31
27
|
/>
|
|
32
28
|
</transition>
|
|
33
|
-
|
|
34
29
|
</div>
|
|
35
30
|
</template>
|
|
36
31
|
|
|
37
32
|
<script>
|
|
38
|
-
import UserPreferences from "../UserPreferences";
|
|
39
33
|
|
|
40
34
|
export default {
|
|
41
35
|
name: "CodeEditor",
|
|
36
|
+
components: { AceEditor: require('vue2-ace-editor') },
|
|
42
37
|
props: {
|
|
43
|
-
value: String,
|
|
44
|
-
height: String,
|
|
45
|
-
lang: String
|
|
38
|
+
value: { type: String, default: null },
|
|
39
|
+
height: { type: String, required: true },
|
|
40
|
+
lang: { type: String, required: true }
|
|
46
41
|
},
|
|
47
42
|
data() {
|
|
48
43
|
return {
|
|
49
|
-
loading: true,
|
|
50
|
-
userPreferences: {}
|
|
51
44
|
}
|
|
52
45
|
},
|
|
53
46
|
computed: {
|
|
54
47
|
wrapMode() {
|
|
55
|
-
return this.userPreferences.get('editor.ace.wordWrap');
|
|
48
|
+
return this.$store.getters.userPreferences.get('editor.ace.wordWrap');
|
|
56
49
|
},
|
|
57
50
|
highlightActiveLine() {
|
|
58
|
-
return this.userPreferences.get('editor.ace.highlightActiveLine');
|
|
51
|
+
return this.$store.getters.userPreferences.get('editor.ace.highlightActiveLine');
|
|
59
52
|
},
|
|
60
53
|
showPrintMargin() {
|
|
61
|
-
return this.userPreferences.get('editor.ace.showPrintMargin');
|
|
54
|
+
return this.$store.getters.userPreferences.get('editor.ace.showPrintMargin');
|
|
62
55
|
},
|
|
63
56
|
showInvisibles() {
|
|
64
|
-
return this.userPreferences.get('editor.ace.showInvisibles');
|
|
57
|
+
return this.$store.getters.userPreferences.get('editor.ace.showInvisibles');
|
|
65
58
|
},
|
|
66
59
|
theme() {
|
|
67
|
-
return this.userPreferences.get('editor.ace.theme').replace('ace/theme/', '');
|
|
60
|
+
return this.$store.getters.userPreferences.get('editor.ace.theme').replace('ace/theme/', '');
|
|
68
61
|
},
|
|
69
62
|
fontSize() {
|
|
70
|
-
return this.userPreferences.get('editor.ace.fontSize');
|
|
63
|
+
return this.$store.getters.userPreferences.get('editor.ace.fontSize');
|
|
71
64
|
}
|
|
72
65
|
},
|
|
73
|
-
async mounted() {
|
|
74
|
-
this.userPreferences = await UserPreferences.load();
|
|
75
|
-
this.loading = false;
|
|
76
|
-
},
|
|
77
|
-
components: { AceEditor: require('vue2-ace-editor') },
|
|
78
66
|
methods: {
|
|
79
67
|
editorInit() {
|
|
80
68
|
require('brace/ext/language_tools') //language extension prerequsite...
|
|
81
|
-
require(
|
|
82
|
-
require(
|
|
83
|
-
require(
|
|
69
|
+
require(`brace/mode/${this.lang}`);
|
|
70
|
+
require(`brace/theme/${this.theme}`);
|
|
71
|
+
require(`brace/snippets/${this.lang}`);
|
|
84
72
|
|
|
85
73
|
// ignore first missing DOCTYPE warning
|
|
86
74
|
let session = this.$refs.ace.editor.getSession();
|
|
@@ -99,8 +87,4 @@ export default {
|
|
|
99
87
|
width: 100%;
|
|
100
88
|
position: relative;
|
|
101
89
|
}
|
|
102
|
-
|
|
103
|
-
.load-screen ::v-deep .loading-background {
|
|
104
|
-
background-color: transparent;
|
|
105
|
-
}
|
|
106
90
|
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-button size="is-small" type="is-text" icon-right="pencil-alt" class="show-on-hover" @click="edit" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
export default {
|
|
7
|
+
name: "EditButtonOnRowHover",
|
|
8
|
+
props: { edit: { type: Function, required: true }}
|
|
9
|
+
}
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<style scoped>
|
|
13
|
+
.show-on-hover {
|
|
14
|
+
opacity: 0;
|
|
15
|
+
transition: opacity 0.2s ease;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
tr:hover .show-on-hover {
|
|
19
|
+
opacity: 1;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<div class="hero is-fullheight has-background-white-ter">
|
|
3
|
+
<div class="hero-body has-text-centered is-justify-content-center">
|
|
4
|
+
<div>
|
|
5
|
+
<h1 class="title">Page Not Found</h1>
|
|
6
|
+
<p>Perhaps you have mistyped the URL or clicked on a dead link?</p>
|
|
7
|
+
<br />
|
|
8
|
+
<b-button tag="router-link" to="/" type="is-primary">Back to Dashboard</b-button>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
</div>
|
|
5
14
|
</template>
|
|
6
15
|
|
|
7
16
|
<script>
|
|
8
17
|
export default {
|
|
9
|
-
name: "Error404"
|
|
18
|
+
name: "Error404",
|
|
19
|
+
components: {}
|
|
10
20
|
}
|
|
11
21
|
</script>
|
|
12
22
|
|
|
13
23
|
<style scoped>
|
|
14
24
|
|
|
15
|
-
</style>
|
|
25
|
+
</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<b-modal :active="active"
|
|
3
|
-
trap-focus
|
|
2
|
+
<b-modal :active="active" trap-focus
|
|
4
3
|
aria-role="dialog"
|
|
5
|
-
aria-modal
|
|
4
|
+
aria-modal
|
|
5
|
+
@update:active="arg => $emit('update:active', arg)">
|
|
6
6
|
<div class="modal-card" style="width: auto">
|
|
7
7
|
<header class="modal-card-head">
|
|
8
8
|
<p class="modal-card-title">
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
<section class="modal-card-body full-height-container">
|
|
17
17
|
<slot name="explanation"></slot>
|
|
18
18
|
<b-field>
|
|
19
|
-
<b-input
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
<b-input v-model="searchQuery"
|
|
20
|
+
placeholder="Search..."
|
|
21
|
+
type="search" icon="search" rounded>
|
|
22
22
|
</b-input>
|
|
23
23
|
</b-field>
|
|
24
|
-
<EventsTable :paginated-items="eventsPaginatedItems" :on-page-change="(page) =>
|
|
25
|
-
<template
|
|
24
|
+
<EventsTable :paginated-items="eventsPaginatedItems" :on-page-change="(page) => eventsPaginatedItems.currentPage = page">
|
|
25
|
+
<template #actions="slotProps">
|
|
26
26
|
<div class="buttons" style="flex-wrap: nowrap;">
|
|
27
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
|
|
28
|
+
<b-button rounded :type="disableEvent(slotProps.row) ? '' : 'is-success'" :disabled="disableEvent(slotProps.row)" @click="$emit('selected', slotProps.row)">Choose</b-button>
|
|
29
29
|
</div>
|
|
30
30
|
</template>
|
|
31
31
|
</EventsTable>
|
|
@@ -40,11 +40,12 @@ import EventsTable from './EventsTable.vue';
|
|
|
40
40
|
|
|
41
41
|
export default {
|
|
42
42
|
name: "EventsChooser",
|
|
43
|
+
components: { EventsTable },
|
|
43
44
|
props: {
|
|
44
45
|
active: Boolean,
|
|
45
46
|
disableEvent: {
|
|
46
47
|
type: Function,
|
|
47
|
-
default: (
|
|
48
|
+
default: () => { return false; }
|
|
48
49
|
}
|
|
49
50
|
},
|
|
50
51
|
data() {
|
|
@@ -62,7 +63,6 @@ export default {
|
|
|
62
63
|
async created() {
|
|
63
64
|
await this.fetchData()
|
|
64
65
|
},
|
|
65
|
-
components: { EventsTable },
|
|
66
66
|
methods: {
|
|
67
67
|
async fetchData() {
|
|
68
68
|
this.eventsPaginatedItems.loading = true;
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
<b-table
|
|
4
4
|
:data="paginatedItems === null || paginatedItems.items === null ? [] : paginatedItems.items"
|
|
5
5
|
:checked-rows="checkedRows"
|
|
6
|
-
v-on:update:checkedRows="$emit('update:checkedRows', $event)"
|
|
7
6
|
:loading="paginatedItems.items === null || paginatedItems.loading"
|
|
8
7
|
:checkable="checkable"
|
|
9
8
|
custom-row-key="id"
|
|
@@ -12,23 +11,24 @@
|
|
|
12
11
|
:total="paginatedItems.totalItems"
|
|
13
12
|
:per-page="paginatedItems.itemsPerPage"
|
|
14
13
|
:current-page="paginatedItems.currentPage"
|
|
15
|
-
@page-change="onPageChange"
|
|
16
14
|
aria-next-label="Next page"
|
|
17
15
|
aria-previous-label="Previous page"
|
|
18
16
|
aria-page-label="Page"
|
|
19
17
|
aria-current-label="Current page"
|
|
20
|
-
class="full-height-flex full-height-container"
|
|
21
|
-
|
|
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
22
|
{{ props.row.title }}
|
|
23
23
|
</b-table-column>
|
|
24
24
|
|
|
25
|
-
<b-table-column label="Display on website"
|
|
25
|
+
<b-table-column v-slot="props" label="Display on website">
|
|
26
26
|
<em v-if="!props.row.active">No</em>
|
|
27
27
|
<span v-else>{{ props.row.startDate ? new Date(props.row.startDate).toDateString() : '?'}} - {{ props.row.endDate ? new Date(props.row.endDate).toDateString() : '?'}}</span>
|
|
28
28
|
</b-table-column>
|
|
29
29
|
|
|
30
30
|
<b-table-column v-slot="props">
|
|
31
|
-
<slot name="actions"
|
|
31
|
+
<slot name="actions" :row="props.row"></slot>
|
|
32
32
|
</b-table-column>
|
|
33
33
|
|
|
34
34
|
<template slot="empty">
|
|
@@ -50,8 +50,14 @@
|
|
|
50
50
|
export default {
|
|
51
51
|
name: "EventsTable",
|
|
52
52
|
props: {
|
|
53
|
-
paginatedItems:
|
|
54
|
-
|
|
53
|
+
paginatedItems: {
|
|
54
|
+
type: Object,
|
|
55
|
+
required: true
|
|
56
|
+
},
|
|
57
|
+
onPageChange: {
|
|
58
|
+
type: Function,
|
|
59
|
+
required: true
|
|
60
|
+
},
|
|
55
61
|
checkedRows: {
|
|
56
62
|
type: Array,
|
|
57
63
|
default: () => { return []; }
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<transition name="fade" mode="out-in">
|
|
3
|
+
<div v-if="updatedValue === null">
|
|
4
|
+
<slot name="display" :edit="edit" :value="data[fieldName]">
|
|
5
|
+
<p>
|
|
6
|
+
{{ data[fieldName] }}
|
|
7
|
+
<EditButtonOnRowHover :edit="edit" />
|
|
8
|
+
</p>
|
|
9
|
+
</slot>
|
|
10
|
+
</div>
|
|
11
|
+
<slot v-else name="edit" :submit="submitValue" :initial-value="data[fieldName]" :updating="updating">
|
|
12
|
+
<b-field :label="label" :label-position="label ? 'inside' : null">
|
|
13
|
+
<b-input v-model="updatedValue" :type="type" :disabled="updating" class="not-full-width" @keyup.enter.native="submit"></b-input>
|
|
14
|
+
<p class="control">
|
|
15
|
+
<b-button type="is-primary" :loading="updating" @click="submit">Change</b-button>
|
|
16
|
+
</p>
|
|
17
|
+
</b-field>
|
|
18
|
+
</slot>
|
|
19
|
+
</transition>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import {morphToNotification} from "../api";
|
|
24
|
+
import EditButtonOnRowHover from "./EditButtonOnRowHover.vue";
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
name: "GenericEditableField",
|
|
28
|
+
components: {EditButtonOnRowHover},
|
|
29
|
+
props: {
|
|
30
|
+
api: { type: Object, required: true},
|
|
31
|
+
data: { type: Object, required: true },
|
|
32
|
+
fieldName: { type: String, required: true },
|
|
33
|
+
label: { type: String, default: null },
|
|
34
|
+
type: { type: String, default: 'text' }
|
|
35
|
+
},
|
|
36
|
+
data() {
|
|
37
|
+
return {
|
|
38
|
+
updatedValue: null,
|
|
39
|
+
updating: false,
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
methods: {
|
|
43
|
+
async submitValue(value) {
|
|
44
|
+
this.updatedValue = value;
|
|
45
|
+
this.submit();
|
|
46
|
+
},
|
|
47
|
+
async submit() {
|
|
48
|
+
this.updating = true;
|
|
49
|
+
let data = {
|
|
50
|
+
id: this.data.id
|
|
51
|
+
};
|
|
52
|
+
data[this.fieldName] = this.updatedValue;
|
|
53
|
+
try {
|
|
54
|
+
let response = this.fieldName === 'fullName' ? await this.api.updateFullName(this.data.id, this.updatedValue) : await this.api.update(data);
|
|
55
|
+
this.updating = false;
|
|
56
|
+
this.updatedValue = null;
|
|
57
|
+
this.$buefy.toast.open(morphToNotification(response));
|
|
58
|
+
this.$emit('update:data', response.item);
|
|
59
|
+
} catch(e) {
|
|
60
|
+
this.updating = false;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
edit() {
|
|
64
|
+
this.updatedValue = this.data[this.fieldName];
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.not-full-width {
|
|
72
|
+
width: 15rem;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-field>
|
|
3
|
+
<b-autocomplete
|
|
4
|
+
:value="computedValue"
|
|
5
|
+
:loading="loading || updating"
|
|
6
|
+
open-on-focus
|
|
7
|
+
field="name"
|
|
8
|
+
:data="users"
|
|
9
|
+
placeholder="Select a group..."
|
|
10
|
+
clearable
|
|
11
|
+
@typing="fetchData"
|
|
12
|
+
@select="o => $emit('select', o)">
|
|
13
|
+
<template #empty>No results found</template>
|
|
14
|
+
<template slot-scope="props">
|
|
15
|
+
<p><b-icon :icon="props.option.icon"></b-icon><strong>{{ props.option.name }}</strong></p>
|
|
16
|
+
<p class="is-size-7" style="white-space: break-spaces;">{{ props.option.description }}</p>
|
|
17
|
+
</template>
|
|
18
|
+
</b-autocomplete>
|
|
19
|
+
</b-field>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script>
|
|
23
|
+
import GroupsApi from "../GroupsApi";
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
name: "GroupsChooser",
|
|
27
|
+
props: {
|
|
28
|
+
value: { required: false, type: Object, default: null },
|
|
29
|
+
updating: { type: Boolean, default: false }
|
|
30
|
+
},
|
|
31
|
+
data() {
|
|
32
|
+
return {
|
|
33
|
+
groupsApi: new GroupsApi(this.$buefy),
|
|
34
|
+
loading: true,
|
|
35
|
+
users: []
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
computed: {
|
|
39
|
+
computedValue() {
|
|
40
|
+
return this.value ? this.value.name : '';
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async mounted() {
|
|
44
|
+
await this.fetchData('');
|
|
45
|
+
},
|
|
46
|
+
methods: {
|
|
47
|
+
async fetchData(name) {
|
|
48
|
+
this.loading = true;
|
|
49
|
+
this.users = (await this.groupsApi.list(false, 1, name)).items;
|
|
50
|
+
this.loading = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
|
|
58
|
+
</style>
|