@oxygen-cms/ui 1.6.5 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-cms/ui",
3
- "version": "1.6.5",
3
+ "version": "1.7.0",
4
4
  "description": "Various utilities for UI-building in Vue.js",
5
5
  "main": "none",
6
6
  "repository": {
@@ -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');
@@ -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>
@@ -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>
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
  };
@@ -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
- }