@oxygen-cms/ui 1.7.1 → 1.8.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/{.eslintrc.js → .eslintrc.json} +3 -3
- package/package.json +14 -4
- package/src/CrudApi.js +23 -9
- package/src/MediaApi.js +3 -3
- package/src/PagesApi.js +48 -0
- package/src/PartialsApi.js +30 -0
- package/src/components/AuthenticatedLayout.vue +3 -1
- package/src/components/CodeEditor.vue +1 -1
- package/src/components/GroupsChooser.vue +2 -2
- package/src/components/GroupsList.vue +2 -2
- package/src/components/PageActions.vue +28 -0
- package/src/components/PageEdit.vue +164 -0
- package/src/components/PageNestedPagination.vue +27 -0
- package/src/components/PageNestedRow.vue +52 -0
- package/src/components/PageStatusIcon.vue +33 -0
- package/src/components/PageTable.vue +156 -0
- package/src/components/PartialActions.vue +28 -0
- package/src/components/PartialList.vue +74 -0
- package/src/components/PartialStatusIcon.vue +29 -0
- package/src/components/PartialTable.vue +65 -0
- package/src/components/ResourceList.vue +132 -0
- package/src/components/UserManagement.vue +2 -2
- package/src/components/UserProfileForm.vue +1 -1
- package/src/components/UserProfilePage.vue +1 -1
- package/src/components/content/CommandsList.vue +108 -0
- package/src/components/content/ContentEditor.vue +489 -0
- package/src/components/content/GridCellNodeView.vue +82 -0
- package/src/components/content/GridRowNodeView.vue +53 -0
- package/src/components/content/HtmlNodeView.vue +89 -0
- package/src/components/content/MarkMenu.vue +116 -0
- package/src/components/content/MediaNodeView.vue +83 -0
- package/src/components/content/ObjectLinkNodeView.vue +181 -0
- package/src/components/content/PartialNodeView.vue +217 -0
- package/src/components/content/commands.js +72 -0
- package/src/components/content/suggestion.js +211 -0
- package/src/components/media/MediaChooseDirectory.vue +2 -2
- package/src/components/media/MediaDirectory.vue +1 -1
- package/src/components/media/MediaInsertModal.vue +11 -2
- package/src/components/media/MediaItem.vue +1 -1
- package/src/components/media/MediaItemPreview.vue +18 -2
- package/src/components/media/MediaList.vue +4 -5
- package/src/components/media/MediaUpload.vue +1 -1
- package/src/components/media/media.scss +1 -0
- package/src/components/pages/PageList.vue +65 -0
- package/src/components/preferences/PreferencesField.vue +1 -1
- package/src/components/users/CreateUserModal.vue +1 -1
- package/src/components/util.css +1 -1
- package/src/icons.js +33 -5
- package/src/main.js +4 -0
- package/src/modules/PagesPartials.js +74 -2
- package/src/styles/pages-table.scss +34 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
{
|
|
2
2
|
"env": {
|
|
3
3
|
"browser": true,
|
|
4
4
|
"es2021": true,
|
|
@@ -11,7 +11,7 @@ module.exports = {
|
|
|
11
11
|
"prettier"
|
|
12
12
|
],
|
|
13
13
|
"parserOptions": {
|
|
14
|
-
"ecmaVersion":
|
|
14
|
+
"ecmaVersion": 2022,
|
|
15
15
|
"sourceType": "module"
|
|
16
16
|
},
|
|
17
17
|
"plugins": [
|
|
@@ -20,4 +20,4 @@ module.exports = {
|
|
|
20
20
|
],
|
|
21
21
|
"rules": {
|
|
22
22
|
}
|
|
23
|
-
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxygen-cms/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Various utilities for UI-building in Vue.js",
|
|
5
5
|
"main": "none",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/oxygen-cms/ui.git"
|
|
9
9
|
},
|
|
10
|
+
"type": "module",
|
|
10
11
|
"author": "Chris Chamberlain <chris@chamberlain.id.au>",
|
|
11
12
|
"license": "MIT",
|
|
12
13
|
"private": false,
|
|
@@ -15,6 +16,12 @@
|
|
|
15
16
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
|
16
17
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
|
17
18
|
"@fortawesome/vue-fontawesome": "^0.1.10",
|
|
19
|
+
"@tiptap/core": "^2.0.0-beta.209",
|
|
20
|
+
"@tiptap/extension-link": "^2.0.0-beta.209",
|
|
21
|
+
"@tiptap/extension-underline": "^2.0.0-beta.209",
|
|
22
|
+
"@tiptap/starter-kit": "^2.0.0-beta.209",
|
|
23
|
+
"@tiptap/suggestion": "^2.0.0-beta.209",
|
|
24
|
+
"@tiptap/vue-2": "^2.0.0-beta.209",
|
|
18
25
|
"autoprefixer": "^9.8.5",
|
|
19
26
|
"brace": "^0.11.1",
|
|
20
27
|
"buefy": "^0.9.10",
|
|
@@ -23,6 +30,8 @@
|
|
|
23
30
|
"downloadjs": "^1.4.7",
|
|
24
31
|
"libphonenumber-js": "^1.9.11",
|
|
25
32
|
"lodash": "^4.17.21",
|
|
33
|
+
"portal-vue": "^2.1.7",
|
|
34
|
+
"tippy.js": "^6.3.7",
|
|
26
35
|
"title-case": "^3.0.3",
|
|
27
36
|
"ua-parser-js": "^0.7.24",
|
|
28
37
|
"v-hotkey": "^0.8.0",
|
|
@@ -32,16 +41,17 @@
|
|
|
32
41
|
"vue-router": "^3.5.1",
|
|
33
42
|
"vue-template-compiler": "^2.6.11",
|
|
34
43
|
"vue2-ace-editor": "^0.0.15",
|
|
35
|
-
"vuex": "^3.6.2"
|
|
44
|
+
"vuex": "^3.6.2",
|
|
45
|
+
"zeed-dom": "^0.9.26"
|
|
36
46
|
},
|
|
37
47
|
"devDependencies": {
|
|
38
48
|
"@babel/core": "^7.13.1",
|
|
39
49
|
"@babel/preset-env": "^7.13.5",
|
|
40
50
|
"babel-jest": "^26.6.3",
|
|
41
51
|
"babel-polyfill": "^6.26.0",
|
|
42
|
-
"eslint": "^
|
|
52
|
+
"eslint": "^8.0",
|
|
43
53
|
"eslint-config-prettier": "^8.3.0",
|
|
44
|
-
"eslint-plugin-jest": "^
|
|
54
|
+
"eslint-plugin-jest": "^27.2.1",
|
|
45
55
|
"eslint-plugin-vue": "^7.17.0",
|
|
46
56
|
"jest": "^26.6.3",
|
|
47
57
|
"regenerator-runtime": "^0.13.7",
|
package/src/CrudApi.js
CHANGED
|
@@ -9,12 +9,13 @@ export const getApiRoot = () => {
|
|
|
9
9
|
|
|
10
10
|
export class CrudApi {
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
static setBuefy($buefy) {
|
|
13
13
|
this.$buefy = $buefy;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
request(method) {
|
|
17
|
-
|
|
17
|
+
if(!CrudApi.$buefy) { throw new Error("need to call setBuefy() first"); }
|
|
18
|
+
return FetchBuilder.default(CrudApi.$buefy, method);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
static getResourceName() {
|
|
@@ -31,12 +32,18 @@ export class CrudApi {
|
|
|
31
32
|
return m;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
static convertModelFromAPI(data) {
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async list({ inTrash, page, q, sortField, sortOrder }) {
|
|
35
40
|
return this.request('get')
|
|
36
41
|
.withQueryParams({
|
|
37
|
-
page: (
|
|
42
|
+
page: (q !== null && q !== '' ) ? null : page,
|
|
43
|
+
sortField: sortField,
|
|
44
|
+
sortOrder: sortOrder,
|
|
38
45
|
trash: (inTrash ? 'true' : 'false'),
|
|
39
|
-
q: (
|
|
46
|
+
q: (q !== null && q !== '' ) ? q : null
|
|
40
47
|
})
|
|
41
48
|
.fetch(this.constructor.getResourceRoot());
|
|
42
49
|
}
|
|
@@ -77,7 +84,7 @@ export class CrudApi {
|
|
|
77
84
|
|
|
78
85
|
async confirmForceDelete(id) {
|
|
79
86
|
const promise = new Promise((resolve) => {
|
|
80
|
-
|
|
87
|
+
CrudApi.$buefy.dialog.confirm({
|
|
81
88
|
message: 'Are you sure you want to delete this record forever?',
|
|
82
89
|
onConfirm: resolve
|
|
83
90
|
});
|
|
@@ -85,20 +92,20 @@ export class CrudApi {
|
|
|
85
92
|
|
|
86
93
|
await promise;
|
|
87
94
|
let data = await this.forceDelete(id);
|
|
88
|
-
|
|
95
|
+
CrudApi.$buefy.toast.open(morphToNotification(data));
|
|
89
96
|
return data;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
async restoreAndNotify(id) {
|
|
93
100
|
let data = await this.request('post')
|
|
94
101
|
.fetch(this.constructor.getResourceRoot() + '/' + id + '/restore');
|
|
95
|
-
|
|
102
|
+
CrudApi.$buefy.toast.open(morphToNotification(data));
|
|
96
103
|
return data;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
async deleteAndNotify(id) {
|
|
100
107
|
let data = await this.delete(id);
|
|
101
|
-
|
|
108
|
+
CrudApi.$buefy.toast.open(morphToNotification(data));
|
|
102
109
|
return data;
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -111,4 +118,11 @@ export class CrudApi {
|
|
|
111
118
|
return this.request('post')
|
|
112
119
|
.fetch(this.constructor.getResourceRoot() + '/' + id + '/make-head');
|
|
113
120
|
}
|
|
121
|
+
|
|
122
|
+
async publish(id) {
|
|
123
|
+
let data = await this.request('post').fetch(this.constructor.getResourceRoot() + '/' + id + '/publish');
|
|
124
|
+
CrudApi.$buefy.toast.open(morphToNotification(data));
|
|
125
|
+
return data.item;
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
}
|
package/src/MediaApi.js
CHANGED
|
@@ -33,12 +33,12 @@ export default class MediaApi extends CrudApi {
|
|
|
33
33
|
.fetch(this.constructor.getResourceRoot());
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
async list(inTrash,
|
|
36
|
+
async list({ inTrash, q, page, path }) {
|
|
37
37
|
return this.request('get')
|
|
38
38
|
.withQueryParams({
|
|
39
39
|
path: path,
|
|
40
|
-
q: (
|
|
41
|
-
page: (
|
|
40
|
+
q: (q !== null && q !== '' ) ? q : null,
|
|
41
|
+
page: (q !== null && q !== '' ) ? null : page,
|
|
42
42
|
trash: (inTrash ? 'true' : 'false'),
|
|
43
43
|
})
|
|
44
44
|
.fetch(this.constructor.getResourceRoot());
|
package/src/PagesApi.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CrudApi } from './CrudApi';
|
|
2
|
+
import {getApiHost} from "./api.js";
|
|
3
|
+
|
|
4
|
+
export default class PagesApi extends CrudApi {
|
|
5
|
+
|
|
6
|
+
static STAGE_DRAFT = 0;
|
|
7
|
+
static STAGE_PENDING_REVIEW = 1;
|
|
8
|
+
static STAGE_PUBLISHED = 2;
|
|
9
|
+
static STAGE_ARCHIVED = 3;
|
|
10
|
+
|
|
11
|
+
static getResourceName() {
|
|
12
|
+
return 'pages';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
static prepareModelForAPI(data) {
|
|
16
|
+
let m = { ...data };
|
|
17
|
+
delete m.id;
|
|
18
|
+
delete m.createdAt;
|
|
19
|
+
delete m.updatedAt;
|
|
20
|
+
delete m.deletedAt;
|
|
21
|
+
delete m.createdBy;
|
|
22
|
+
delete m.updatedBy;
|
|
23
|
+
delete m.headVersion;
|
|
24
|
+
return m;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static slugToUrl(slug) {
|
|
28
|
+
if(slug === '/') { slug = ''; }
|
|
29
|
+
return getApiHost() + slug;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async list({ inTrash, page, q, path, sortField, sortOrder }) {
|
|
33
|
+
if(!path) {
|
|
34
|
+
path = q ? null : '/';
|
|
35
|
+
}
|
|
36
|
+
return this.request('get')
|
|
37
|
+
.withQueryParams({
|
|
38
|
+
path: path,
|
|
39
|
+
page: (q !== null && q !== '' ) ? null : page,
|
|
40
|
+
trash: (inTrash ? 'true' : 'false'),
|
|
41
|
+
q: (q !== null && q !== '' ) ? q : null,
|
|
42
|
+
sortField,
|
|
43
|
+
sortOrder
|
|
44
|
+
})
|
|
45
|
+
.fetch(this.constructor.getResourceRoot());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CrudApi } from './CrudApi';
|
|
2
|
+
|
|
3
|
+
export default class PartialsApi extends CrudApi {
|
|
4
|
+
|
|
5
|
+
static STAGE_DRAFT = 0;
|
|
6
|
+
static STAGE_PUBLISHED = 1;
|
|
7
|
+
|
|
8
|
+
static getResourceName() {
|
|
9
|
+
return 'partials';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static prepareModelForAPI(data) {
|
|
13
|
+
let m = { ...data };
|
|
14
|
+
delete m.id;
|
|
15
|
+
return m;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async list({ inTrash, page, q, sortField, sortOrder }) {
|
|
19
|
+
return this.request('get')
|
|
20
|
+
.withQueryParams({
|
|
21
|
+
page: (q !== null && q !== '' ) ? null : page,
|
|
22
|
+
trash: (inTrash ? 'true' : 'false'),
|
|
23
|
+
q: (q !== null && q !== '' ) ? q : null,
|
|
24
|
+
sortField,
|
|
25
|
+
sortOrder
|
|
26
|
+
})
|
|
27
|
+
.fetch(this.constructor.getResourceRoot());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
}
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
<router-view></router-view>
|
|
52
52
|
</transition>
|
|
53
53
|
|
|
54
|
+
<portal-target name="editors"></portal-target>
|
|
55
|
+
|
|
54
56
|
</div>
|
|
55
57
|
</div>
|
|
56
58
|
</template>
|
|
@@ -66,7 +68,7 @@ export default {
|
|
|
66
68
|
emits: ["logout"],
|
|
67
69
|
data() {
|
|
68
70
|
return {
|
|
69
|
-
usersApi: new UsersApi(
|
|
71
|
+
usersApi: new UsersApi(),
|
|
70
72
|
setCollapsed: false,
|
|
71
73
|
requestedCollapsed: false
|
|
72
74
|
}
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
},
|
|
31
31
|
data() {
|
|
32
32
|
return {
|
|
33
|
-
groupsApi: new GroupsApi(
|
|
33
|
+
groupsApi: new GroupsApi(),
|
|
34
34
|
loading: true,
|
|
35
35
|
users: []
|
|
36
36
|
}
|
|
@@ -46,7 +46,7 @@ export default {
|
|
|
46
46
|
methods: {
|
|
47
47
|
async fetchData(name) {
|
|
48
48
|
this.loading = true;
|
|
49
|
-
this.users = (await this.groupsApi.list(false, 1, name)).items;
|
|
49
|
+
this.users = (await this.groupsApi.list({ inTrash: false, page: 1, q: name })).items;
|
|
50
50
|
this.loading = false;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -86,7 +86,7 @@ export default {
|
|
|
86
86
|
components: { UserJoined, GenericEditableField, EditButtonOnRowHover },
|
|
87
87
|
data() {
|
|
88
88
|
return {
|
|
89
|
-
groupsApi: new GroupsApi(
|
|
89
|
+
groupsApi: new GroupsApi(),
|
|
90
90
|
paginatedItems: {items: null, totalItems: null, itemsPerPage: null, loading: false, currentPage: 1},
|
|
91
91
|
}
|
|
92
92
|
},
|
|
@@ -96,7 +96,7 @@ export default {
|
|
|
96
96
|
methods: {
|
|
97
97
|
async fetchData() {
|
|
98
98
|
this.paginatedItems.loading = true;
|
|
99
|
-
let data = await this.groupsApi.list(false, this.paginatedItems.currentPage, null);
|
|
99
|
+
let data = await this.groupsApi.list({ inTrash: false, page: this.paginatedItems.currentPage, q: null });
|
|
100
100
|
this.paginatedItems.items = data.items;
|
|
101
101
|
this.paginatedItems.totalItems = data.totalItems;
|
|
102
102
|
this.paginatedItems.itemsPerPage = data.itemsPerPage;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<b-button v-if="item.stage !== STAGE_PUBLISHED" rounded size="is-small" icon-left="globe-asia" class="mr-2" @click="publish">Publish</b-button>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import PagesApi from "../PagesApi.js";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: "PageActions",
|
|
12
|
+
props: {
|
|
13
|
+
item: { type: Object, required: true }
|
|
14
|
+
},
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
STAGE_PUBLISHED: PagesApi.STAGE_PUBLISHED,
|
|
18
|
+
pagesApi: new PagesApi()
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
methods: {
|
|
22
|
+
async publish() {
|
|
23
|
+
let item = await this.pagesApi.publish(this.item.id);
|
|
24
|
+
this.$emit('update', item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="full-height is-flex is-flex-direction-column is-align-items-stretch">
|
|
3
|
+
<div class="page-details is-flex is-flex-direction-row">
|
|
4
|
+
<div class="page-title is-flex-grow-1">
|
|
5
|
+
<transition name="fade" mode="out-in">
|
|
6
|
+
<b-skeleton v-if="loading" key="skeleton"></b-skeleton>
|
|
7
|
+
<span v-else key="name" class="title">Edit Page - {{ model.title }}</span>
|
|
8
|
+
</transition>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="is-flex-grow-1"></div>
|
|
11
|
+
<div>
|
|
12
|
+
<b-switch :value="editable" @input="v => editable = v">Editable?</b-switch>
|
|
13
|
+
<b-button icon-left="cog" @click="editPageModalActive = true">Page Settings</b-button>
|
|
14
|
+
<b-button type="is-primary" icon-left="save" @click="save">Save</b-button>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<b-modal :active="editPageModalActive" has-modal-card trap-focus aria-modal auto-focus @update:active="v => editPageModalActive = v">
|
|
18
|
+
<div class="modal-card is-relative">
|
|
19
|
+
<div class="modal-card-head">
|
|
20
|
+
<p class="modal-card-title">Edit Page Settings</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="modal-card-body">
|
|
23
|
+
<b-loading :active="loading" :is-full-page="false"></b-loading>
|
|
24
|
+
<div v-if="!loading">
|
|
25
|
+
<b-field label="Title">
|
|
26
|
+
<b-input v-model="model.title" placeholder="Title"></b-input>
|
|
27
|
+
</b-field>
|
|
28
|
+
<b-field>
|
|
29
|
+
<template #label>
|
|
30
|
+
URL
|
|
31
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="The URL at which this page is located. It can only contain letters, numbers, slashes and dashes. Valid examples include: '/', 'my-page-123', 'some/nested/page'">
|
|
32
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
33
|
+
</b-tooltip>
|
|
34
|
+
</template>
|
|
35
|
+
<b-input v-model="model.slug" placeholder="slug"></b-input>
|
|
36
|
+
</b-field>
|
|
37
|
+
<b-field label="Description">
|
|
38
|
+
<b-input v-model="model.description" type="textarea" placeholder="page description"></b-input>
|
|
39
|
+
</b-field>
|
|
40
|
+
<b-field>
|
|
41
|
+
<template #label>
|
|
42
|
+
Tags
|
|
43
|
+
<b-tooltip multilined position="is-right" type="is-dark" label="A list of keywords for this page. Used for SEO">
|
|
44
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
45
|
+
</b-tooltip>
|
|
46
|
+
</template>
|
|
47
|
+
<b-taginput v-model="model.tags"></b-taginput>
|
|
48
|
+
</b-field>
|
|
49
|
+
<!-- <b-collapse-->
|
|
50
|
+
<!-- class="card"-->
|
|
51
|
+
<!-- animation="slide"-->
|
|
52
|
+
<!-- :open="false">-->
|
|
53
|
+
<!-- <template #trigger="props">-->
|
|
54
|
+
<!-- <div-->
|
|
55
|
+
<!-- class="card-header"-->
|
|
56
|
+
<!-- role="button"-->
|
|
57
|
+
<!-- :aria-expanded="props.open"-->
|
|
58
|
+
<!-- >-->
|
|
59
|
+
<!-- <p class="card-header-title">-->
|
|
60
|
+
<!-- Advanced Options-->
|
|
61
|
+
<!-- </p>-->
|
|
62
|
+
<!-- <a class="card-header-icon">-->
|
|
63
|
+
<!-- <b-icon-->
|
|
64
|
+
<!-- :icon="props.open ? 'caret-down' : 'caret-up'">-->
|
|
65
|
+
<!-- </b-icon>-->
|
|
66
|
+
<!-- </a>-->
|
|
67
|
+
<!-- </div>-->
|
|
68
|
+
<!-- </template>-->
|
|
69
|
+
<!-- <div class="card-content">-->
|
|
70
|
+
<!-- <div class="content">-->
|
|
71
|
+
<div class="field">
|
|
72
|
+
<label class="label">
|
|
73
|
+
Metadata
|
|
74
|
+
<b-tooltip multilined label="An HTML field used to inject custom metadata into the page. Used for SEO." position="is-right" type="is-dark">
|
|
75
|
+
<b-icon size="is-small" icon="question-circle"></b-icon>
|
|
76
|
+
</b-tooltip>
|
|
77
|
+
</label>
|
|
78
|
+
<div class="control">
|
|
79
|
+
<CodeEditor v-model="model.meta" lang="html" height="10rem" />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="field">
|
|
83
|
+
<label class="label">Options</label>
|
|
84
|
+
<div class="control">
|
|
85
|
+
<CodeEditor v-model="model.options" lang="json" height="10rem" />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<!-- </div>-->
|
|
89
|
+
<!-- </div>-->
|
|
90
|
+
<!-- </b-collapse>-->
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</b-modal>
|
|
95
|
+
<div class="is-relative editor-parent">
|
|
96
|
+
<b-loading :active="loading" :is-full-page="false"></b-loading>
|
|
97
|
+
<ContentEditor v-if="!loading" :editable="editable" :expanded="true" :content="model.richContent" @update:content="v => model.richContent = v" />
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
import ContentEditor from "./content/ContentEditor.vue";
|
|
104
|
+
import PagesApi from "../PagesApi";
|
|
105
|
+
import CodeEditor from "./CodeEditor.vue";
|
|
106
|
+
import {morphToNotification} from "../api.js";
|
|
107
|
+
export default {
|
|
108
|
+
name: "PageCreate",
|
|
109
|
+
components: {ContentEditor, CodeEditor},
|
|
110
|
+
data() {
|
|
111
|
+
return {
|
|
112
|
+
loading: true,
|
|
113
|
+
model: null,
|
|
114
|
+
serverModel: null,
|
|
115
|
+
editable: true,
|
|
116
|
+
editPageModalActive: false,
|
|
117
|
+
pagesApi: new PagesApi(),
|
|
118
|
+
// content: { type: 'doc', content: [{type:'paragraph',content:[{type:'text',text:'I’m running Tiptap with Vue.js. 🎉'}]}]}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
mounted() {
|
|
122
|
+
this.fetchData()
|
|
123
|
+
},
|
|
124
|
+
methods: {
|
|
125
|
+
async fetchData() {
|
|
126
|
+
this.loading = true;
|
|
127
|
+
let res = await this.pagesApi.get(this.$route.params.id);
|
|
128
|
+
console.log(res);
|
|
129
|
+
this.setModel(res.item);
|
|
130
|
+
},
|
|
131
|
+
setModel(model) {
|
|
132
|
+
this.loading = false;
|
|
133
|
+
this.model = model;
|
|
134
|
+
this.serverModel = model;
|
|
135
|
+
},
|
|
136
|
+
async save() {
|
|
137
|
+
// this.submittingForm = true;
|
|
138
|
+
let response = await this.pagesApi.update(this.model);
|
|
139
|
+
// this.submittingForm = false;
|
|
140
|
+
this.setModel(response.item);
|
|
141
|
+
this.$buefy.toast.open(morphToNotification(response));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<style scoped>
|
|
148
|
+
@import "../components/util.css";
|
|
149
|
+
|
|
150
|
+
.page-details {
|
|
151
|
+
padding: 1rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.editor-parent {
|
|
155
|
+
flex: 1;
|
|
156
|
+
overflow-y: auto;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/*.editor-gutter {*/
|
|
160
|
+
/* width: 160px;*/
|
|
161
|
+
/* background-color: #444;*/
|
|
162
|
+
/* box-shadow: inset -5px 0 10px 2px #222;*/
|
|
163
|
+
/*}*/
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr v-if="pagesByParent[item.id] && pagesByParent[item.id].totalItems > pagesByParent[item.id].itemsPerPage" :class="'row-depth-' + depth">
|
|
3
|
+
<td></td>
|
|
4
|
+
<td colspan="5">
|
|
5
|
+
<div class="is-flex">
|
|
6
|
+
<div class="is-flex-grow-1"></div>
|
|
7
|
+
<b-pagination :total="pagesByParent[item.id].totalItems" :current="pagesByParent[item.id].currentPage" :per-page="pagesByParent[item.id].itemsPerPage" @change="p => paginate(item, p)"></b-pagination>
|
|
8
|
+
</div>
|
|
9
|
+
</td>
|
|
10
|
+
</tr>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
export default {
|
|
15
|
+
name: "PageNestedPagination",
|
|
16
|
+
props: {
|
|
17
|
+
item: Object,
|
|
18
|
+
pagesByParent: Object,
|
|
19
|
+
depth: Number,
|
|
20
|
+
paginate: Function
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
|
|
27
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<tr :class="'row-depth-' + depth + (isFirst ? ' first-child' : '')">
|
|
3
|
+
<td>
|
|
4
|
+
<a v-if="item.numChildren > 0" role="button" class="expand-button" @click="event => $emit('toggle-expand', item)">
|
|
5
|
+
<b-icon :icon="expanded ? 'angle-down' : 'angle-right'"></b-icon>
|
|
6
|
+
</a>
|
|
7
|
+
</td>
|
|
8
|
+
<td>{{ item.title }} <PageStatusIcon :item="item"></PageStatusIcon></td>
|
|
9
|
+
<td><a :href="PagesApi.slugToUrl(item.slug)" class="is-size-7" target="_blank">{{ PagesApi.slugToUrl(item.slug) }} <b-icon icon="external-link-alt"></b-icon></a></td>
|
|
10
|
+
<td><div class="is-size-7">{{ item.description }}</div></td>
|
|
11
|
+
<td>
|
|
12
|
+
<div v-if="item" class="is-size-7"><Updated :model="item"></Updated></div>
|
|
13
|
+
</td>
|
|
14
|
+
<td><slot name="actions" :row="item"></slot></td>
|
|
15
|
+
</tr>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script>
|
|
19
|
+
import PageStatusIcon from "./PageStatusIcon.vue";
|
|
20
|
+
import Updated from "./Updated.vue";
|
|
21
|
+
import PagesApi from "../PagesApi.js";
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
name: "PageNestedRow",
|
|
25
|
+
components: {PageStatusIcon, Updated},
|
|
26
|
+
props: {
|
|
27
|
+
item: Object,
|
|
28
|
+
isFirst: Boolean,
|
|
29
|
+
depth: Number,
|
|
30
|
+
expanded: Boolean
|
|
31
|
+
},
|
|
32
|
+
data() {
|
|
33
|
+
return {
|
|
34
|
+
PagesApi: PagesApi
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<style scoped lang="scss">
|
|
41
|
+
@import "../styles/pages-table.scss";
|
|
42
|
+
|
|
43
|
+
//.has-darker-top-border {
|
|
44
|
+
////border-top-color: $grey-dark;
|
|
45
|
+
////border-top-width: 1px;
|
|
46
|
+
// box-shadow: 0 10px 10px -10px inset $grey-dark;
|
|
47
|
+
//}
|
|
48
|
+
|
|
49
|
+
.expand-button:hover {
|
|
50
|
+
color: $primary !important;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-icon :icon="statusIcon"></b-icon>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import PagesApi from "../PagesApi.js";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: "PageStatusIcon",
|
|
10
|
+
props: {
|
|
11
|
+
item: Object
|
|
12
|
+
},
|
|
13
|
+
computed: {
|
|
14
|
+
statusIcon() {
|
|
15
|
+
if(this.item.stage === PagesApi.STAGE_ARCHIVED) {
|
|
16
|
+
return 'archive';
|
|
17
|
+
} else if(this.item.stage === PagesApi.STAGE_DRAFT) {
|
|
18
|
+
return 'pen-square';
|
|
19
|
+
} else if(this.item.stage === PagesApi.STAGE_PENDING_REVIEW) {
|
|
20
|
+
return 'user-edit';
|
|
21
|
+
} else if(this.item.stage === PagesApi.STAGE_PUBLISHED) {
|
|
22
|
+
return 'globe-asia';
|
|
23
|
+
} else {
|
|
24
|
+
throw new Error('unknown stage ' + this.item.stage);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
|
|
33
|
+
</style>
|