@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
@@ -0,0 +1,129 @@
1
+ <template>
2
+ <div class="box">
3
+ <div class="level">
4
+ <div class="level-left">
5
+ <h1 class="title">Groups</h1>
6
+ </div>
7
+ <div class="level-right">
8
+ </div>
9
+ </div>
10
+
11
+ <b-table :data="paginatedItems.items === null ? [] : paginatedItems.items" :loading="paginatedItems.loading">
12
+ <b-table-column v-slot="props" label="">
13
+ <GenericEditableField :api="groupsApi" :data="props.row" field-name="icon" @update:data="updateGroup">
14
+ <template #display="{ value, edit }">
15
+ <b-icon :icon="value" size="is-small"></b-icon>
16
+ <EditButtonOnRowHover :edit="edit" />
17
+ </template>
18
+ </GenericEditableField>
19
+ </b-table-column>
20
+ <b-table-column v-slot="props" label="Name">
21
+ <GenericEditableField :api="groupsApi" :data="props.row" field-name="name" @update:data="updateGroup" />
22
+ </b-table-column>
23
+ <b-table-column v-slot="props" label="Description">
24
+ <GenericEditableField :api="groupsApi" :data="props.row" field-name="description" @update:data="updateGroup" />
25
+ </b-table-column>
26
+ <b-table-column v-slot="props" label="Nickname">
27
+ <GenericEditableField :api="groupsApi" :data="props.row" field-name="nickname" @update:data="updateGroup">
28
+ <template #display="{ value, edit }">
29
+ <code>{{ value }}</code>
30
+ <EditButtonOnRowHover :edit="edit" />
31
+ </template>
32
+ </GenericEditableField>
33
+ </b-table-column>
34
+ <b-table-column v-slot="props" label="Created">
35
+ <UserJoined :user="props.row"></UserJoined>
36
+ </b-table-column>
37
+ <b-table-column v-slot="props" label="" width="25em">
38
+ <div class="buttons">
39
+ <b-button v-if="props.row.deletedAt" rounded outlined icon-left="recycle" size="is-small" @click="restoreItem(props.row.id)">Restore</b-button>
40
+ <b-button v-if="props.row.deletedAt" rounded type="is-danger" outlined icon-left="trash" size="is-small" @click="forceDeleteItem(props.row.id)">Delete Forever</b-button>
41
+ <b-button v-if="!props.row.deletedAt" rounded icon-left="trash" size="is-small" @click="deleteItem(props.row.id)">Delete</b-button>
42
+ </div>
43
+ </b-table-column>
44
+ </b-table>
45
+
46
+ <hr />
47
+
48
+ <div class="content">
49
+ <h3>Permissions</h3>
50
+ <p>To edit group permissions, you will need access to the <code>artisan</code> console command which comes installed with this application.</p>
51
+ <p>Here are some getting-started tips:</p>
52
+ <pre><code>
53
+ # list all permissions recognised the application
54
+ artisan permissions
55
+
56
+ # list permissions explictly set for the "admin" group
57
+ artisan permissions admin
58
+
59
+ # list all permissions for the "admin" group
60
+ artisan permissions admin -a
61
+
62
+ # grant a permission to group "admin"
63
+ artisan permissions admin --grant pages.postCreate
64
+
65
+ # unset a permission for group "admin"
66
+ artisan permissions admin --unset pages.postCreate
67
+
68
+ # explicitly deny a permission for group "admin"
69
+ artisan permissions admin --deny pages.postCreate
70
+
71
+ # sets that "pages" should inherit their permissions from "_content"
72
+ artisan permissions admin --inherit pages:_content</code></pre>
73
+ </div>
74
+
75
+ </div>
76
+ </template>
77
+
78
+ <script>
79
+ import GroupsApi from "../GroupsApi";
80
+ import UserJoined from "./UserJoined.vue";
81
+ import GenericEditableField from "./GenericEditableField.vue";
82
+ import EditButtonOnRowHover from "./EditButtonOnRowHover.vue";
83
+
84
+ export default {
85
+ name: "GroupsList",
86
+ components: { UserJoined, GenericEditableField, EditButtonOnRowHover },
87
+ data() {
88
+ return {
89
+ groupsApi: new GroupsApi(this.$buefy),
90
+ paginatedItems: {items: null, totalItems: null, itemsPerPage: null, loading: false, currentPage: 1},
91
+ }
92
+ },
93
+ async mounted() {
94
+ this.fetchData()
95
+ },
96
+ methods: {
97
+ async fetchData() {
98
+ this.paginatedItems.loading = true;
99
+ let data = await this.groupsApi.list(false, this.paginatedItems.currentPage, null);
100
+ this.paginatedItems.items = data.items;
101
+ this.paginatedItems.totalItems = data.totalItems;
102
+ this.paginatedItems.itemsPerPage = data.itemsPerPage;
103
+ this.paginatedItems.loading = false;
104
+ },
105
+ updateGroup(group) {
106
+ this.paginatedItems.items = this.paginatedItems.items.map(g => {
107
+ return g.id === group.id ? group : g;
108
+ });
109
+ this.$emit('updated');
110
+ },
111
+ async deleteItem(id) {
112
+ await this.groupsApi.deleteAndNotify(id);
113
+ await this.fetchData();
114
+ },
115
+ async forceDeleteItem(id) {
116
+ await this.groupsApi.confirmForceDelete(id);
117
+ await this.fetchData();
118
+ },
119
+ async restoreItem(id) {
120
+ await this.groupsApi.restoreAndNotify(id);
121
+ await this.fetchData();
122
+ }
123
+ }
124
+ }
125
+ </script>
126
+
127
+ <style scoped>
128
+
129
+ </style>
@@ -1,10 +1,41 @@
1
1
  <template>
