@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.
Files changed (84) hide show
  1. package/.babelrc +1 -0
  2. package/.eslintrc.js +22 -0
  3. package/.github/workflows/node.js.yml +29 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/ui.iml +10 -0
  6. package/.jshintrc +3 -0
  7. package/README.md +7 -0
  8. package/assets/oxygen-icon.png +0 -0
  9. package/jest.init.js +1 -0
  10. package/package.json +72 -0
  11. package/src/AuthApi.js +116 -0
  12. package/src/CrudApi.js +112 -0
  13. package/src/EventsApi.js +16 -0
  14. package/src/GroupsApi.js +9 -0
  15. package/src/Internationalize.js +31 -0
  16. package/src/MediaApi.js +52 -0
  17. package/src/MediaDirectoryApi.js +62 -0
  18. package/src/PreferencesApi.js +47 -0
  19. package/src/UserPermissions.js +66 -0
  20. package/src/UserPreferences.js +69 -0
  21. package/src/UserPreferences.test.js +23 -0
  22. package/src/UsersApi.js +41 -0
  23. package/src/api.js +209 -0
  24. package/src/components/App.vue +61 -0
  25. package/src/components/AuthenticatedLayout.vue +254 -0
  26. package/src/components/AuthenticationLog.vue +196 -0
  27. package/src/components/CodeEditor.vue +90 -0
  28. package/src/components/EditButtonOnRowHover.vue +21 -0
  29. package/src/components/Error404.vue +25 -0
  30. package/src/components/EventsChooser.vue +88 -0
  31. package/src/components/EventsTable.vue +82 -0
  32. package/src/components/GenericEditableField.vue +74 -0
  33. package/src/components/GroupsChooser.vue +58 -0
  34. package/src/components/GroupsList.vue +129 -0
  35. package/src/components/ImportExport.vue +45 -0
  36. package/src/components/LegacyPage.vue +256 -0
  37. package/src/components/UserJoined.vue +35 -0
  38. package/src/components/UserManagement.vue +168 -0
  39. package/src/components/UserProfileForm.vue +214 -0
  40. package/src/components/ViewProfile.vue +32 -0
  41. package/src/components/auth/Auth404.vue +16 -0
  42. package/src/components/auth/Login.vue +135 -0
  43. package/src/components/auth/LoginLogo.vue +30 -0
  44. package/src/components/auth/Logout.vue +26 -0
  45. package/src/components/auth/PasswordRemind.vue +71 -0
  46. package/src/components/auth/PasswordReset.vue +97 -0
  47. package/src/components/auth/TwoFactorSetup.vue +115 -0
  48. package/src/components/auth/VerifyEmail.vue +71 -0
  49. package/src/components/auth/WelcomeFloat.vue +87 -0
  50. package/src/components/auth/login.scss +17 -0
  51. package/src/components/media/MediaChooseDirectory.vue +129 -0
  52. package/src/components/media/MediaDirectory.vue +109 -0
  53. package/src/components/media/MediaInsertModal.vue +88 -0
  54. package/src/components/media/MediaItem.vue +282 -0
  55. package/src/components/media/MediaItemPreview.vue +45 -0
  56. package/src/components/media/MediaList.vue +305 -0
  57. package/src/components/media/MediaPage.vue +44 -0
  58. package/src/components/media/MediaResponsiveImages.vue +51 -0
  59. package/src/components/media/MediaUpload.vue +133 -0
  60. package/src/components/media/media.scss +51 -0
  61. package/src/components/preferences/PreferencesAdminAppearance.vue +22 -0
  62. package/src/components/preferences/PreferencesAuthentication.vue +27 -0
  63. package/src/components/preferences/PreferencesEventTemplates.vue +22 -0
  64. package/src/components/preferences/PreferencesField.vue +215 -0
  65. package/src/components/preferences/PreferencesList.vue +50 -0
  66. package/src/components/preferences/PreferencesPageTemplates.vue +23 -0
  67. package/src/components/preferences/PreferencesSiteAppearance.vue +22 -0
  68. package/src/components/preferences/PreferencesThemeChooser.vue +73 -0
  69. package/src/components/preferences/ShowIfPermitted.vue +37 -0
  70. package/src/components/preferences/UserPreferences.vue +30 -0
  71. package/src/components/users/CreateUserModal.vue +73 -0
  72. package/src/components/util.css +47 -0
  73. package/src/icons.js +90 -0
  74. package/src/main.js +112 -0
  75. package/src/modules/LegacyPages.js +18 -0
  76. package/src/modules/Media.js +45 -0
  77. package/src/modules/UserManagement.js +24 -0
  78. package/src/routes/index.js +92 -0
  79. package/src/store/index.js +70 -0
  80. package/src/styles/_variables.scss +23 -0
  81. package/src/styles/app.scss +76 -0
  82. package/src/unsavedChanges.js +16 -0
  83. package/src/util.js +65 -0
  84. package/src/util.test.js +39 -0
