@oxygen-cms/ui 1.5.2 → 1.6.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.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Various utilities for UI-building in Vue.js",
5
5
  "main": "none",
6
6
  "repository": {
@@ -48,7 +48,7 @@
48
48
  "eslint-plugin-jest": "^24.4.0",
49
49
  "eslint-plugin-vue": "^7.17.0",
50
50
  "jest": "^26.6.3",
51
- "node-sass": "^4.13.1",
51
+ "sass-embedded": "^1.49.9",
52
52
  "postcss-loader": "^3.0.0",
53
53
  "regenerator-runtime": "^0.13.7",
54
54
  "sass-loader": "^8.0.2",
@@ -23,10 +23,11 @@ export default class UserPermissions {
23
23
  let keyParts = key.split('.');
24
24
 
25
25
  if(keyParts.length !== 2) {
26
- throw new Error('TreePermissionsSystem Requires a Dot-Seperated Permissions Key');
26
+ throw new Error('TreePermissionsSystem Requires a Dot-Seperated Permissions Key: ' + key);
27
27
  }
28
28
 
29
29
  // check for the specific key
30
+ // console.log(key + " = " + result);
30
31
  return this.hasKey(keyParts[0], keyParts[1]);
31
32
  }
32
33
 
@@ -4,17 +4,11 @@ jest.mock('./AuthApi');
4
4
 
5
5
  test('gets and sets preferences', async () => {
6
6
  UserPreferences.setBuefy({});
7
- UserPreferences.authApi.userDetails.mockResolvedValue({
8
- user: {
9
- preferences: {
10
- foo: 'bar',
11
- baz: { qux: 'fub '}
12
- }
13
- }
7
+ let prefs = new UserPreferences({
8
+ foo: 'bar',
9
+ baz: { qux: 'fub '}
14
10
  });
15
11
 
16
- let prefs = await UserPreferences.load();
17
-
18
12
  expect(prefs.get('foo')).toBe('bar');
19
13
  expect(prefs.has('fob')).toBe(false);
20
14
  expect(prefs.has('baz.qux')).toBe(true);
@@ -0,0 +1,99 @@
1
+ <template>
2
+ <div v-if="userPermissions">
3
+ <b-menu-list>
4
+ <b-menu-item icon="home" tag="router-link" to="/dashboard" label="Dashboard"></b-menu-item>
5
+ </b-menu-list>
6
+
7
+ <b-menu-list v-for="(category, label) in categoriesWithPermission(items)"
8
+ :key="label"
9
+ :label="label">
10
+ <b-menu-item v-for="(group, groupLabel) in groupsWithPermission(category)"
11
+ :key="groupLabel"
12
+ :icon="group.icon"
13
+ :tag="userPermissions.has(group.listPermission) ? 'router-link' : null"
14
+ :expanded="group.groupPrefix ? $route.fullPath.startsWith(group.groupPrefix) : null"
15
+ :to="group.listAction">
16
+ <template #label>
17
+ {{ groupLabel }}
18
+ <b-button v-if="userPermissions.has(group.addPermission)"
19
+ tag="router-link"
20
+ type="is-text"
21
+ class="is-pulled-right show-if-active"
22
+ :icon-right="group.addIcon"
23
+ :to="group.addAction"></b-button>
24
+ </template>
25
+ <b-menu-item v-for="(item, itemLabel) in itemsWithPermission(group.items)" :key="itemLabel" :label="itemLabel" tag="router-link" :to="item.to"></b-menu-item>
26
+ </b-menu-item>
27
+ </b-menu-list>
28
+
29
+ <b-menu-list v-if="userPermissions && userPermissions.hasOneOf(['preferences.getValue', 'users.getList', 'groups.getList', 'importExport.getList'])" label="System">
30
+ <b-menu-item v-if="userPermissions && userPermissions.has('preferences.getValue')" icon="cogs" tag="router-link" to="/preferences" label="Preferences"></b-menu-item>
31
+ <b-menu-item v-if="userPermissions && userPermissions.has('users.getList')" icon="users" tag="router-link" to="/users" label="Users and Permissions"></b-menu-item>
32
+ </b-menu-list>
33
+ </div>
34
+
35
+ </template>
36
+
37
+ <script>
38
+ export default {
39
+ name: "MainMenu",
40
+ props: {
41
+ items: {
42
+ type: Object,
43
+ required: true
44
+ }
45
+ },
46
+ computed: {
47
+ userPermissions() { return this.$store.getters.userPermissions; }
48
+ },
49
+ methods: {
50
+ categoriesWithPermission(categories) {
51
+ return Object.fromEntries(Object.entries(categories).filter(([,category]) => this.userPermissions.hasOneOf(Object.values(category).flatMap((group) => this.getPermissionsForGroup(group))) ));
52
+ },
53
+ getPermissionsForGroup(group) {
54
+ let values = Object.values(group.items).map(s => s.permission);
55
+ if(group.addPermission) { values.push(group.addPermission); }
56
+ if(group.listPermission) { values.push(group.listPermission); }
57
+ return values;
58
+ },
59
+ itemsWithPermission(items) {
60
+ return Object.fromEntries(Object.entries(items).filter(([, item]) => this.userPermissions.has(item.permission)));
61
+ },
62
+ groupsWithPermission(groups) {
63
+ return Object.fromEntries(Object.entries(groups).filter(([, group]) => this.userPermissions.hasOneOf(this.getPermissionsForGroup(group))));
64
+ }
65
+ }
66
+ }
67
+ </script>
68
+
69
+ <style scoped lang="scss">
70
+ .show-if-active {
71
+ visibility: hidden;
72
+ opacity: 0;
73
+ padding: 0;
74
+ transition: visibility 0.2s ease, opacity 0.2s ease;
75
+ height: auto;
76
+
77
+ .router-link-active & {
78
+ visibility: visible;
79
+ opacity: 1;
80
+ }
81
+
82
+ .menu-list li:hover & {
83
+ visibility: visible;
84
+ opacity: 1;
85
+ }
86
+ }
87
+
88
+ .left-navigation-container.is-collapsed .show-if-active {
89
+ visibility: hidden !important;
90
+ opacity: 0 !important;
91
+ }
92
+ </style>
93
+
94
+ <style>
95
+ .show-if-active .icon:first-child:last-child {
96
+ margin-left: 0;
97
+ margin-right: 0;
98
+ }
99
+ </style>
@@ -0,0 +1,20 @@
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>
@@ -0,0 +1,88 @@
1
+ <template>
2
+ <div class="full-height scroll-container">
3
+ <div class="has-background-white-ter">
4
+ <div class="container hero is-medium">
5
+ <div class="hero-body">
6
+ <h1 class="title" style="font-size: 3rem;">
7
+ <transition name="fade">
8
+ <span v-if="user">Welcome, {{ user.fullName }}</span>
9
+ </transition>
10
+ </h1>
11
+ <br>
12
+ <h2 class="subtitle">The Oxygen CMS administration panel allows you to edit the site content, manage events and more.</h2>
13
+ </div>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="has-background-white">
18
+ <div class="container hero">
19
+ <div class="hero-body">
20
+ <section class="tile is-ancestor">
21
+ <div class="tile is-parent">
22
+ <div class="tile is-child heading-box">
23
+ <h2 class="title">Manage site</h2>
24
+ </div>
25
+ </div>
26
+ </section>
27
+
28
+ <div class="grid-loading">
29
+ <b-loading :active="!userPermissions" :is-full-page="false"></b-loading>
30
+ <transition name="fade">
31
+ <section v-if="userPermissions" class="tile is-ancestor">
32
+ <div v-for="(group, i) in panels" :key="i" class="tile is-vertical">
33
+ <div v-if="userPermissions && userPermissions.hasOneOf(group.map(panel => panel.permission))" class="tile is-parent is-vertical">
34
+ <div v-for="panel in group" :key="panel.name" class="tile is-child notification">
35
+ <component :is="panel.component"></component>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </section>
40
+ </transition>
41
+ </div>
42
+
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <component :is="item" v-for="(item, i) in extraRows" :key="i"></component>
48
+ </div>
49
+ </template>
50
+
51
+ <script>
52
+ export default {
53
+ name: "MainDashboard",
54
+ props: {
55
+ panels: {
56
+ type: Array,
57
+ required: true
58
+ },
59
+ extraRows: {
60
+ type: Array,
61
+ default: () => { return []; }
62
+ }
63
+ },
64
+ data() {
65
+ return {
66
+ }
67
+ },
68
+ computed: {
69
+ userPermissions() { return this.$store.getters.userPermissions; },
70
+ user() { return this.$store.state.user; }
71
+ },
72
+ }
73
+ </script>
74
+
75
+ <style scoped>
76
+ .title .icon {
77
+ margin-left: 1rem;
78
+ }
79
+
80
+ .grid-loading {
81
+ position: relative;
82
+ min-height: 30rem;
83
+ }
84
+
85
+ .notification {
86
+ text-align: center;
87
+ }
88
+ </style>
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <article>
3
+ <p class="title">Photos &amp; Files <b-icon icon="photo-video" size="is-medium"></b-icon></p>
4
+ <p class="subtitle">Photos, videos, audio, PDFs.</p>
5
+ <div class="buttons">
6
+ <b-button tag="router-link" to="/media/list" icon-left="list">Manage Photos & Files</b-button>
7
+ <b-button tag="router-link" type="is-success" to="/media/list?upload=true" icon-left="upload">Upload</b-button>
8
+ <b-button tag="router-link" to="/media/responsive-images" type="is-light" icon-left="mail-bulk">Generate Responsive Images</b-button>
9
+ </div>
10
+ </article>
11
+ </template>
12
+
13
+ <script>
14
+ export default {
15
+ name: "MediaPanel"
16
+ }
17
+ </script>
18
+
19
+ <style scoped lang="scss">
20
+ @import './panel.scss';
21
+ </style>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <article>
3
+ <p class="title">Pages<b-icon icon="file-alt" size="is-medium"></b-icon></p>
4
+ <p class="subtitle">Manage the content of the website</p>
5
+ <div class="buttons">
6
+ <b-button tag="router-link" to="/pages" icon-left="list">Manage Pages</b-button>
7
+ <b-button tag="router-link" to="/partials" icon-left="puzzle-piece">Manage Partials</b-button>
8
+ </div>
9
+ <b-button tag="a" href="/" icon-left="eye" type="is-primary" rounded>View Live Site</b-button>
10
+ </article>
11
+ </template>
12
+
13
+ <script>
14
+ export default {
15
+ name: "PagesPartialsPanel",
16
+ computed: {
17
+ userPermissions() { return this.$store.getters.userPermissions; },
18
+ }
19
+ }
20
+ </script>
21
+
22
+ <style scoped lang="scss">
23
+ @import './panel.scss';
24
+ </style>
@@ -0,0 +1,8 @@
1
+ .buttons {
2
+ justify-content: center;
3
+ }
4
+
5
+ .title .icon {
6
+ display: inline-block;
7
+ margin-left: 1rem;
8
+ }
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <PreferencesList>
3
+ <template #default="slotProps">
4
+ <b-tab-item v-if="slotProps.canAccessPrefs(['appearance.themes', 'appearance.pages', 'appearance.events'].concat(getExtraPrefsPermissions('appearance')))" label="Website Theme">
5
+ <PreferencesThemeChooser @theme-changed="onThemeChanged" />
6
+ <PreferencesPageTemplates :current-theme="currentTheme" />
7
+ <PreferencesEventTemplates :current-theme="currentTheme" />
8
+ <PreferencesSiteAppearance :current-theme="currentTheme" />
9
+ <component :is="pref.component" v-for="pref in getExtraPrefs('appearance')" :key="pref.key" :current-theme="currentTheme" />
10
+ </b-tab-item>
11
+ <b-tab-item v-if="slotProps.canAccessPrefs(getExtraPrefsPermissions('external'))" label="External Integrations">
12
+ <component :is="pref.component" v-for="pref in getExtraPrefs('external')" :key="pref.key" :current-theme="currentTheme" />
13
+ </b-tab-item>
14
+ <b-tab-item v-if="slotProps.canAccessPrefs(['modules.auth'])" label="Authentication & Security">
15
+ <PreferencesAuthentication :current-theme="currentTheme" />
16
+ </b-tab-item>
17
+ <b-tab-item v-if="slotProps.canAccessPrefs(['appearance.auth'])" label="Admin Look & Feel">
18
+ <PreferencesAdminAppearance :current-theme="currentTheme" />
19
+ </b-tab-item>
20
+ <b-tab-item v-if="userPermissions.has('importExport.getExport')" label="Website Data">
21
+ <ImportExport />
22
+ </b-tab-item>
23
+ </template>
24
+ </PreferencesList>
25
+ </template>
26
+
27
+ <script>
28
+
29
+ import PreferencesList from './PreferencesList.vue';
30
+ import PreferencesPageTemplates from './PreferencesPageTemplates.vue';
31
+ import PreferencesEventTemplates from './PreferencesEventTemplates.vue';
32
+ import PreferencesThemeChooser from './PreferencesThemeChooser.vue';
33
+ import PreferencesAuthentication from './PreferencesAuthentication.vue';
34
+ import PreferencesAdminAppearance from './PreferencesAdminAppearance.vue';
35
+ import PreferencesSiteAppearance from './PreferencesSiteAppearance.vue';
36
+ import ImportExport from '../ImportExport.vue';
37
+
38
+ export default {
39
+ name: "Preferences",
40
+ components: {
41
+ PreferencesList,
42
+ PreferencesPageTemplates,
43
+ PreferencesEventTemplates,
44
+ PreferencesThemeChooser,
45
+ PreferencesAuthentication,
46
+ PreferencesAdminAppearance,
47
+ PreferencesSiteAppearance,
48
+ ImportExport
49
+ },
50
+ props: {
51
+ extraPrefs: {
52
+ type: Object,
53
+ default: () => { return {}; }
54
+ }
55
+ },
56
+ data() {
57
+ return {
58
+ currentTheme: null,
59
+ }
60
+ },
61
+ computed: {
62
+ userPermissions() {
63
+ return this.$store.getters.userPermissions;
64
+ }
65
+ },
66
+ methods: {
67
+ onThemeChanged(currentTheme) {
68
+ this.currentTheme = currentTheme;
69
+ },
70
+ getExtraPrefsPermissions(category) {
71
+ return (this.extraPrefs[category] ?? []).map(pref => pref.key);
72
+ },
73
+ getExtraPrefs(category) {
74
+ return (this.extraPrefs[category] ?? []);
75
+ }
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <style scoped>
81
+
82
+ </style>
package/src/icons.js CHANGED
@@ -73,7 +73,8 @@ import {
73
73
  faLandmark,
74
74
  faFolderOpen,
75
75
  faImages,
76
- faMinusCircle
76
+ faMinusCircle,
77
+ faCalendarPlus, faPaperPlane
77
78
  } from "@fortawesome/free-solid-svg-icons";
78
79
 
79
80
  export const addIconsToLibrary = () => {
@@ -86,5 +87,5 @@ export const addIconsToLibrary = () => {
86
87
  faFileExcel, faFileCsv, faChevronCircleDown, faChevronCircleUp, faTrash,
87
88
  faEye, faEyeSlash, faCaretDown, faCaretUp, faUpload, faUser, faFolder, faHome, faFilePdf, faSignOutAlt, faTag,
88
89
  faFolderPlus, faTimes, faQuestionCircle, faFileUpload, faLandmark,
89
- faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages);
90
+ faFolderOpen, faFile, faFileAudio, faFileImage, faShare, faImages, faCalendarPlus, faPaperPlane);
90
91
  };
package/src/main.js CHANGED
@@ -12,6 +12,7 @@ import { AuthRoutes, makeAuthenticatedRoute } from "./routes";
12
12
  import createStore from "./store/index";
13
13
  import { checkAuthenticated } from "./AuthApi";
14
14
  import Error404 from "./components/Error404.vue";
15
+ import MainMenu from "./components/MainMenu.vue";
15
16
 
16
17
  /**
17
18
  * Creates the Vue.js Oxygen application, allowing for a few points of customization (i.e.: adding modules)
@@ -26,7 +27,8 @@ export default class OxygenUI {
26
27
  this.Vue = Vue;
27
28
  this.authenticatedRoutes = []
28
29
  this.unauthenticatedRoutes = []
29
- this.rootComponents = { App }
30
+ this.mainMenuItems = {}
31
+ this.rootComponents = { App, MainMenu }
30
32
  this.beforeMountHooks = []
31
33
  }
32
34
 
@@ -40,6 +42,17 @@ export default class OxygenUI {
40
42
  this.authenticatedRoutes.push(route);
41
43
  }
42
44
 
45
+ addMainMenuGroup(category, group) {
46
+ if(!this.mainMenuItems[category]) {
47
+ this.mainMenuItems[category] = {};
48
+ }
49
+ if(!group.items) {
50
+ group.items = {};
51
+ }
52
+ this.mainMenuItems[category][group.name] = group;
53
+ return group;
54
+ }
55
+
43
56
  addUnauthenticatedRoutes(routes) {
44
57
  for (let route of routes) {
45
58
  this.unauthenticatedRoutes.push(route);
@@ -91,6 +104,9 @@ export default class OxygenUI {
91
104
 
92
105
  this.app = new this.Vue({
93
106
  router: router,
107
+ data: {
108
+ mainMenuItems: this.mainMenuItems
109
+ },
94
110
  components: this.rootComponents,
95
111
  store
96
112
  });
@@ -109,3 +125,5 @@ export default class OxygenUI {
109
125
  }
110
126
  }
111
127
 
128
+ export const WEB_CONTENT = 'Web Content';
129
+
@@ -1,6 +1,40 @@
1
1
  import LegacyPage from "../components/LegacyPage.vue";
2
+ import { WEB_CONTENT } from "../main.js";
2
3
 
3
4
  export default function(ui) {
5
+ ui.addMainMenuGroup(WEB_CONTENT, {
6
+ name: 'Pages',
7
+ icon: 'file-alt',
8
+ listAction: '/pages',
9
+ listPermission: 'pages.getList',
10
+ addIcon: 'plus',
11
+ addPermission: 'pages.postCreate',
12
+ addAction: '/pages/create',
13
+ items: {
14
+ }
15
+ });
16
+ ui.addMainMenuGroup(WEB_CONTENT, {
17
+ name: 'Partials',
18
+ icon: 'puzzle-piece',
19
+ listAction: '/partials',
20
+ listPermission: 'partials.getList',
21
+ addIcon: 'plus',
22
+ addPermission: 'partials.postCreate',
23
+ addAction: '/partials/create',
24
+ items: {
25
+ }
26
+ });
27
+ ui.addMainMenuGroup(WEB_CONTENT, {
28
+ name: 'Events',
29
+ icon: 'calendar-alt',
30
+ listAction: '/upcoming-events',
31
+ listPermission: 'upcomingEvents.getList',
32
+ addIcon: 'calendar-plus',
33
+ addPermission: 'upcomingEvents.postCreate',
34
+ addAction: '/upcoming-events/create',
35
+ items: {
36
+ }
37
+ });
4
38
  ui.addAuthenticatedRoutes([
5
39
  {
6
40
  // will match everything, try to render a legacy Oxygen page...
@@ -1,7 +1,22 @@
1
1
  import MediaPage from "../components/media/MediaPage.vue";
2
2
  import MediaResponsiveImages from "../components/media/MediaResponsiveImages.vue";
3
+ import {WEB_CONTENT} from "../main";
3
4
 
4
5
  export default function(ui) {
6
+ ui.addMainMenuGroup(WEB_CONTENT, {
7
+ name: 'Photos & Files',
8
+ icon: 'photo-video',
9
+ groupPrefix: '/media',
10
+ addAction: '/media/list/?upload=true',
11
+ addIcon: 'upload',
12
+ addPermission: 'media.postCreate',
13
+ listAction: '/media/list',
14
+ listPermission: 'media.getList',
15
+ items: {
16
+ 'Deleted Photos & Files': { to: '/media/trash', permission: 'media.getList' },
17
+ 'Responsive Images': { to: '/media/responsive-images', permission: 'media.postMakeResponsive' }
18
+ }
19
+ });
5
20
  ui.addAuthenticatedRoutes([
6
21
  {
7
22
  path: 'media/list/:currentPath(.*)?',
@@ -10,7 +10,6 @@ $menu-item-active-color: lighten($grey, 4%);
10
10
 
11
11
  // $menu-item-hover-background-color: rgba(255, 255, 255, 0.1);
12
12
  // $menu-item-hover-color: $white-bis;
13
-
14
13
  //$navbar-item-active-color: #fff;
15
14
  //$navbar-item-hover-background-color: #f3f3f9;
16
15
  //
@@ -5,7 +5,7 @@
5
5
  @import "~buefy/src/scss/buefy";
6
6
 
7
7
  .navbar {
8
- box-shadow: 0px 0px 2px $dark;
8
+ box-shadow: 0 0 2px $dark;
9
9
  }
10
10
 
11
11
  .navbar-item {