2
+ <div class="content">
3
+ <h3>Export Data</h3>
4
+ <p>
5
+ Create a backup of all the content and settings of the website.<br>
6
+ It is recommended to make regular backups to ensure the safety of your content.
7
+ </p>
8
+ <b-button type="is-primary" size="is-medium" :loading="exporting" @click="processDownload">Download data as `.zip`</b-button>
2
9
 
10
+ <h3>Import Data</h3>
11
+ <p>To restore the contents and settings of this site from a backup, use the Artisan Console.</p>
12
+ </div>
3
13
  </template>
4
14
 
5
15
  <script>
16
+ import {FetchBuilder} from "../api";
17
+ import {API_ROOT} from "../CrudApi";
18
+ import download from "downloadjs";
19
+
6
20
  export default {
7
- name: "ImportExport"
21
+ name: "ImportExport",
22
+ data() {
23
+ return {
24
+ exporting: false
25
+ }
26
+ },
27
+ methods: {
28
+ async processDownload() {
29
+ this.exporting = true;
30
+ let response = await (new FetchBuilder(this.$buefy, 'post'))
31
+ .cookies()
32
+ .fetchRaw(API_ROOT + "import-export/export");
33
+ this.$buefy.notification.open({ message: 'Export successful', type: 'is-success', queue: false });
34
+ let blob = await response.blob();
35
+ download(blob, 'database ' + (new Date()).toLocaleString() + '.zip');
36
+ this.exporting = false;
37
+ }
38
+ }
8
39
  }
9
40
  </script>
10
41
 
@@ -1,20 +1,18 @@
1
1
  <template>
2
2
  <div class="full-height full-height-container legacy-container">
3
3
  <transition name="fade">
4
- <iframe ref="iframe" class="iframe" v-show="!loading" />
4
+ <iframe v-show="!loading" ref="iframe" class="iframe" />
5
5
  </transition>
6
- <!-- <b-loading :is-full-page="false" v-model="loading" :can-cancel="false"></b-loading>-->
7
6
 
8
7
  <MediaInsertModal :active.sync="isInsertMediaItemModalActive" @close="closeInsertMediaItemModal" @select="onFilesSelected" />
9
8
  </div>
10
9
  </template>
11
10
 
12
11
  <script>
13
- import MediaInsertModal from "./MediaInsertModal.vue";
12
+ import MediaInsertModal from "./media/MediaInsertModal.vue";
14
13
 
15
14
  import { morphToNotification } from "../api";
