Skip to content

Commit fa0e5bc

Browse files
committed
Add a content crud view in CMS
1 parent d5866e3 commit fa0e5bc

4 files changed

Lines changed: 503 additions & 10 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const flattenSubtree = (contentModel) => {
2+
const nodes = []
3+
const subtree = contentModel.subtree || contentModel
4+
5+
for (const collection of (subtree.collections || [])) {
6+
nodes.push({
7+
type: 'collection',
8+
name: collection.title,
9+
data: collection,
10+
children: buildCollectionChildren(collection)
11+
})
12+
}
13+
14+
if (subtree.homepage) {
15+
nodes.push({
16+
type: 'homepage',
17+
name: subtree.homepage.title || 'Homepage',
18+
data: subtree.homepage
19+
})
20+
}
21+
22+
for (const subpage of (subtree.subpages || [])) {
23+
nodes.push({
24+
type: 'subpage',
25+
name: subpage.title,
26+
data: subpage
27+
})
28+
}
29+
30+
return nodes
31+
}
32+
33+
const buildCollectionChildren = (collection) => {
34+
const nodes = []
35+
const subtree = collection.subtree || collection
36+
37+
const categories = (subtree.categories || [])
38+
.filter(category => !category.isDefaultCategory)
39+
for (const category of categories) {
40+
nodes.push({
41+
type: 'category',
42+
name: category.title,
43+
data: category,
44+
children: buildCategoryChildren(category)
45+
})
46+
}
47+
48+
const posts = subtree.levelPosts || subtree.posts || []
49+
for (const post of posts) {
50+
nodes.push({
51+
type: 'entry',
52+
name: post.title,
53+
data: post
54+
})
55+
}
56+
57+
return nodes
58+
}
59+
60+
const buildCategoryChildren = (category) => {
61+
const nodes = []
62+
const subtree = category.subtree || category
63+
64+
const subcategories = (subtree.categories || [])
65+
.filter(subcategory => !subcategory.isDefaultCategory)
66+
for (const subcategory of subcategories) {
67+
nodes.push({
68+
type: 'category',
69+
name: subcategory.title,
70+
data: subcategory,
71+
children: buildCategoryChildren(subcategory)
72+
})
73+
}
74+
75+
const posts = subtree.levelPosts || subtree.posts || []
76+
for (const post of posts) {
77+
nodes.push({
78+
type: 'entry',
79+
name: post.title,
80+
data: post
81+
})
82+
}
83+
84+
return nodes
85+
}
86+
87+
export { flattenSubtree }
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { createDOMNodeFromHTML } from '../../common.js'
2+
import api from '../../../api.js'
3+
import dialog from '../dialog.js'
4+
import defaultContentTypes from '../../defaultContentTypes.js'
5+
import { flattenSubtree } from './contentTree.js'
6+
import { template } from './template.js'
7+
8+
const INITIAL_CONTENT_TYPES_SHOWN = 5
9+
10+
const state = {
11+
tree: [],
12+
path: [],
13+
showAllTypes: false
14+
}
15+
16+
const resolveNodes = () => {
17+
let nodes = state.tree
18+
for (const step of state.path) {
19+
nodes = nodes[step.index].children || []
20+
}
21+
return nodes
22+
}
23+
24+
const getBreadcrumb = () => {
25+
return ['Content', ...state.path.map(step => step.name)]
26+
.join(' › ')
27+
}
28+
29+
const getVisibleContentTypes = () => {
30+
if (state.showAllTypes) {
31+
return defaultContentTypes
32+
}
33+
const entryTypes = defaultContentTypes.filter(
34+
contentType => contentType.model === 'entry'
35+
)
36+
return entryTypes.slice(0, INITIAL_CONTENT_TYPES_SHOWN)
37+
}
38+
39+
const addListeners = ($panel, nodes) => {
40+
const $back = $panel.querySelector('.content-panel-back-btn')
41+
if ($back) {
42+
$back.addEventListener('click', () => {
43+
state.path.pop()
44+
update()
45+
})
46+
}
47+
48+
$panel.querySelectorAll('[data-drill]').forEach($el => {
49+
$el.addEventListener('click', () => {
50+
const i = parseInt($el.dataset.drill, 10)
51+
state.path.push({ index: i, name: nodes[i].name })
52+
update()
53+
})
54+
})
55+
56+
$panel.querySelectorAll('[data-edit]').forEach($btn => {
57+
$btn.addEventListener('click', () => {
58+
const i = parseInt($btn.dataset.edit, 10)
59+
const node = nodes[i]
60+
console.log(
61+
'Route to edit:', node.type, node.name,
62+
node.data
63+
)
64+
})
65+
})
66+
67+
$panel.querySelectorAll('[data-delete]').forEach($btn => {
68+
$btn.addEventListener('click', () => {
69+
const i = parseInt($btn.dataset.delete, 10)
70+
const node = nodes[i]
71+
console.log(
72+
'Route to delete:', node.type, node.name,
73+
node.data
74+
)
75+
})
76+
})
77+
78+
$panel.querySelectorAll('[data-create-type]').forEach($btn => {
79+
$btn.addEventListener('click', () => {
80+
console.log(
81+
'Route to create:',
82+
$btn.dataset.createType,
83+
'at', getBreadcrumb()
84+
)
85+
})
86+
})
87+
88+
const $expand = $panel.querySelector('.content-panel-expand-btn')
89+
if ($expand) {
90+
$expand.addEventListener('click', () => {
91+
state.showAllTypes = true
92+
update()
93+
})
94+
}
95+
}
96+
97+
const render = async () => {
98+
dialog.textContent('Loading…').show()
99+
100+
const contentModel = await api.contentModel.get()
101+
state.tree = flattenSubtree(contentModel)
102+
state.path = []
103+
state.showAllTypes = false
104+
105+
update()
106+
}
107+
108+
const update = () => {
109+
const nodes = resolveNodes()
110+
const visibleContentTypes = getVisibleContentTypes()
111+
const html = template({
112+
breadcrumb: getBreadcrumb(),
113+
nodes,
114+
showBack: state.path.length > 0,
115+
visibleContentTypes,
116+
canExpand: visibleContentTypes.length <
117+
defaultContentTypes.length
118+
})
119+
const $panel = createDOMNodeFromHTML(html)
120+
addListeners($panel, nodes)
121+
dialog.html('')
122+
dialog.appendChild($panel)
123+
}
124+
125+
export default { render }

0 commit comments

Comments
 (0)