@@ -0,0 +1,196 @@
1
+ <template>
2
+ <div class="full-height scroll-container pad">
3
+ <div class="box">
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 &amp; Login Attempts <b-button icon-left="redo-alt" rounded @click="paginatedItems.currentPage = 1; fetchLogins"></b-button></h1>
38
+
39
+ <b-table :data="paginatedItems.items === null ? [] : paginatedItems.items"
40
+ :loading="paginatedItems.loading"
41
+ paginated
42
+ backend-pagination
43
+ :per-page="paginatedItems.itemsPerPage"
44
+ :total="paginatedItems.totalItems"
45
+ :row-class="(row) => row.type === 1 ? 'is-danger' : ''"
46
+ @page-change="paginatedItems.currentPage = $event">
47
+ <b-table-column v-slot="props" label="IP Address">
48
+ {{ props.row.ipAddress }}
49
+ </b-table-column>
50
+
51
+ <b-table-column v-slot="props" label="Location">
52
+ <div v-if="props.row.geolocationInfo !== null">
53
+ {{ props.row.geolocationInfo }}
54
+ </div>
55
+ <b-progress v-else></b-progress>
56
+
57
+ </b-table-column>
58
+
59
+ <b-table-column v-slot="props" label="Browser / Device">
60
+ <b-tooltip :label="props.row.userAgent" animated>{{ parseUserAgent(props.row.userAgent) }}</b-tooltip>
61
+
62
+ </b-table-column>
63
+
64
+ <b-table-column v-slot="props" label="Time">
65
+ {{ Internationalize.formatDateTime(props.row.timestamp) }}
66
+ </b-table-column>
67
+
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>
78
+ </b-table-column>
79
+ </b-table>
80
+ </div>
81
+ </div>
82
+ </template>
83
+
84
+ <script>
85
+ import {FetchBuilder} from "../api";
86
+ import Internationalize from "../Internationalize";
87
+ import UAParser from 'ua-parser-js';
88
+ import AuthApi from "../AuthApi";
89
+
90
+ const IP_INFO_LOADING = 'loading';
91
+
92
+ export default {
93
+ name: "AuthenticationLog",
94
+ data() {
95
+ return {
96
+ authApi: new AuthApi(this.$buefy),
97
+ paginatedItems: { items: null, currentPage: 1, loading: true, totalItems: null, itemsPerPage: null },
98
+ sessions: { items: null, loading: true },
99
+ ipInfo: new Map(),
100
+ Internationalize
101
+ }
102
+ },
103
+ watch: {
104
+ 'paginatedItems.currentPage': 'fetchData'
105
+ },
106
+ created() {
107
+ this.fetchData()
108
+ },
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
+ },
134
+ async fetchData() {
135
+ this.fetchSessions();
136
+ this.fetchLogins();
137
+ },
138
+ geoIP(ip) {
139
+ if(this.ipInfo.has(ip)) {
140
+ if(this.ipInfo.get(ip) !== IP_INFO_LOADING) {
141
+ this.updateInfoForIp(ip);
142
+ }
143
+ return;
144
+ }
145
+ this.ipInfo.set(ip, IP_INFO_LOADING);
146
+ FetchBuilder.default(this.$buefy, 'post')
147
+ .fetch('/oxygen/api/auth/ip-location/' + ip, (data) => data)
148
+ .then((data) => {
149
+ this.ipInfo.set(ip, data);
150
+ this.updateInfoForIp(ip);
151
+ }).catch(() => {
152
+ this.updateInfoForIp(ip);
153
+ })
154
+ },
155
+ updateInfoForIp(ip) {
156
+ let geolocationInfo = this.ipInfo.has(ip) ? this.getGeolocationInfo(this.ipInfo.get(ip)) : '';
157
+ for(let item of this.paginatedItems.items) {
158
+ if(item.ipAddress === ip) {
159
+ item.geolocationInfo = geolocationInfo;
160
+ }
161
+ }
162
+ for(let item of this.sessions.items) {
163
+ if(item.ipAddress === ip) {
164
+ item.geolocationInfo = geolocationInfo;
165
+ }
166
+ }
167
+ },
168
+ parseUserAgent(userAgent) {
169
+ let ua = new UAParser(userAgent);
170
+ let browser = ua.getBrowser();
171
+ let device = ua.getDevice();
172
+ // console.log(device);
173
+ return browser.name + ' ' + browser.version + ' on ' + ua.getOS().name + ', ' + (device.vendor ? device.vendor : '(unknown device)');
174
+ },
175
+ getGeolocationInfo(data) {
176
+ if(!data.city || !data.country_name) {
177
+ return 'unknown';
178
+ }
179
+ return data.city + ', ' + data.country_name;
180
+ },
181
+ async deleteSession(id) {
182
+ await this.authApi.deleteUserSession(id);
183
+ this.$buefy.notification.open({ message: 'User session removed', type: 'is-warning' });
184
+ await this.fetchData();
185
+ }
186
+ }
187
+ }
188
+ </script>
189
+
190
+ <style scoped>
191
+ @import './util.css';
192
+
193
+ tr.is-danger {
194
+ color: #f00;
195
+ }
196
+ </style>
@@ -0,0 +1,90 @@
1
+ <template>
2
+ <div class="editor-container has-background-grey-darker" :style="'height: ' + height + ';'">
3
+ <transition name="fade">
4
+ <AceEditor
5
+ key="editor"
6
+ ref="ace"
7
+ :value="value"
8
+ :lang="lang"
9
+ :theme="theme"
10
+ width="100%"
11
+ height="100%"
12
+ :options="{
13
+ enableBasicAutocompletion: true,
14
+ enableLiveAutocompletion: true,
15
+ fontSize: fontSize,
16
+ highlightActiveLine: highlightActiveLine,
17
+ enableSnippets: true,
18
+ showLineNumbers: true,
19
+ wrap: wrapMode,
20
+ tabSize: 4,
21
+ showPrintMargin: showPrintMargin,
22
+ showInvisibles: showInvisibles,
23
+ showGutter: true,
24
+ }"
25
+ @input="$emit('input', $event)"
26
+ @init="editorInit"
27
+ />
28
+ </transition>
29
+ </div>
30
+ </template>
31
+
32
+ <script>
33
+
34
+ export default {
35
+ name: "CodeEditor",
36
+ components: { AceEditor: require('vue2-ace-editor') },
37
+ props: {
38
+ value: { type: String, default: null },
39
+ height: { type: String, required: true },
40
+ lang: { type: String, required: true }
41
+ },
42
+ data() {
43
+ return {
44
+ }
45
+ },
46
+ computed: {
47
+ wrapMode() {
48
+ return this.$store.getters.userPreferences.get('editor.ace.wordWrap');
49
+ },
50
+ highlightActiveLine() {
51
+ return this.$store.getters.userPreferences.get('editor.ace.highlightActiveLine');
52
+ },
53
+ showPrintMargin() {
54
+ return this.$store.getters.userPreferences.get('editor.ace.showPrintMargin');
55
+ },
56
+ showInvisibles() {
57
+ return this.$store.getters.userPreferences.get('editor.ace.showInvisibles');
58
+ },
59
+ theme() {
60
+ return this.$store.getters.userPreferences.get('editor.ace.theme').replace('ace/theme/', '');
61
+ },
62
+ fontSize() {
63
+ return this.$store.getters.userPreferences.get('editor.ace.fontSize');
64
+ }
65
+ },
66
+ methods: {
67
+ editorInit() {
68
+ require('brace/ext/language_tools') //language extension prerequsite...
69
+ require(`brace/mode/${this.lang}`);
70
+ require(`brace/theme/${this.theme}`);
71
+ require(`brace/snippets/${this.lang}`);
72
+
73
+ // ignore first missing DOCTYPE warning
74
+ let session = this.$refs.ace.editor.getSession();
75
+ session.on("changeAnnotation", () => {
76
+ const a = session.getAnnotations();
77
+ const b = a.slice(0).filter( (item) => item.text.indexOf('DOC') === -1 );
78
+ if(a.length > b.length) session.setAnnotations(b);
79
+ });
80
+ }
81
+ }
82
+ }
83
+ </script>
84
+
85
+ <style scoped>
86
+ .editor-container {
87
+ width: 100%;
88
+ position: relative;
89
+ }
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>
@@ -0,0 +1,25 @@
1
+ <template>
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>
14
+ </template>
15
+
16
+ <script>
17
+ export default {
18
+ name: "Error404",
19
+ components: {}
20
+ }
21
+ </script>
22
+
23
+ <style scoped>
24
+
25
+ </style>
@@ -0,0 +1,88 @@
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>
@@ -0,0 +1,82 @@
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>
@@ -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>