@oxygen-cms/ui 1.7.2 → 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/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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-table
|
|
3
|
+
ref="table"
|
|
4
|
+
:data="pagesByParent[0].items"
|
|
5
|
+
:loading="pagesByParent[0].loading"
|
|
6
|
+
custom-row-key="id"
|
|
7
|
+
:paginated="pagesByParent[0].totalItems > pagesByParent[0].itemsPerPage"
|
|
8
|
+
backend-pagination
|
|
9
|
+
:total="pagesByParent[0].totalItems"
|
|
10
|
+
:per-page="pagesByParent[0].itemsPerPage"
|
|
11
|
+
:current-page="pagesByParent[0].currentPage"
|
|
12
|
+
detailed
|
|
13
|
+
:has-detailed-visible="rowHasChildren"
|
|
14
|
+
custom-detail-row
|
|
15
|
+
:backend-sorting="!!onSort"
|
|
16
|
+
default-sort-direction="asc"
|
|
17
|
+
:default-sort="[sortField, sortOrder]"
|
|
18
|
+
sticky-header
|
|
19
|
+
detail-key="id"
|
|
20
|
+
aria-next-label="Next page"
|
|
21
|
+
aria-previous-label="Previous page"
|
|
22
|
+
aria-page-label="Page"
|
|
23
|
+
aria-current-label="Current page"
|
|
24
|
+
class="full-height-flex full-height-container"
|
|
25
|
+
@page-change="onPageChange"
|
|
26
|
+
@details-open="item => setExpanded(item, true)"
|
|
27
|
+
@details-close="item => setExpanded(item, false)"
|
|
28
|
+
@sort="onSort">
|
|
29
|
+
<b-table-column v-slot="props" label="Title" :sortable="!!onSort" field="title">{{ props.row.title }} <PageStatusIcon :item="props.row"></PageStatusIcon></b-table-column>
|
|
30
|
+
<b-table-column v-slot="props" label="URL" :sortable="!!onSort" field="slugPart"><a :href="PagesApi.slugToUrl(props.row.slug)" class="is-size-7" target="_blank">{{ PagesApi.slugToUrl(props.row.slug) }} <b-icon icon="external-link-alt"></b-icon></a></b-table-column>
|
|
31
|
+
<b-table-column v-slot="props" label="Description" width="30%" :sortable="!!onSort" field="description"><div class="is-size-7">{{ props.row.description }}</div></b-table-column>
|
|
32
|
+
<b-table-column v-slot="props" label="Last Updated" field="updatedAt" :sortable="!!onSort">
|
|
33
|
+
<div v-if="props.row.updatedAt" class="is-size-7"><Updated :model="props.row"></Updated></div>
|
|
34
|
+
</b-table-column>
|
|
35
|
+
<b-table-column v-slot="props" class="toolbar">
|
|
36
|
+
<slot name="actions" :row="props.row"></slot>
|
|
37
|
+
</b-table-column>
|
|
38
|
+
<!-- TODO: this is a massive hack, only supports up to a certain level of nesting -->
|
|
39
|
+
<template #detail="slot">
|
|
40
|
+
<template v-if="expanded[slot.row.id]">
|
|
41
|
+
<template v-for="(item1, i) in pagesByParent[slot.row.id].items">
|
|
42
|
+
<PageNestedRow :key="item1.id" :item="item1" :is-first="i === 0" :depth="1" :expanded="expanded[item1.id]" @toggle-expand="item => setExpanded(item, !expanded[item.id])">
|
|
43
|
+
<template #actions="props"><slot name="actions" :row="props.row"></slot></template>
|
|
44
|
+
</PageNestedRow>
|
|
45
|
+
<template v-if="expanded[item1.id]">
|
|
46
|
+
<template v-for="(item2, j) in pagesByParent[item1.id].items">
|
|
47
|
+
<PageNestedRow :key="item2.id" :item="item2" :is-first="j === 0" :depth="2" :expanded="expanded[item2.id]" @toggle-expand="item => setExpanded(item, !expanded[item.id])">
|
|
48
|
+
<template #actions="props"><slot name="actions" :row="props.row"></slot></template>
|
|
49
|
+
</PageNestedRow>
|
|
50
|
+
<template v-if="expanded[item2.id]">
|
|
51
|
+
<template v-for="(item3, k) in pagesByParent[item2.id].items">
|
|
52
|
+
<PageNestedRow :key="item3.id" :item="item3" :is-first="k === 0" :depth="3" :expanded="expanded[item3.id]" @toggle-expand="item => setExpanded(item, !expanded[item.id])">
|
|
53
|
+
<template #actions="props"><slot name="actions" :row="props.row"></slot></template>
|
|
54
|
+
</PageNestedRow>
|
|
55
|
+
</template></template>
|
|
56
|
+
<PageNestedPagination v-if="expanded[item2.id]" :key="item2.key" :item="item2" :pages-by-parent="pagesByParent" :depth="3" :paginate="paginate"></PageNestedPagination>
|
|
57
|
+
</template>
|
|
58
|
+
</template>
|
|
59
|
+
<PageNestedPagination v-if="expanded[item1.id]" :key="item1.key" :item="item1" :pages-by-parent="pagesByParent" :depth="2" :paginate="paginate"></PageNestedPagination>
|
|
60
|
+
</template>
|
|
61
|
+
<PageNestedPagination :item="slot.row" :pages-by-parent="pagesByParent" :depth="1" :paginate="paginate"></PageNestedPagination>
|
|
62
|
+
</template>
|
|
63
|
+
</template>
|
|
64
|
+
</b-table>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script>
|
|
68
|
+
import PageStatusIcon from "./PageStatusIcon.vue";
|
|
69
|
+
import Updated from "./Updated.vue";
|
|
70
|
+
import PagesApi from "../PagesApi.js";
|
|
71
|
+
import Vue from "vue";
|
|
72
|
+
import PageNestedRow from "./PageNestedRow.vue";
|
|
73
|
+
import PageNestedPagination from "./PageNestedPagination.vue";
|
|
74
|
+
|
|
75
|
+
export default {
|
|
76
|
+
name: "PageTable",
|
|
77
|
+
components: {PageNestedPagination, PageNestedRow, PageStatusIcon, Updated},
|
|
78
|
+
props: {
|
|
79
|
+
sortField: String,
|
|
80
|
+
sortOrder: String,
|
|
81
|
+
onSort: { type: Function, default: () => {} },
|
|
82
|
+
paginatedItems: { type: Object },
|
|
83
|
+
onPageChange: Function,
|
|
84
|
+
},
|
|
85
|
+
data() {
|
|
86
|
+
return {
|
|
87
|
+
pagesApi: new PagesApi(),
|
|
88
|
+
PagesApi: PagesApi,
|
|
89
|
+
expanded: {},
|
|
90
|
+
// `0` denotes the "root" level
|
|
91
|
+
pagesByParent: {0: {items: [], loading: true, totalItems: 0, itemsPerPage: 0, currentPage: 1}},
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
watch: {
|
|
95
|
+
'paginatedItems.items': 'loadAllDetails'
|
|
96
|
+
},
|
|
97
|
+
beforeMount() {
|
|
98
|
+
this.loadAllDetails();
|
|
99
|
+
},
|
|
100
|
+
methods: {
|
|
101
|
+
async loadAllDetails() {
|
|
102
|
+
this.pagesByParent[0].loading = true;
|
|
103
|
+
this.pagesByParent[0].items = await Promise.all(this.paginatedItems.items.map(this.loadChildren));
|
|
104
|
+
this.pagesByParent[0].totalItems = this.paginatedItems.totalItems;
|
|
105
|
+
this.pagesByParent[0].itemsPerPage = this.paginatedItems.itemsPerPage;
|
|
106
|
+
this.pagesByParent[0].currentPage = this.paginatedItems.currentPage;
|
|
107
|
+
this.pagesByParent[0].loading = false;
|
|
108
|
+
},
|
|
109
|
+
rowHasChildren(row) {
|
|
110
|
+
return row.numChildren > 0;
|
|
111
|
+
},
|
|
112
|
+
async loadChildren(page) {
|
|
113
|
+
return await this.paginate(page, 1);
|
|
114
|
+
},
|
|
115
|
+
async paginate(page, pageNum) {
|
|
116
|
+
if (!this.pagesByParent[page.id]) {
|
|
117
|
+
Vue.set(this.pagesByParent, page.id, {items: [], itemsPerPage: 1, totalItems: 0, currentPage: pageNum});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// if the slug is the root ( "/" ) then we don't show any child pages, since they're displayed as sibling pages.
|
|
121
|
+
if (page.slug !== '/' && page.numChildren > 0) {
|
|
122
|
+
console.log('loading children for', page.slug, 'page #', pageNum);
|
|
123
|
+
let result = await this.pagesApi.list({ inTrash: false, page: pageNum, q: null, path: page.slug, sortField: this.sortField, sortOrder: this.sortOrder });
|
|
124
|
+
// TODO: this currently recursively grabs all children ==> as many as N http requests where N is the number of pages in the site.
|
|
125
|
+
// Can we be more efficient?
|
|
126
|
+
this.pagesByParent[page.id].items = await Promise.all(result.items.map(this.loadChildren));
|
|
127
|
+
this.pagesByParent[page.id].itemsPerPage = result.itemsPerPage;
|
|
128
|
+
this.pagesByParent[page.id].totalItems = result.totalItems;
|
|
129
|
+
this.pagesByParent[page.id].currentPage = pageNum;
|
|
130
|
+
} else {
|
|
131
|
+
this.pagesByParent[page.id].items = [];
|
|
132
|
+
this.pagesByParent[page.id].itemsPerPage = 0;
|
|
133
|
+
this.pagesByParent[page.id].totalItems = 0;
|
|
134
|
+
this.pagesByParent[page.id].currentPage = 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return page;
|
|
138
|
+
},
|
|
139
|
+
async setExpanded(item, expanded) {
|
|
140
|
+
let o = {};
|
|
141
|
+
o[item.id] = expanded;
|
|
142
|
+
this.expanded = Object.assign({}, this.expanded, o);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style scoped lang="scss">
|
|
149
|
+
@import "./util.css";
|
|
150
|
+
|
|
151
|
+
::v-deep .table-wrapper.has-sticky-header {
|
|
152
|
+
flex: 1 1 auto;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@import "../styles/pages-table";
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<b-button v-if="item.stage !== STAGE_PUBLISHED" class="mr-2" rounded size="is-small" icon-left="globe-asia" @click="publish">Publish</b-button>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script>
|
|
8
|
+
import PartialsApi from "../PartialsApi.js";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
name: "PartialActions",
|
|
12
|
+
props: {
|
|
13
|
+
item: Object
|
|
14
|
+
},
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
STAGE_PUBLISHED: PartialsApi.STAGE_PUBLISHED,
|
|
18
|
+
partialsApi: new PartialsApi()
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
methods: {
|
|
22
|
+
async publish() {
|
|
23
|
+
let item = await this.partialsApi.publish(this.item.id);
|
|
24
|
+
this.$emit('update', item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="full-height-flex scroll-container">
|
|
3
|
+
|
|
4
|
+
<b-loading v-model="paginatedItems.loading" :is-full-page="false" :can-cancel="false"></b-loading>
|
|
5
|
+
|
|
6
|
+
<h2 v-if="!paginatedItems.loading && paginatedItems.items.length === 0" class="subtitle">
|
|
7
|
+
No items found.
|
|
8
|
+
</h2>
|
|
9
|
+
|
|
10
|
+
<div
|
|
11
|
+
v-for="item in paginatedItems.items"
|
|
12
|
+
:key="item.id"
|
|
13
|
+
class="card"
|
|
14
|
+
@click="$emit('choose', item)">
|
|
15
|
+
<span>{{ item.title }} ( <code>{{ item.key }}</code> )</span>
|
|
16
|
+
<ContentEditor :editable="false" :content="item.richContent" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import PartialsApi from "../PartialsApi.js";
|
|
23
|
+
import ContentEditor from "./content/ContentEditor.vue";
|
|
24
|
+
|
|
25
|
+
export default {
|
|
26
|
+
name: "PartialList",
|
|
27
|
+
components: {ContentEditor},
|
|
28
|
+
props: {
|
|
29
|
+
inTrash: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: false
|
|
32
|
+
},
|
|
33
|
+
searchQuery: {
|
|
34
|
+
type: String,
|
|
35
|
+
required: true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
data() {
|
|
39
|
+
return {
|
|
40
|
+
partialsApi: new PartialsApi(),
|
|
41
|
+
paginatedItems: {items: [], totalItems: 0, itemsPerPage: 0, loading: false, currentPage: 1},
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
created() {
|
|
45
|
+
this.fetchData()
|
|
46
|
+
},
|
|
47
|
+
methods: {
|
|
48
|
+
async fetchData() {
|
|
49
|
+
if(this.paginatedItems.loading) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.paginatedItems.loading = true;
|
|
53
|
+
this.paginatedItems.items = [];
|
|
54
|
+
|
|
55
|
+
let data = await this.partialsApi.list({ inTrash: this.inTrash, page: this.paginatedItems.currentPage, q: this.searchQuery });
|
|
56
|
+
|
|
57
|
+
this.paginatedItems.items = data.items;
|
|
58
|
+
this.paginatedItems.loading = false;
|
|
59
|
+
this.paginatedItems.itemsPerPage = data.itemsPerPage;
|
|
60
|
+
this.paginatedItems.totalItems = data.totalItems;
|
|
61
|
+
|
|
62
|
+
console.log(data);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
@import "./util.css";
|
|
70
|
+
|
|
71
|
+
.card {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-icon :icon="statusIcon"></b-icon>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import PartialsApi from "../PartialsApi.js";
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: "PartialStatusIcon",
|
|
10
|
+
props: {
|
|
11
|
+
item: Object
|
|
12
|
+
},
|
|
13
|
+
computed: {
|
|
14
|
+
statusIcon() {
|
|
15
|
+
if(this.item.stage === PartialsApi.STAGE_DRAFT) {
|
|
16
|
+
return 'pen-square';
|
|
17
|
+
} else if(this.item.stage === PartialsApi.STAGE_PUBLISHED) {
|
|
18
|
+
return 'globe-asia';
|
|
19
|
+
} else {
|
|
20
|
+
throw new Error('unknown stage ' + this.item.stage);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style scoped>
|
|
28
|
+
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-table
|
|
3
|
+
ref="table"
|
|
4
|
+
:data="(paginatedItems === null || paginatedItems.items === null) ? [] : paginatedItems.items"
|
|
5
|
+
:loading="paginatedItems === null || paginatedItems.items === null || paginatedItems.loading"
|
|
6
|
+
custom-row-key="id"
|
|
7
|
+
:paginated="paginatedItems !== null && (paginatedItems.totalItems > paginatedItems.itemsPerPage)"
|
|
8
|
+
backend-pagination
|
|
9
|
+
:total="paginatedItems !== null ? paginatedItems.totalItems : 0"
|
|
10
|
+
:per-page="paginatedItems !== null ? paginatedItems.itemsPerPage : 0"
|
|
11
|
+
:current-page="paginatedItems !== null ? paginatedItems.currentPage : 1"
|
|
12
|
+
:detailed="false"
|
|
13
|
+
:backend-sorting="!!onSort"
|
|
14
|
+
default-sort-direction="asc"
|
|
15
|
+
:default-sort="[sortField, sortOrder]"
|
|
16
|
+
sticky-header
|
|
17
|
+
aria-next-label="Next page"
|
|
18
|
+
aria-previous-label="Previous page"
|
|
19
|
+
aria-page-label="Page"
|
|
20
|
+
aria-current-label="Current page"
|
|
21
|
+
class="full-height-flex full-height-container"
|
|
22
|
+
@page-change="onPageChange"
|
|
23
|
+
@sort="onSort">
|
|
24
|
+
<b-table-column v-slot="props" label="Title" :sortable="!!onSort" field="title">{{ props.row.title }} <PartialStatusIcon :item="props.row"></PartialStatusIcon></b-table-column>
|
|
25
|
+
<b-table-column v-slot="props" label="Key" :sortable="!!onSort" field="key">{{ props.row.key }}</b-table-column>
|
|
26
|
+
<b-table-column v-slot="props" label="Last Updated" field="updatedAt" :sortable="!!onSort">
|
|
27
|
+
<div v-if="props.row.updatedAt" class="is-size-7"><Updated :model="props.row"></Updated></div>
|
|
28
|
+
</b-table-column>
|
|
29
|
+
<b-table-column v-slot="props">
|
|
30
|
+
<slot name="actions" :row="props.row"></slot>
|
|
31
|
+
</b-table-column>
|
|
32
|
+
</b-table>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import Updated from "./Updated.vue";
|
|
37
|
+
import PartialStatusIcon from "./PartialStatusIcon.vue";
|
|
38
|
+
|
|
39
|
+
export default {
|
|
40
|
+
name: "PartialTable",
|
|
41
|
+
components: {PartialStatusIcon, Updated},
|
|
42
|
+
props: {
|
|
43
|
+
detailed: Boolean,
|
|
44
|
+
sortField: String,
|
|
45
|
+
sortOrder: String,
|
|
46
|
+
onSort: { type: Function, default: () => {} },
|
|
47
|
+
paginatedItems: { type: Object, default: null },
|
|
48
|
+
onPageChange: Function,
|
|
49
|
+
},
|
|
50
|
+
data() {
|
|
51
|
+
return {
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
watch: {
|
|
55
|
+
},
|
|
56
|
+
methods: {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<style scoped lang="scss">
|
|
62
|
+
::v-deep .table-wrapper.has-sticky-header {
|
|
63
|
+
flex: 1 1 auto;
|
|
64
|
+
}
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="full-height full-height-container pad">
|
|
3
|
+
<div class="top-bar">
|
|
4
|
+
<h1 v-if="!inTrash" class="title">{{ displayName }}</h1>
|
|
5
|
+
<b-button v-if="inTrash" tag="router-link" :to="'/' + routePrefix + '/'" outlined rounded icon-left="arrow-left">All {{ displayName }}</b-button>
|
|
6
|
+
<h1 v-if="inTrash" class="title">
|
|
7
|
+
Deleted {{ displayName }}
|
|
8
|
+
</h1>
|
|
9
|
+
<div class="is-flex-grow-1">
|
|
10
|
+
</div>
|
|
11
|
+
<b-input v-model.lazy="searchQuery" rounded :placeholder="'Search ' + displayName" icon="search" icon-pack="fas" class="mr-3"></b-input>
|
|
12
|
+
<b-button v-if="!inTrash" tag="router-link" :to="'/' + routePrefix + '/create'" type="is-success" icon-left="pencil-alt" class="mr-3">Create
|
|
13
|
+
{{ singularDisplayName }}</b-button>
|
|
14
|
+
<b-button v-if="!inTrash" tag="router-link" :to="'/' + routePrefix + '/trash'" type="is-danger" outlined icon-left="trash">Deleted
|
|
15
|
+
{{ displayName }}</b-button>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="full-height full-height-container box">
|
|
19
|
+
<component :is="tableComponent" :paginated-items="paginatedItems" :on-page-change="page => paginatedItems.currentPage = page" :detailed="!searchQuery" :on-sort="onSort">
|
|
20
|
+
<template #actions="slotProps">
|
|
21
|
+
<div class="buttons" style="min-width: 18rem">
|
|
22
|
+
<component :is="actionsComponent" :item="slotProps.row" @update="updateItem"></component>
|
|
23
|
+
<b-button rounded icon-left="pencil-alt" tag="router-link" :to="'/' + routePrefix + '/' + slotProps.row.id" size="is-small">Edit</b-button>
|
|
24
|
+
<b-button
|
|
25
|
+
v-if="inTrash" rounded outlined icon-left="recycle"
|
|
26
|
+
size="is-small" @click="restoreItem(slotProps.row.id)">Restore
|
|
27
|
+
</b-button>
|
|
28
|
+
<b-button
|
|
29
|
+
v-if="inTrash" rounded type="is-danger" outlined icon-left="trash"
|
|
30
|
+
size="is-small" @click="forceDeleteItem(slotProps.row.id)">Delete Forever
|
|
31
|
+
</b-button>
|
|
32
|
+
<b-button
|
|
33
|
+
v-if="!inTrash" rounded icon-left="trash"
|
|
34
|
+
size="is-small" @click="deleteItem(slotProps.row.id)">Delete</b-button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
</component>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
import ContentEditor from "./content/ContentEditor.vue";
|
|
44
|
+
import {debounce} from "lodash";
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
name: "ResourceList",
|
|
48
|
+
components: {ContentEditor},
|
|
49
|
+
props: {
|
|
50
|
+
routePrefix: String,
|
|
51
|
+
displayName: String,
|
|
52
|
+
singularDisplayName: String,
|
|
53
|
+
inTrash: {
|
|
54
|
+
type: Boolean,
|
|
55
|
+
default: false
|
|
56
|
+
},
|
|
57
|
+
defaultSortField: String,
|
|
58
|
+
defaultSortOrder: String,
|
|
59
|
+
tableComponent: Object,
|
|
60
|
+
actionsComponent: Object,
|
|
61
|
+
resourceApi: Object
|
|
62
|
+
},
|
|
63
|
+
data() {
|
|
64
|
+
return {
|
|
65
|
+
searchQuery: '',
|
|
66
|
+
sortField: this.defaultSortField,
|
|
67
|
+
sortOrder: this.defaultSortOrder,
|
|
68
|
+
paginatedItems: {items: [], totalItems: 0, itemsPerPage: 0, loading: false, currentPage: 1},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
watch: {
|
|
72
|
+
'resourceApi': 'fetchData',
|
|
73
|
+
'inTrash': 'fetchData',
|
|
74
|
+
'sortField': 'fetchData',
|
|
75
|
+
'sortOrder': 'fetchData',
|
|
76
|
+
'searchQuery': 'debouncedFetchData',
|
|
77
|
+
'paginatedItems.currentPage': 'fetchData'
|
|
78
|
+
},
|
|
79
|
+
created() {
|
|
80
|
+
this.fetchData()
|
|
81
|
+
},
|
|
82
|
+
methods: {
|
|
83
|
+
debouncedFetchData: debounce(async function() {
|
|
84
|
+
await this.fetchData()
|
|
85
|
+
}, 1000),
|
|
86
|
+
async fetchData() {
|
|
87
|
+
if(this.paginatedItems.loading) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.paginatedItems.loading = true;
|
|
91
|
+
this.paginatedItems.items = [];
|
|
92
|
+
|
|
93
|
+
let data = await this.resourceApi.list({
|
|
94
|
+
q: this.searchQuery,
|
|
95
|
+
inTrash: this.inTrash,
|
|
96
|
+
page: this.paginatedItems.currentPage,
|
|
97
|
+
sortField: this.sortField,
|
|
98
|
+
sortOrder: this.sortOrder
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
this.paginatedItems.items = data.items;
|
|
102
|
+
this.paginatedItems.loading = false;
|
|
103
|
+
this.paginatedItems.itemsPerPage = data.itemsPerPage;
|
|
104
|
+
this.paginatedItems.totalItems = data.totalItems;
|
|
105
|
+
},
|
|
106
|
+
// updates a single item
|
|
107
|
+
updateItem(item) {
|
|
108
|
+
this.paginatedItems.items = this.paginatedItems.items.map(i => { return i.id === item.id ? item : i; });
|
|
109
|
+
},
|
|
110
|
+
onSort(field, order) {
|
|
111
|
+
this.sortField = field;
|
|
112
|
+
this.sortOrder = order;
|
|
113
|
+
},
|
|
114
|
+
async restoreItem(id) {
|
|
115
|
+
await this.resourceApi.restoreAndNotify(id);
|
|
116
|
+
await this.fetchData();
|
|
117
|
+
},
|
|
118
|
+
async forceDeleteItem(id) {
|
|
119
|
+
await this.resourceApi.confirmForceDelete(id);
|
|
120
|
+
await this.fetchData();
|
|
121
|
+
},
|
|
122
|
+
async deleteItem(id) {
|
|
123
|
+
await this.resourceApi.deleteAndNotify(id);
|
|
124
|
+
await this.fetchData();
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<style scoped>
|
|
131
|
+
@import "./util.css";
|
|
132
|
+
</style>
|
|
@@ -100,7 +100,7 @@ export default {
|
|
|
100
100
|
EditButtonOnRowHover, GroupsChooser, GenericEditableField, UserJoined, GroupsList},
|
|
101
101
|
data() {
|
|
102
102
|
return {
|
|
103
|
-
usersApi: new UsersApi(
|
|
103
|
+
usersApi: new UsersApi(),
|
|
104
104
|
selectedUser: null,
|
|
105
105
|
searchQuery: null,
|
|
106
106
|
isCreateUserModalActive: false,
|
|
@@ -116,7 +116,7 @@ export default {
|
|
|
116
116
|
methods: {
|
|
117
117
|
async fetchData() {
|
|
118
118
|
this.paginatedItems.loading = true;
|
|
119
|
-
let data = await this.usersApi.list(false, this.paginatedItems.currentPage, this.searchQuery);
|
|
119
|
+
let data = await this.usersApi.list({ inTrash: false, page: this.paginatedItems.currentPage, q: this.searchQuery });
|
|
120
120
|
|
|
121
121
|
this.paginatedItems.items = data.items;
|
|
122
122
|
this.paginatedItems.totalItems = data.totalItems;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<b-dropdown ref="dropdown" v-model="selectedIndex" :inline="true" :scrollable="true" class="items" :style="{ top: (top + 30) + 'px', left: (left + 4) + 'px', display: !visible ? 'none !important' : 'inherit' }" @change="index => selectItem(items[index])">
|
|
3
|
+
<b-dropdown-item
|
|
4
|
+
v-for="(item, index) in items"
|
|
5
|
+
:key="index"
|
|
6
|
+
:value="index"
|
|
7
|
+
@click="selectItem(item)"
|
|
8
|
+
>
|
|
9
|
+
<b-icon :icon="item.icon"></b-icon>
|
|
10
|
+
{{ item.title }}
|
|
11
|
+
</b-dropdown-item>
|
|
12
|
+
<!-- </b-dropdown-item>-->
|
|
13
|
+
<b-dropdown-item v-if="items.length === 0">
|
|
14
|
+
No result
|
|
15
|
+
</b-dropdown-item>
|
|
16
|
+
</b-dropdown>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script>
|
|
20
|
+
export default {
|
|
21
|
+
props: {
|
|
22
|
+
items: {
|
|
23
|
+
type: Array,
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
command: {
|
|
27
|
+
type: Function,
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
top: {
|
|
31
|
+
type: Number,
|
|
32
|
+
required: true
|
|
33
|
+
},
|
|
34
|
+
left: {
|
|
35
|
+
type: Number,
|
|
36
|
+
required: true
|
|
37
|
+
},
|
|
38
|
+
visible: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
required: true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
data() {
|
|
45
|
+
return {
|
|
46
|
+
selectedIndex: 0,
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
watch: {
|
|
51
|
+
items() {
|
|
52
|
+
this.selectedIndex = 0
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
methods: {
|
|
57
|
+
onKeyDown({ event }) {
|
|
58
|
+
if (event.key === 'ArrowUp') {
|
|
59
|
+
this.upHandler()
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (event.key === 'ArrowDown') {
|
|
64
|
+
this.downHandler()
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (event.key === 'Enter') {
|
|
69
|
+
this.enterHandler()
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false
|
|
74
|
+
},
|
|
75
|
+
upHandler() {
|
|
76
|
+
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length;
|
|
77
|
+
let el = this.$refs.dropdown.$children[this.selectedIndex].$el;
|
|
78
|
+
el.scrollIntoView({
|
|
79
|
+
block: 'nearest'
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
downHandler() {
|
|
83
|
+
this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
|
|
84
|
+
let el = this.$refs.dropdown.$children[this.selectedIndex].$el;
|
|
85
|
+
el.scrollIntoView({
|
|
86
|
+
block: 'nearest'
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
enterHandler() {
|
|
90
|
+
const item = this.items[this.selectedIndex];
|
|
91
|
+
this.selectItem(item)
|
|
92
|
+
},
|
|
93
|
+
selectItem(item) {
|
|
94
|
+
if (item) {
|
|
95
|
+
this.command(item)
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<style lang="scss">
|
|
103
|
+
.items {
|
|
104
|
+
left: 10;
|
|
105
|
+
position: absolute;
|
|
106
|
+
z-index: 9999;
|
|
107
|
+
}
|
|
108
|
+
</style>
|