@oxygen-cms/ui 1.6.4 → 1.7.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.
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ChangeListManager">
4
+ <list default="true" id="4ad0dcde-54f6-459b-8ef0-38a17e946358" name="Changes" comment="" />
5
+ <option name="SHOW_DIALOG" value="false" />
6
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
7
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
8
+ <option name="LAST_RESOLUTION" value="IGNORE" />
9
+ </component>
10
+ <component name="ComposerSettings">
11
+ <execution />
12
+ </component>
13
+ <component name="ProjectId" id="27rS1jBVazc5EdthHlwaXfJm1hC" />
14
+ <component name="ProjectViewState">
15
+ <option name="hideEmptyMiddlePackages" value="true" />
16
+ <option name="showLibraryContents" value="true" />
17
+ </component>
18
+ <component name="PropertiesComponent"><![CDATA[{
19
+ "keyToString": {
20
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
21
+ "RunOnceActivity.ShowReadmeOnStart": "true",
22
+ "WebServerToolWindowFactoryState": "false",
23
+ "last_opened_file_path": "/home/chris/code/oxygen/Components/ui"
24
+ }
25
+ }]]></component>
26
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
27
+ <component name="TaskManager">
28
+ <task active="true" id="Default" summary="Default task">
29
+ <changelist id="4ad0dcde-54f6-459b-8ef0-38a17e946358" name="Changes" comment="" />
30
+ <created>1650076498224</created>
31
+ <option name="number" value="Default" />
32
+ <option name="presentableId" value="Default" />
33
+ <updated>1650076498224</updated>
34
+ </task>
35
+ <servers />
36
+ </component>
37
+ </project>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-cms/ui",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "Various utilities for UI-building in Vue.js",
5
5
  "main": "none",