16
15
  import MediaApi from "../MediaApi";
17
- import UserPreferences from "../UserPreferences";
18
16
 
19
17
  // from https://gist.github.com/hdodov/a87c097216718655ead6cf2969b0dcfa
20
18
 
@@ -50,9 +48,17 @@ const iframeURLChange = (iframe, callback, legacyPage) => {
50
48
 
51
49
  export default {
52
50
  name: "LegacyPage",
51
+ components: { MediaInsertModal },
52
+ beforeRouteLeave(to, from, next) {
53
+ window.document.body.style.overflowY = 'auto';
54
+ window.document.documentElement.style.overflowY = 'auto';
55
+ this.$parent.$data.requestedCollapsed = false;
56
+ next();
57
+ },
53
58
  props: {
54
- legacyPrefix: String,
55
- adminPrefix: String
59
+ fullPath: { type: String, required: true },
60
+ legacyPrefix: { type: String, required: true },
61
+ adminPrefix: { type: String, required: true }
56
62
  },
57
63
  data() {
58
64
  return {
@@ -60,17 +66,18 @@ export default {
60
66
  currentPath: null,
61
67
  isInsertMediaItemModalActive: false,
62
68
  resolveInsertMediaItems: null,
63
- rejectInsertMediaItems: null,
64
- userPreferences: null
69
+ rejectInsertMediaItems: null
65
70
  }
66
71
  },
67
- components: { MediaInsertModal },
68
72
  computed: {
69
- loading() { return this.loadingPath !== null; }
73
+ loading() { return this.loadingPath !== null; },
74
+ userPreferences() { return this.$store.getters.userPreferences; }
75
+ },
76
+ 'watch': {
77
+ 'fullPath': 'onFullPathChanged'
70
78
  },
71
79
  async mounted() {
72
80
  this.loadingPath = 'prefs';
73
- this.userPreferences = await UserPreferences.load();
74
81
 
75
82
  window.document.body.style.overflowY = 'hidden';
76
83
  window.document.documentElement.style.overflowY = 'hidden';
@@ -89,18 +96,11 @@ export default {
89
96
  unmounted() {
90
97
  this.$parent.$data.requestedCollapsed = false;
91
98
  },
92
- beforeRouteUpdate(to, from, next) {
93
- // when the Vue route changes, load this path inside the iframe
94
- this.loadPath(to.fullPath);
95
- next();
96
- },
97
- beforeRouteLeave(to, from, next) {
98
- window.document.body.style.overflowY = 'auto';
99
- window.document.documentElement.style.overflowY = 'auto';
100
- this.$parent.$data.requestedCollapsed = false;
101
- next();
102
- },
103
99
  methods: {
100
+ onFullPathChanged(newFullPath) {
101
+ console.log('Route changed', );
102
+ this.loadPath(newFullPath);
103
+ },
104
104
  setupIframeIntegrations() {
105
105
  console.log('[LegacyPage] Setting up iframe integrations for', this.$refs.iframe.contentWindow.location.href);
106
106
  let elem = this.$refs.iframe;
@@ -139,7 +139,6 @@ export default {
139
139
  // We detect when the iframe url changes, and update our window accordingly...
140
140
  onNavigated(newURL) {
141
141
  console.log('[LegacyPage] Navigated to ' + newURL);
142
-
143
142
  },
144
143
  showInnerNotification(data) {
145
144
  this.$buefy.notification.open(morphToNotification(data));
@@ -0,0 +1,35 @@
1
+ <template>
2
+ <span>{{ joined }}, on {{ joinedAbs }}</span>
3
+ </template>
4
+
5
+ <script>
6
+ import Internationalize from "../Internationalize";
7
+
8
+ export default {
9
+ name: "UserJoined",
10
+ props: {
11
+ user: {
12
+ type: Object,
13
+ required: true
14
+ }
15
+ },
16
+ computed: {
17
+ joined() {
18
+ const rtf1 = new Intl.RelativeTimeFormat('en', { style: 'narrow' });
19
+ let daysSinceJoined = (new Date(this.user.createdAt) - new Date()) / (1000 * 24 * 60 * 60);
20
+ let shouldUseYears = daysSinceJoined < -400;
21
+ return rtf1.format(
22
+ Math.round(shouldUseYears ? daysSinceJoined / 365 : daysSinceJoined),
23
+ shouldUseYears ? 'year' : 'day'
24
+ );
25
+ },
26
+ joinedAbs() {
27
+ return Internationalize.formatDate(this.user.createdAt);
28
+ },
29
+ }
30
+ }
31
+ </script>
32
+
33
+ <style scoped>
34
+
35
+ </style>
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <div class="full-height scroll-container pad">
3
+ <div class="box">
4
+ <div class="level">
5
+ <div class="level-left">
6
+ <h1 class="title">Users</h1>
7
+ </div>
8
+ <div class="level-right">
9
+ <b-input v-model="searchQuery" icon-left="search" rounded placeholder="Search for users..." class="mr-4"></b-input>
10
+ <b-button type="is-success" icon-left="plus" @click="isCreateUserModalActive = true">Create Account</b-button>
11
+ </div>
12
+ </div>
13
+
14
+ <b-table :data="paginatedItems.items === null ? [] : paginatedItems.items" :loading="paginatedItems.loading">
15
+ <b-table-column v-slot="props" label="Full Name">
16
+ <GenericEditableField :api="usersApi" :data="props.row" field-name="fullName" @update:data="updateUser">
17
+ <template #display="{ value, edit }">
18
+ <p>
19
+ {{ value }}
20
+ <EditButtonOnRowHover :edit="edit" />
21
+ </p>
22
+ </template>
23
+ </GenericEditableField>
24
+ </b-table-column>
25
+ <b-table-column v-slot="props" label="Email">
26
+ <GenericEditableField :api="usersApi" :data="props.row" field-name="email" type="email" @update:data="updateUser">
27
+ <template #display="{ value, edit }">
28
+ <p>
29
+ <a :href="'mailto:' + value" target="_blank" class="is-size-7">{{ value }}</a>
30
+ <EditButtonOnRowHover :edit="edit" />
31
+ </p>
32
+ </template>
33
+ </GenericEditableField>
34
+ </b-table-column>
35
+ <b-table-column v-slot="props" label="Group">
36
+ <GenericEditableField :api="usersApi" :data="props.row" field-name="group" @update:data="updateUser">
37
+ <template #display="{ value, edit }">
38
+ <p>
39
+ {{ value.name }}
40
+ <EditButtonOnRowHover :edit="edit" />
41
+ </p>
42
+ </template>
43
+ <template #edit="{ initialValue, submit, updating }">
44
+ <GroupsChooser :value="initialValue" :updating="updating" @select="submit" />
45
+ </template>
46
+ </GenericEditableField>
47
+ </b-table-column>
48
+ <b-table-column v-slot="props" label="Username">
49
+ <GenericEditableField :api="usersApi" :data="props.row" field-name="username" @update:data="updateUser">
50
+ <template #display="{ value, edit }">
51
+ <p>
52
+ {{ value }}
53
+ <EditButtonOnRowHover :edit="edit" />
54
+ </p>
55
+ </template>
56
+ </GenericEditableField>
57
+ </b-table-column>
58
+ <b-table-column v-slot="props" label="Email Verified">
59
+ {{ props.row.emailVerified ? 'Yes' : 'No' }}
60
+ </b-table-column>
61
+ <b-table-column v-slot="props" label="Two-Factor Auth">
62
+ {{ props.row.twoFactorAuthEnabled ? 'Yes' : 'No' }}
63
+ </b-table-column>
64
+ <b-table-column v-slot="props" label="Joined">
65
+ <UserJoined :user="props.row"></UserJoined>
66
+ </b-table-column>
67
+ <b-table-column v-slot="props" label="" width="25em">
68
+ <div class="buttons">
69
+ <b-button rounded icon-left="sign-in-alt" size="is-small" type="is-info" @click="impersonate(props.row.id)">Login as this user</b-button>
70
+ <b-button v-if="props.row.deletedAt" rounded icon-left="trash" size="is-small" type="is-danger" @click="forceDelete(props.row.id)">Delete Forever</b-button>
71
+ <b-button v-if="!props.row.deletedAt" rounded icon-left="minus-circle" size="is-small" @click="deactivate(props.row.id)">Deactivate</b-button>
72
+ <b-button v-else rounded icon-left="plus" size="is-small" type="is-success" @click="activate(props.row.id)">Activate</b-button>
73
+ </div>
74
+ </b-table-column>
75
+ </b-table>
76
+
77
+ <CreateUserModal :active.sync="isCreateUserModalActive" @update:users="fetchData" />
78
+ </div>
79
+
80
+ <GroupsList @updated="fetchData" />
81
+ </div>
82
+ </template>
83
+
84
+ <script>
85
+ import UsersApi from "../UsersApi";
86
+ import UserJoined from "./UserJoined.vue";
87
+ import {morphToNotification} from "../api";
88
+ import {isNavigationFailure, NavigationFailureType} from "vue-router/src/util/errors";
89
+ import GenericEditableField from "./GenericEditableField.vue";
90
+ import GroupsChooser from "./GroupsChooser.vue";
91
+ import GroupsList from "./GroupsList.vue";
92
+ import EditButtonOnRowHover from "./EditButtonOnRowHover.vue";
93
+ import CreateUserModal from "./users/CreateUserModal.vue";
94
+
95
+ export default {
96
+ name: "UserManagement",
97
+ components: {
98
+ CreateUserModal,
99
+ EditButtonOnRowHover, GroupsChooser, GenericEditableField, UserJoined, GroupsList},
100
+ data() {
101
+ return {
102
+ usersApi: new UsersApi(this.$buefy),
103
+ selectedUser: null,
104
+ searchQuery: null,
105
+ isCreateUserModalActive: false,
106
+ paginatedItems: {items: null, totalItems: null, itemsPerPage: null, loading: false, currentPage: 1},
107
+ }
108
+ },
109
+ watch: {
110
+ 'searchQuery': 'fetchData'
111
+ },
112
+ created() {
113
+ this.fetchData()
114
+ },
115
+ methods: {
116
+ async fetchData() {
117
+ this.paginatedItems.loading = true;
118
+ let data = await this.usersApi.list(false, this.paginatedItems.currentPage, this.searchQuery);
119
+
120
+ this.paginatedItems.items = data.items;
121
+ this.paginatedItems.totalItems = data.totalItems;
122
+ this.paginatedItems.itemsPerPage = data.itemsPerPage;
123
+ this.paginatedItems.loading = false;
124
+ },
125
+ async impersonate(id) {
126
+ const promise = new Promise((resolve) => {
127
+ this.$buefy.dialog.confirm({
128
+ message: 'This super-admin functionality allows you to impersonate another user. Are you sure you want to continue?',
129
+ onConfirm: resolve
130
+ });
131
+ });
132
+
133
+ await promise;
134
+ let response = await this.usersApi.impersonate(id);
135
+ console.log(response);
136
+ this.$buefy.notification.open(morphToNotification(response));
137
+ this.$store.commit('setImpersonating', response.user);
138
+ // ignore duplicated navigation failure
139
+ await this.$router.push({ path: '/' }).catch(failure => {
140
+ if(!isNavigationFailure(failure, NavigationFailureType.duplicated)) {
141
+ throw failure;
142
+ }
143
+ });
144
+ },
145
+ async deactivate(id) {
146
+ await this.usersApi.deleteAndNotify(id);
147
+ await this.fetchData();
148
+ },
149
+ async activate(id) {
150
+ await this.usersApi.restoreAndNotify(id);
151
+ await this.fetchData();
152
+ },
153
+ async forceDelete(id) {
154
+ await this.usersApi.confirmForceDelete(id);
155
+ await this.fetchData();
156
+ },
157
+ updateUser(user) {
158
+ this.paginatedItems.items = this.paginatedItems.items.map(u => {
159
+ return u.id === user.id ? user : u;
160
+ })
161
+ }
162
+ }
163
+ }
164
+ </script>
165
+
166
+ <style scoped>
167
+ @import './util.css';
168
+ </style>