@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.
Files changed (69) hide show
  1. package/.eslintrc.js +23 -0
  2. package/.github/workflows/node.js.yml +4 -4
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/ui.iml +10 -0
  5. package/package.json +13 -5
  6. package/src/AuthApi.js +77 -42
  7. package/src/CrudApi.js +3 -3
  8. package/src/GroupsApi.js +9 -0
  9. package/src/MediaDirectoryApi.js +1 -1
  10. package/src/PreferencesApi.js +2 -0
  11. package/src/UserPermissions.js +2 -9
  12. package/src/UserPreferences.js +0 -4
  13. package/src/UserPreferences.test.js +0 -2
  14. package/src/UsersApi.js +41 -0
  15. package/src/api.js +96 -38
  16. package/src/components/App.vue +19 -240
  17. package/src/components/AuthenticatedLayout.vue +254 -0
  18. package/src/components/AuthenticationLog.vue +86 -30
  19. package/src/components/CodeEditor.vue +16 -32
  20. package/src/components/EditButtonOnRowHover.vue +21 -0
  21. package/src/components/Error404.vue +15 -5
  22. package/src/components/EventsChooser.vue +11 -11
  23. package/src/components/EventsTable.vue +14 -8
  24. package/src/components/GenericEditableField.vue +74 -0
  25. package/src/components/GroupsChooser.vue +58 -0
  26. package/src/components/GroupsList.vue +129 -0
  27. package/src/components/ImportExport.vue +32 -1
  28. package/src/components/LegacyPage.vue +22 -23
  29. package/src/components/UserJoined.vue +35 -0
  30. package/src/components/UserManagement.vue +168 -0
  31. package/src/components/UserProfileForm.vue +214 -0
  32. package/src/components/ViewProfile.vue +7 -219
  33. package/src/components/auth/Auth404.vue +16 -0
  34. package/src/components/auth/Login.vue +135 -0
  35. package/src/components/auth/LoginLogo.vue +30 -0
  36. package/src/components/auth/Logout.vue +26 -0
  37. package/src/components/auth/PasswordRemind.vue +71 -0
  38. package/src/components/auth/PasswordReset.vue +97 -0
  39. package/src/components/auth/TwoFactorSetup.vue +115 -0
  40. package/src/components/auth/VerifyEmail.vue +71 -0
  41. package/src/components/auth/WelcomeFloat.vue +87 -0
  42. package/src/components/auth/login.scss +17 -0
  43. package/src/components/{MediaChooseDirectory.vue → media/MediaChooseDirectory.vue} +12 -12
  44. package/src/components/{MediaDirectory.vue → media/MediaDirectory.vue} +8 -8
  45. package/src/components/{MediaInsertModal.vue → media/MediaInsertModal.vue} +2 -2
  46. package/src/components/{MediaItem.vue → media/MediaItem.vue} +24 -23
  47. package/src/components/{MediaItemPreview.vue → media/MediaItemPreview.vue} +5 -5
  48. package/src/components/{MediaList.vue → media/MediaList.vue} +42 -38
  49. package/src/components/{MediaPage.vue → media/MediaPage.vue} +1 -1
  50. package/src/components/{MediaResponsiveImages.vue → media/MediaResponsiveImages.vue} +5 -5
  51. package/src/components/{MediaUpload.vue → media/MediaUpload.vue} +10 -10
  52. package/src/components/{media.scss → media/media.scss} +1 -1
  53. package/src/components/preferences/PreferencesField.vue +10 -10
  54. package/src/components/preferences/PreferencesList.vue +13 -20
  55. package/src/components/preferences/PreferencesThemeChooser.vue +9 -9
  56. package/src/components/preferences/ShowIfPermitted.vue +9 -14
  57. package/src/components/users/CreateUserModal.vue +73 -0
  58. package/src/icons.js +90 -0
  59. package/src/main.js +111 -0
  60. package/src/modules/LegacyPages.js +18 -0
  61. package/src/modules/Media.js +45 -0
  62. package/src/modules/UserManagement.js +24 -0
  63. package/src/routes/index.js +92 -0
  64. package/src/store/index.js +70 -0
  65. package/src/styles/_variables.scss +1 -0
  66. package/src/styles/app.scss +15 -2
  67. package/src/login.js +0 -17
  68. package/src/routes.js +0 -61
  69. 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
- <h1 class="title">Logins, Logouts &amp; Login Attempts</h1>
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>
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" v-slot="props">
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 label="Location" v-slot="props">
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" v-slot="props">
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 label="Time" v-slot="props">
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 label="Type" v-slot="props">
36
- {{ getInfo(props.row) }}
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.paginatedItems.loading = true;
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
- getInfo(data) {
120
- if(data.type === 0) {
121
- return 'Login';
122
- } else if(data.type === 1) {
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('brace/mode/' + this.lang);
82
- require('brace/theme/' + this.theme);
83
- require('brace/snippets/' + this.lang);
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
- <p>
3
- Error 404!
4
- </p>
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" @update:active="arg => $emit('update:active', arg)"
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 placeholder="Search..."
20
- type="search"
21
- icon="search" rounded v-model="searchQuery">
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) => this.eventsPaginatedItems.currentPage = page">
25
- <template v-slot:actions="slotProps">
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 @click="$emit('selected', slotProps.row)" :type="disableEvent(slotProps.row) ? '' : 'is-success'" :disabled="disableEvent(slotProps.row)">Choose</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
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: (event) => { return false; }
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
- <b-table-column label="Title" v-slot="props">
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" v-slot="props">
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" v-bind:row="props.row"></slot>
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: Object,
54
- onPageChange: Function,
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>