6
6
  "repository": {
package/src/AuthApi.js CHANGED
@@ -2,6 +2,13 @@ import {getApiRoot} from "./CrudApi";
2
2
  import {FetchBuilder, initCsrfCookie} from "./api";
3
3
  import UserPermissions from "./UserPermissions";
4
4
 
5
+ export const LOGIN_AGAIN_NOTIFICATION = {
6
+ message: 'You need to login again.',
7
+ type: 'is-warning',
8
+ duration: 5000,
9
+ queue: false
10
+ };
11
+
5
12
  export default class AuthApi {
6
13
 
7
14
  constructor($buefy) {
@@ -12,6 +12,10 @@ export default class Internationalize {
12
12
  return format.format(date);
13
13
  }
14
14
 
15
+ static formatDateExtended(date) {
16
+ return new Date(date).toDateString();
17
+ }
18
+
15
19
  static formatLastUpdated(updatedAt) {
16
20
  let d = new Date(updatedAt);
17
21
  return d.toDateString() + ' ' + d.toLocaleTimeString();
package/src/UsersApi.js CHANGED
@@ -23,6 +23,11 @@ export default class UsersApi extends CrudApi {
23
23
  .fetch(getApiRoot() + 'users/' + id + '/fullName');
24
24
  }
25
25
 
26
+ async getBasic(id) {
27
+ return this.request('get')
28
+ .fetch(this.constructor.getResourceRoot() + '/' + id + '/basic');
29
+ }
30
+
26
31
  async impersonate(id) {
27
32
  return await this.request('post')
28
33
  .fetch(getApiRoot() + 'users/' + id + '/impersonate');
package/src/api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {getApiRoot} from "./CrudApi";
2
+ import {LOGIN_AGAIN_NOTIFICATION} from "./AuthApi";
2
3
 
3
4
  export const getApiHost = () => {
4
5
  if (parseInt(window.location.port) >= 3000) {
@@ -28,6 +29,7 @@ export const initCsrfCookie = async () => {
28
29
  }
29
30
 
30
31
  export class FetchBuilder {
32
+
31
33
  constructor($buefy, method) {
32
34
  this.$buefy = $buefy;
33
35
  this.method = method;
@@ -120,7 +122,7 @@ export class FetchBuilder {
120
122
  return data;
121
123
  }
122
124
 
123
- handleAPIError(data, this.$buefy, FetchBuilder.router, response);
125
+ await handleAPIError(data, this.$buefy, FetchBuilder.router, FetchBuilder.store, response);
124
126
  let e = new Error('Received an error response from API call');
125
127
  e.response = data;
126
128
  throw e;
@@ -135,6 +137,10 @@ export class FetchBuilder {
135
137
  static setRouter(router) {
136
138
  FetchBuilder.router = router;
137
139
  }
140
+
141
+ static setStore(store) {
142
+ FetchBuilder.store = store;
143
+ }
138
144
  }
139
145
 
140
146
  function statusToBueify(status) {
@@ -155,35 +161,29 @@ export function morphToNotification(data) {
155
161
  };
156
162
  }
157
163
 
158
- const handleAPIError = function(content, $buefy, $router, response) {
159
- console.error('API error: ', content);
164
+ const handleAPIError = async (content, $buefy, $router, $store, response) => {
165
+ console.error('API error: ', content, $router);
160
166
  if(response.status === 401 && content.code === 'unauthenticated') {
161
167
  // server is telling us to login again
162
- initCsrfCookie()
163
- .then(() => {
164
- $router.push({path: '/auth/login', query: {redirect: $router.currentRoute.fullPath}});
165
- });
166
- return;
168
+ await $store.commit('setUser', null);
169
+ await initCsrfCookie();
170
+ await $buefy.notification.open(LOGIN_AGAIN_NOTIFICATION);
171
+ await $router.push({path: '/auth/login', query: {redirect: $router.currentRoute.fullPath}});
167
172
  } else if(response.status === 403 && content.code === 'two_factor_setup_required') {
168
- $router.push({ path: '/auth/2fa-setup' });
169
- return;
173
+ await $router.push({ path: '/auth/2fa-setup' });
170
174
  } else if(response.status === 403 && content.code === 'email_unverified') {
171
- $router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
172
- return;
175
+ await $router.push({ path: '/auth/needs-verified-email', query: {redirect: $router.currentRoute.fullPath } });
173
176
  } else if(response.status === 404) {
174
- $router.push({ name: 'error404' });
177
+ await $router.push({ name: 'error404' });
175
178
  } else if(response.status === 429) {
176
- $buefy.notification.open({
179
+ await $buefy.notification.open({
177
180
  message: 'Too many requests within a short timeframe. Please wait.',
178
181
  type: 'is-warning',
179
182
  duration: 10000,
180
183
  queue: false
181
184
  });
182
- return;
183
- }
184
-
185
- // handle generic validation errors
186
- if(typeof content.errors === 'object') {
185
+ } else if(typeof content.errors === 'object') {
186
+ // handle generic validation errors
187
187
  for(const [, errors ] of Object.entries(content.errors)) {
188
188
  for(let error of errors) {
189
189
  $buefy.notification.open({
@@ -194,13 +194,10 @@ const handleAPIError = function(content, $buefy, $router, response) {
194
194
  });
195
195
  }
196
196
  }
197
- return;
198
- }
199
-
200
- if(content.content && content.status) {
201
- $buefy.notification.open(morphToNotification(content));
197
+ } else if(content.content && content.status) {
198
+ await $buefy.notification.open(morphToNotification(content));
202
199
  } else if(content.exception) {
203
- $buefy.notification.open({
200
+ await $buefy.notification.open({
204
201
  message:
205
202
  'PHP Exception of type <pre class="no-pre">' + content.exception +
206
203
  '</pre> with message <pre class="no-pre">' + content.message +
@@ -211,7 +208,7 @@ const handleAPIError = function(content, $buefy, $router, response) {
211
208
  type: 'is-danger'
212
209
  });
213
210
  } else if(response.status === 500) {
214
- $buefy.notification.open({
211
+ await $buefy.notification.open({
215
212
  message:'Whoops, looks like something went wrong.',
216
213
  type: 'is-danger',
217
214
  animation: 'fade',
@@ -225,3 +222,4 @@ export function getXsrfToken() {
225
222
  }
226
223
 
227
224
  FetchBuilder.router = null;
225
+ FetchBuilder.store = null;
@@ -35,8 +35,10 @@ import AceEditor from 'vue2-ace-editor';
35
35
  // language extension pre-requisite...
36
36
  import 'brace/ext/language_tools';
37
37
  import 'brace/mode/html';
38
+ import 'brace/mode/json';
38
39
  import 'brace/mode/twig';
39
40
  import 'brace/snippets/html';
41
+ import 'brace/snippets/json';
40
42
  import 'brace/snippets/twig';
41
43
  import 'brace/theme/tomorrow_night_eighties';
42
44
  import 'brace/theme/tomorrow_night';
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <span>
3
+ {{ Internationalize.formatLastUpdated(model.createdAt) }}<span v-if="model.createdBy">, by <router-link :to="'/users/' + model.createdBy.id">{{ model.createdBy.fullName }}</router-link></span>
4
+ </span>
5
+ </template>
6
+
7
+ <script>
8
+ import Internationalize from "../Internationalize";
9
+
10
+ export default {
11
+ name: "Created",
12
+ props: {
13
+ model: { type: Object, required: true }
14
+ },
15
+ data() {
16
+ return {
17
+ Internationalize
18
+ }
19
+ },
20
+ }
21
+ </script>
22
+
23
+ <style scoped>
24
+
25
+ </style>
@@ -11,8 +11,9 @@
11
11
  <script>
12
12
  import MediaInsertModal from "./media/MediaInsertModal.vue";
13
13
 
14
- import {getApiHost, morphToNotification} from "../api";
14
+ import {getApiHost, initCsrfCookie, morphToNotification} from "../api";
15
15
  import MediaApi from "../MediaApi";
16
+ import {LOGIN_AGAIN_NOTIFICATION} from "../AuthApi";
16
17
 
17
18
  // from https://gist.github.com/hdodov/a87c097216718655ead6cf2969b0dcfa
18
19
 
@@ -20,15 +21,6 @@ const iframeURLChange = (iframe, callback, legacyPage) => {
20
21
  var unloadHandler = function() {
21
22
  console.log('[LegacyPage] Starting load');
22
23
  legacyPage.loadingPath = 'unknown';
23
- // Timeout needed because the URL changes immediately after
24
- // the `unload` event is dispatched.
25
- // TODO: this is rather brittle because I believe it relies upon timing
26
- // setTimeout(function() {
27
- // console.log(iframe.contentWindow);
28
- // if(iframe.contentWindow) {
29
- // callback(iframe.contentWindow.location.href);
30
- // }
31
- // }, 0);
32
24
  };
33
25
 
34
26
  function attachUnload() {
@@ -90,7 +82,7 @@ export default {
90
82
  iframe.addEventListener('load', this.onLoaded.bind(this));
91
83
  if(iframe.contentDocument.readyState === "complete") {
92
84
  console.warn('[LegacyPage] mounted: page was already loaded - perhaps this page was cached?');
93
- this.onLoaded();
85
+ await this.onLoaded();
94
86
  }
95
87
  },
96
88
  unmounted() {
@@ -126,7 +118,8 @@ export default {
126
118
  let urlObj = new URL(url);
127
119
  let urlString = urlObj.toString();
128
120
  if(urlObj.pathname.startsWith(this.legacyPrefix)) {
129
- return { loadInside: 'iframe', location: this.adminPrefix + urlString.split(this.legacyPrefix)[1] };
121
+ let loc = urlString.split(this.legacyPrefix)[1];
122
+ return { loadInside: 'iframe', location: this.adminPrefix + loc, locationWithoutPrefix: loc };
130
123
  } else if(urlObj.pathname.startsWith(this.adminPrefix)) {
131
124
  return { loadInside: 'vue', location: urlString.split(this.adminPrefix)[1] };
132
125
  } else {
@@ -155,7 +148,7 @@ export default {
155
148
  popState() {
156
149
  this.$router.back();
157
150
  },
158
- onLoaded() {
151
+ async onLoaded() {
159
152
  let path = this.$refs.iframe.contentWindow.location.href;
160
153
  if(path === 'about:blank') { return; }
161
154
  console.log('[LegacyPage] Loaded', path);
@@ -166,12 +159,26 @@ export default {
166
159
  if(loadInside === 'iframe') {
167
160
  window.history.pushState({}, "", location);
168
161
  } else if(loadInside === 'vue') {
169
- this.$router.push(location);
162
+ if(location.startsWith('/auth/login?location=')) {
163
+ // If the legacy page is redirecting us to login,
164
+ // then that must be because our auth expired/failed.
165
+ // So we explicitly log ourselves out, and redirect to the login page.
166
+ let redirectTo = this.fullURLToVuePath(this.currentPath);
167
+ if(redirectTo.loadInside !== 'iframe') { throw new Error("this.currentPath was not inside iframe"); }
168
+ console.log("Requested redirect to /auth/login . Clearing user state first", redirectTo);
169
+ this.$store.commit('setUser', null);
170
+ this.$buefy.notification.open(LOGIN_AGAIN_NOTIFICATION);
171
+ await initCsrfCookie();
172
+ location = { name: 'login', query: { redirect: redirectTo.locationWithoutPrefix } };
173
+ }
174
+
175
+ await this.$router.push(location);
176
+ return;
170
177
  } else {
171
178
  // load outside of iframe
172
179
  window.location = location;
180
+ return;
173
181
  }
174
-
175
182
  this.currentPath = path;
176
183
  }
177
184
 
@@ -0,0 +1,25 @@
1
+ <template>
2
+ <span>
3
+ {{ Internationalize.formatLastUpdated(model.updatedAt) }}<span v-if="model.updatedBy">, by <router-link :to="'/users/' + model.updatedBy.id">{{ model.updatedBy.fullName }}</router-link></span>
4
+ </span>
5
+ </template>
6
+
7
+ <script>
8
+ import Internationalize from "../Internationalize";
9
+
10
+ export default {
11
+ name: "Created",
12
+ props: {
13
+ model: { type: Object, required: true }
14
+ },
15
+ data() {
16
+ return {
17
+ Internationalize
18
+ }
19
+ },
20
+ }
21
+ </script>
22
+
23
+ <style scoped>
24
+
25
+ </style>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div class="content">
3
+ <div class="level level-left">
4
+ <strong class="mr-2">Username:</strong>
5
+ <span v-if="user">{{ user.username }}
6
+ <b-tooltip v-if="editable" label="To change username, please contact your administrator." position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
7
+ </span>
8
+ </div>
9
+ <div class="level level-left"><strong class="mr-2">Group: </strong>
10
+ <transition name="fade" mode="out-in">
11
+ <span v-if="user">{{ user.group.name }}
12
+ <b-tooltip :label="user.group.description" position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
13
+ </span>
14
+ <b-skeleton v-else width="20%" :animated="true" />
15
+ </transition>
16
+ </div>
17
+ <div class="level level-left"><strong class="mr-2">Joined: </strong>
18
+ <transition name="fade" mode="out-in">
19
+ <UserJoined v-if="user" :user="user" />
20
+ <b-skeleton v-else width="20%" :animated="true" />
21
+ </transition>
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script>
27
+ import UserJoined from "./UserJoined.vue";
28
+
29
+ export default {
30
+ name: "UserProfileDescription",
31
+ components: { UserJoined },
32
+ props: {
33
+ user: { type: Object, default: null },
34
+ editable: { type: Boolean, required: true }
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <style scoped>
40
+
41
+ </style>
@@ -19,28 +19,7 @@
19
19
  </div>
20
20
  </div>
21
21
 
22
- <div class="content">
23
- <div class="level level-left">
24
- <strong class="mr-2">Username:</strong>
25
- <span>{{ user.username }}
26
- <b-tooltip label="To change username, please contact your administrator." position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
27
- </span>
28
- </div>
29
- <div class="level level-left"><strong class="mr-2">Group: </strong>
30
- <transition name="fade" mode="out-in">
31
- <span v-if="user">{{ user.group.name }}
32
- <b-tooltip :label="user.group.description" position="is-right" multilined><b-icon icon="info-circle"></b-icon></b-tooltip>
33
- </span>
34
- <b-skeleton v-else width="20%" :animated="true" />
35
- </transition>
36
- </div>
37
- <div class="level level-left"><strong class="mr-2">Joined: </strong>
38
- <transition name="fade" mode="out-in">
39
- <UserJoined v-if="user" :user="user" />
40
- <b-skeleton v-else width="20%" :animated="true" />
41
- </transition>
42
- </div>
43
- </div>
22
+ <UserProfileDescription :user="user" :editable="true"></UserProfileDescription>
44
23
 
45
24
  <b-modal :closable="false" :active.sync="isUserPreferencesModalActive" has-modal-card trap-focus aria-role="dialog" aria-modal>
46
25
  <div class="modal-card">
@@ -136,16 +115,16 @@
136
115
 
137
116
  <script>
138
117
  import {morphToNotification} from "../api";
139
- import UserJoined from "./UserJoined.vue";
140
118
  import ShowIfPermitted from "./preferences/ShowIfPermitted.vue";
141
119
  import UserPreferences from "./preferences/UserPreferences.vue";
142
120
  import UsersApi from "../UsersApi";
143
121
  import AuthApi from "../AuthApi";
144
122
  import GenericEditableField from "./GenericEditableField.vue";
123
+ import UserProfileDescription from "./UserProfileDescription.vue";
145
124
 
146
125
  export default {
147
126
  name: "UserProfileForm",
148
- components: {GenericEditableField, ShowIfPermitted, UserJoined, UserPreferences },
127
+ components: {UserProfileDescription, GenericEditableField, ShowIfPermitted, UserPreferences },
149
128
  props: {
150
129
  user: {
151
130
  type: Object,
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div class="full-height-container pad">
3
+
4
+ <div class="box middle-of-page">
5
+
6
+ <div>
7
+ <div class="has-background-grey-light huge-icon-container" style="display: inline-block;">
8
+ <b-icon icon="user" size="is-large" class="has-text-grey-lighter huge-icon"></b-icon>
9
+ </div>
10
+ </div>
11
+
12
+ <h2 class="title">
13
+ <transition name="fade" mode="out-in">
14
+ <span v-if="model">{{model.fullName}}</span>
15
+ <b-skeleton :active="!model" size="is-medium"></b-skeleton>
16
+ </transition>
17
+ </h2>
18
+ <h3 class="subtitle">
19
+ <transition name="fade" mode="out-in">
20
+ <a v-if="model" :href="'mailto:' + model.email">{{ model.email }}</a>
21
+ <b-skeleton :active="!model"></b-skeleton>
22
+ </transition>
23
+ </h3>
24
+
25
+ <div v-if="model && model.id == currentUserId">
26
+ <b-button type="is-primary" tag="router-link" to="/user/profile">Edit Your Profile</b-button>
27
+ </div>
28
+
29
+ <hr>
30
+
31
+ <UserProfileDescription :user="model" :editable="false"></UserProfileDescription>
32
+
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script>
38
+ import UsersApi from "../UsersApi";
39
+ import UserProfileDescription from "./UserProfileDescription.vue";
40
+
41
+ export default {
42
+ name: "UserProfilePage",
43
+ components: {UserProfileDescription},
44
+ data() {
45
+ return {
46
+ model: null,
47
+ loading: true,
48
+ usersApi: new UsersApi(this.$buefy)
49
+ }
50
+ },
51
+ computed: {
52
+ currentUserId() {
53
+ return this.$store.state.user.id;
54
+ }
55
+ },
56
+ created() {
57
+ this.fetchData();
58
+ },
59
+ methods: {
60
+ async fetchData() {
61
+ this.model = (await this.usersApi.getBasic(this.$route.params.id)).item;
62
+ this.loading = false;
63
+ }
64
+ }
65
+ }
66
+ </script>
67
+
68
+ <style scoped>
69
+ @import '~@oxygen-cms/ui/src/components/util.css';
70
+
71
+ .middle-of-page {
72
+ width: 40rem;
73
+ max-width: 100%;
74
+ margin-left: auto;
75
+ margin-right: auto;
76
+ text-align: center;
77
+ }
78
+
79
+ .huge-icon {
80
+ width: 10rem;
81
+ height: 10rem;
82
+ font-size: 3rem;
83
+ }
84
+
85
+ .huge-icon-container {
86
+ border-radius: 50%;
87
+ }
88
+ </style>
@@ -89,9 +89,6 @@ export default {
89
89
  this.submitting = false;
90
90
  this.hasFailedLogin = false;
91
91
  this.$store.commit('setUser', response.user);
92
- if(this.$route.query.location) {
93
- window.location = this.$route.query.location;
94
- }
95
92
  this.$buefy.notification.open({
96
93
  message: "You're now logged in.",
97
94
  type: 'is-info',
@@ -22,5 +22,5 @@ export default {
22
22
  </script>
23
23
 
24
24
  <style scoped>
25
-
25
+ @import './login.scss';
26
26
  </style>
package/src/icons.js CHANGED
@@ -74,7 +74,7 @@ import {
74
74
  faFolderOpen,
75
75
  faImages,
76
76
  faMinusCircle,
77
- faCalendarPlus, faPaperPlane, faHandshakeSlash, faHandshake
77
+ faCalendarPlus, faPaperPlane, faHandshakeSlash, faHandshake, faExclamation, faMousePointer, faUnlink, faAngry
78
78
  } from "@fortawesome/free-solid-svg-icons";
79
79
 
80
80
  export const addIconsToLibrary = () => {
@@ -87,5 +87,5 @@ export const addIconsToLibrary = () => {
87
87
  faFileExcel, faFileCsv, faChevronCircleDown, faChevronCircleUp, faTrash,
88
88
  faEye, faEyeSlash, faCaretDown, faCaretUp, faUpload, faUser, faFolder, faHome, faFilePdf, faSignOutAlt, faTag,
89
89
  faFolderPlus, faTimes, faQuestionCircle, faFileUpload, faLandmark,
90
- faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane, faHandshake, faHandshakeSlash);
90
+ faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane, faHandshake, faHandshakeSlash, faExclamation, faMousePointer, faUnlink, faAngry);
91
91
  };
package/src/main.js CHANGED
@@ -133,6 +133,7 @@ export default class OxygenUI {
133
133
  });
134
134
 
135
135
  FetchBuilder.setRouter(router);
136
+ FetchBuilder.setStore(store);
136
137
  UserPermissions.setBuefy(this.app.$buefy);
137
138
  UserPreferences.setBuefy(this.app.$buefy)
138
139
  return this;
@@ -1,9 +1,16 @@
1
1
  import ViewProfile from "../components/ViewProfile.vue";
2
2
  import AuthenticationLog from "../components/AuthenticationLog.vue";
3
3
  import UserManagement from "../components/UserManagement.vue";
4
+ import UserProfilePage from "../components/UserProfilePage.vue";
4
5
 
5
6
  export default function(ui) {
6
7
  ui.addAuthenticatedRoutes([
8
+ {
9
+ path: 'users/:id',
10
+ name: 'users.viewProfile',
11
+ component: UserProfilePage,
12
+ meta: { title: 'User Profile' }
13
+ },
7
14
  {
8
15
  path: 'user/profile',
9
16
  name: 'auth.viewProfile',
package/src/EventsApi.js DELETED
@@ -1,21 +0,0 @@
1
- import { CrudApi } from './CrudApi';
2
-
3
- export default class EventsApi extends CrudApi {
4
-
5
- static prepareModelForAPI(data) {
6
- let m = { ...data };
7
- delete m.id;
8
- delete m.bookings;
9
- return m;
10
- }
11
-
12
- static getResourceName() {
13
- return 'upcoming-events';
14
- }
15
-
16
- async listTrybookingSessions() {
17
- return this.request('get')
18
- .fetch(this.constructor.getResourceRoot() + '/trybooking-sessions');
19
- }
20
-
21
- }
@@ -1,88 +0,0 @@
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>
@@ -1,82 +0,0 @@
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>
@@ -1,20 +0,0 @@
1
- <template>
2
- <article>
3
- <p class="title">Events<b-icon icon="calendar-alt" size="is-medium"></b-icon></p>
4
- <p class="subtitle">Promotion for upcoming concerts and events</p>
5
- <div class="buttons">
6
- <b-button tag="router-link" to="/upcoming-events" icon-left="list">Manage Events</b-button>
7
- <b-button tag="router-link" to="/upcoming-events/create" icon-left="plus" type="is-success">Create New Event</b-button>
8
- </div>
9
- </article>
10
- </template>
11
-
12
- <script>
13
- export default {
14
- name: "EventsPanel"
15
- }
16
- </script>
17
-
18
- <style scoped lang="scss">
19
- @import './panel.scss';
20
- </style>
@@ -1,35 +0,0 @@
1
- import LegacyPage from "../components/LegacyPage.vue";
2
- import { WEB_CONTENT } from "../main.js";
3
- import PreferencesEventTemplates from "../components/preferences/PreferencesEventTemplates.vue";
4
-
5
- export default function(ui) {
6
- ui.addMainMenuGroup(WEB_CONTENT, {
7
- name: 'Events',
8
- icon: 'calendar-alt',
9
- listAction: '/upcoming-events',
10
- listPermission: 'upcomingEvents.getList',
11
- addIcon: 'calendar-plus',
12
- addPermission: 'upcomingEvents.postCreate',
13
- addAction: '/upcoming-events/create',
14
- items: {
15
- }
16
- });
17
- ui.extraPrefs['appearance'].push({
18
- key: 'appearance.events',
19
- component: PreferencesEventTemplates
20
- });
21
- ui.addAuthenticatedRoutes([
22
- {
23
- // will match everything, try to render a legacy Oxygen page...
24
- path: 'upcoming-events/:subpath*',
25
- component: LegacyPage,
26
- props: (route) => {
27
- return {
28
- fullPath: route.fullPath,
29
- legacyPrefix: '/oxygen/view',
30
- adminPrefix: '/oxygen'
31
- }
32
- }
33
- }
34
- ]);
35
- }