@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.10
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/api/api-key.ts +73 -0
- package/api/backup.ts +38 -0
- package/api/block.ts +96 -30
- package/api/index.ts +12 -1
- package/api/mcp.ts +53 -0
- package/components/AdminPanel.ts +74 -0
- package/components/Backup.ts +10 -0
- package/components/BlockFieldRenderer.ts +121 -0
- package/components/BlockRenderer.ts +22 -0
- package/components/Entries.ts +19 -11
- package/components/Entry.ts +13 -21
- package/components/FieldRenderer.ts +35 -0
- package/components/Render.ts +46 -0
- package/components/Settings.ts +101 -0
- package/components/{layout.ts → SiteLayout.ts} +22 -17
- package/components/fields/Markdown.ts +44 -0
- package/components/fields/Text.ts +42 -0
- package/components/fields/index.ts +4 -0
- package/components/icons.ts +100 -7
- package/index.ts +66 -34
- package/package.json +8 -8
- package/pages/entry/[id].ts +17 -0
- package/{components → pages}/index.ts +7 -4
- package/pages/settings.ts +10 -0
- package/public/studio/css/admin-panel.css +103 -0
- package/public/studio/css/blocks-field.css +53 -0
- package/public/studio/css/settings.css +24 -0
- package/public/studio/js/markdown-editor.js +34 -0
- package/public/studio/js/sortable-list.js +50 -0
- package/public/studio/main.css +161 -0
- package/public/studio/main.js +9 -0
- package/queries/block-with-children.ts +74 -0
- package/queries/block.ts +289 -0
- package/queries/db-types.ts +15 -0
- package/queries/getBlockTrees-2.ts +0 -0
- package/queries/getBlockTrees.ts +316 -0
- package/queries/getBlocks.ts +214 -0
- package/queries/index.ts +2 -98
- package/queries/structure-types.ts +97 -0
- package/readme.md +205 -0
- package/schemas.ts +15 -54
- package/types.ts +133 -5
- package/utils/buildBlocksTree.ts +4 -4
- package/utils/define.ts +41 -0
- package/utils/file-based-router.ts +11 -2
- package/utils/get-config.ts +8 -9
- package/utils/get-or-create-row.ts +41 -0
- package/utils/html.ts +247 -0
- package/utils/startup-log.ts +10 -0
- package/components/AdminPanel/AdminPanel.css +0 -59
- package/components/AdminPanel/AdminPanel.ts +0 -57
- package/components/Block.ts +0 -116
- package/components/Entry.css +0 -7
- package/components/Field.ts +0 -164
- package/components/Fields.ts +0 -43
- package/public/main.css +0 -92
- package/public/main.js +0 -43
- /package/public/{favicon.svg → studio/favicon.svg} +0 -0
package/api/api-key.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
|
+
import { Hono } from 'hono'
|
|
4
|
+
import { streamSSE } from 'hono/streaming'
|
|
5
|
+
import { db } from '@alstar/db'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
9
|
+
import { sql } from '../utils/sql.ts'
|
|
10
|
+
import Settings from '../components/Settings.ts'
|
|
11
|
+
|
|
12
|
+
export default () => {
|
|
13
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
14
|
+
|
|
15
|
+
app.post('/api-key', async (c) => {
|
|
16
|
+
return streamSSE(c, async (stream) => {
|
|
17
|
+
const formData = await c.req.formData()
|
|
18
|
+
const data = Object.fromEntries(formData.entries())
|
|
19
|
+
|
|
20
|
+
if (!data) return
|
|
21
|
+
|
|
22
|
+
const apiKey = crypto.randomUUID()
|
|
23
|
+
const hash = crypto.createHash('sha256')
|
|
24
|
+
|
|
25
|
+
hash.update(apiKey)
|
|
26
|
+
|
|
27
|
+
const digest = hash.digest().toString('base64')
|
|
28
|
+
|
|
29
|
+
const xs = (length: number) => '*'.repeat(length)
|
|
30
|
+
|
|
31
|
+
db.insertInto('api_keys', {
|
|
32
|
+
name: data.name?.toString(),
|
|
33
|
+
value: digest,
|
|
34
|
+
hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await stream.writeSSE({
|
|
38
|
+
event: 'datastar-patch-signals',
|
|
39
|
+
data: `signals { apiKey: '${apiKey}', name: '' }`,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await stream.writeSSE({
|
|
43
|
+
event: 'datastar-patch-elements',
|
|
44
|
+
data: `elements ${stripNewlines(Settings())}`,
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
app.delete('/api-key', async (c) => {
|
|
50
|
+
return streamSSE(c, async (stream) => {
|
|
51
|
+
const formData = await c.req.formData()
|
|
52
|
+
|
|
53
|
+
const value = formData.get('value')?.toString()
|
|
54
|
+
|
|
55
|
+
if (!value) return
|
|
56
|
+
|
|
57
|
+
db.database
|
|
58
|
+
.prepare(sql`
|
|
59
|
+
delete from api_keys
|
|
60
|
+
where
|
|
61
|
+
value = ?
|
|
62
|
+
`)
|
|
63
|
+
.run(value)
|
|
64
|
+
|
|
65
|
+
await stream.writeSSE({
|
|
66
|
+
event: 'datastar-patch-elements',
|
|
67
|
+
data: `elements ${stripNewlines(Settings())}`,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return app
|
|
73
|
+
}
|
package/api/backup.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { streamSSE } from 'hono/streaming'
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
5
|
+
|
|
6
|
+
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
7
|
+
import { db } from '@alstar/db'
|
|
8
|
+
|
|
9
|
+
export default () => {
|
|
10
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
11
|
+
|
|
12
|
+
app.post('/backup', async (c) => {
|
|
13
|
+
// const totalPagesTransferred = await backup(db.database, './backups/backup.db', {
|
|
14
|
+
// rate: 1, // Copy one page at a time.
|
|
15
|
+
// progress: ({ totalPages, remainingPages }) => {
|
|
16
|
+
// console.log('Backup in progress', { totalPages, remainingPages })
|
|
17
|
+
// },
|
|
18
|
+
// })
|
|
19
|
+
|
|
20
|
+
// console.log('Backup completed', totalPagesTransferred)
|
|
21
|
+
|
|
22
|
+
return c.html('good')
|
|
23
|
+
|
|
24
|
+
// return streamSSE(c, async (stream) => {
|
|
25
|
+
// await stream.writeSSE({
|
|
26
|
+
// event: 'datastar-patch-signals',
|
|
27
|
+
// data: `signals {}`,
|
|
28
|
+
// })
|
|
29
|
+
|
|
30
|
+
// await stream.writeSSE({
|
|
31
|
+
// event: 'datastar-patch-elements',
|
|
32
|
+
// data: `elements ${stripNewlines(Settings())}`,
|
|
33
|
+
// })
|
|
34
|
+
// })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return app
|
|
38
|
+
}
|
package/api/block.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
-
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { streamSSE } from 'hono/streaming'
|
|
5
5
|
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
6
6
|
import { sql } from '../utils/sql.ts'
|
|
7
7
|
import { type Structure } from '../types.ts'
|
|
8
8
|
import { db } from '@alstar/db'
|
|
9
|
-
import Entries from '../components/Entries.ts'
|
|
10
9
|
import Entry from '../components/Entry.ts'
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
import {
|
|
11
|
+
blockWithChildren,
|
|
12
|
+
deleteBlockWithChildren,
|
|
13
|
+
} from '../queries/block-with-children.ts'
|
|
14
|
+
import AdminPanel from '../components/AdminPanel.ts'
|
|
15
|
+
import { query } from '../queries/index.ts'
|
|
16
|
+
|
|
17
|
+
export default (structure: Structure) => {
|
|
13
18
|
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
14
19
|
|
|
15
20
|
app.post('/block', async (c) => {
|
|
@@ -17,19 +22,19 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
17
22
|
const formData = await c.req.formData()
|
|
18
23
|
const data = Object.fromEntries(formData.entries())
|
|
19
24
|
|
|
20
|
-
const row = structure.
|
|
25
|
+
const row = structure[data.name?.toString()]
|
|
21
26
|
|
|
22
27
|
if (!row) return
|
|
23
28
|
|
|
24
29
|
db.insertInto('blocks', {
|
|
25
|
-
name:
|
|
26
|
-
label: row.label
|
|
27
|
-
type: row.type
|
|
30
|
+
name: data.name?.toString(),
|
|
31
|
+
label: row.label,
|
|
32
|
+
type: row.type,
|
|
28
33
|
})
|
|
29
34
|
|
|
30
35
|
await stream.writeSSE({
|
|
31
36
|
event: 'datastar-patch-elements',
|
|
32
|
-
data: `elements ${stripNewlines(
|
|
37
|
+
data: `elements ${stripNewlines(AdminPanel())}`,
|
|
33
38
|
})
|
|
34
39
|
})
|
|
35
40
|
})
|
|
@@ -37,26 +42,19 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
37
42
|
app.post('/new-block', async (c) => {
|
|
38
43
|
return streamSSE(c, async (stream) => {
|
|
39
44
|
const formData = await c.req.formData()
|
|
40
|
-
const
|
|
41
|
-
const columns = newBlock?.map((field) => field.split(':')) as string[][]
|
|
42
|
-
const data = Object.fromEntries(columns)
|
|
43
|
-
|
|
44
|
-
const parent_block_id = formData?.get('parent_block_id')?.toString()
|
|
45
|
-
const sort_order = formData?.get('sort_order')?.toString()
|
|
46
|
-
|
|
47
|
-
if (!parent_block_id || !sort_order) return
|
|
45
|
+
const data = Object.fromEntries(formData)
|
|
48
46
|
|
|
49
47
|
db.insertInto('blocks', {
|
|
50
|
-
type: data.type,
|
|
51
|
-
name: data.name,
|
|
52
|
-
label: data.label,
|
|
53
|
-
|
|
54
|
-
sort_order,
|
|
48
|
+
type: data.type.toString(),
|
|
49
|
+
name: data.name.toString(),
|
|
50
|
+
label: data.label.toString(),
|
|
51
|
+
parent_id: data.parent_id.toString(),
|
|
52
|
+
sort_order: data.sort_order.toString(),
|
|
55
53
|
})
|
|
56
54
|
|
|
57
55
|
await stream.writeSSE({
|
|
58
56
|
event: 'datastar-patch-elements',
|
|
59
|
-
data: `elements ${stripNewlines(Entry({ entryId: data.entry_id
|
|
57
|
+
data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
|
|
60
58
|
})
|
|
61
59
|
})
|
|
62
60
|
})
|
|
@@ -67,31 +65,99 @@ export const sectionRoutes = (structure: Structure) => {
|
|
|
67
65
|
|
|
68
66
|
const id = formData.get('id')?.toString()
|
|
69
67
|
const value = formData.get('value')?.toString()
|
|
68
|
+
const name = formData.get('name')?.toString()
|
|
70
69
|
const entryId = formData.get('entryId')?.toString()
|
|
71
70
|
const parentId = formData.get('parentId')?.toString()
|
|
72
|
-
const sortOrder = formData.get('sort_order')?.toString()
|
|
71
|
+
// const sortOrder = formData.get('sort_order')?.toString()
|
|
73
72
|
|
|
74
|
-
if (!id || !value
|
|
73
|
+
if (!id || !value) return
|
|
75
74
|
|
|
76
75
|
const transaction = db.database.prepare(sql`
|
|
77
76
|
update blocks
|
|
78
77
|
set
|
|
79
78
|
value = ?
|
|
80
79
|
where
|
|
81
|
-
id =
|
|
82
|
-
and sort_order = ?;
|
|
80
|
+
id = ?;
|
|
83
81
|
`)
|
|
84
82
|
|
|
85
|
-
transaction.run(value, id
|
|
83
|
+
transaction.run(value, id)
|
|
84
|
+
|
|
85
|
+
if (entryId === parentId && name?.toString() === 'title') {
|
|
86
|
+
const rootBlock = query.block({
|
|
87
|
+
id: parentId?.toString() || null,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (rootBlock.type !== 'single') {
|
|
91
|
+
await stream.writeSSE({
|
|
92
|
+
event: 'datastar-patch-elements',
|
|
93
|
+
data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
app.patch('/value', async (c) => {
|
|
101
|
+
const body = await c.req.json()
|
|
102
|
+
|
|
103
|
+
const transaction = db.database.prepare(sql`
|
|
104
|
+
update blocks
|
|
105
|
+
set
|
|
106
|
+
value = ?
|
|
107
|
+
where
|
|
108
|
+
id = ?;
|
|
109
|
+
`)
|
|
110
|
+
|
|
111
|
+
transaction.run(body.value, body.id)
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
return c.json({ status: 200, message: 'success' })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
app.delete('/block', async (c) => {
|
|
117
|
+
return streamSSE(c, async (stream) => {
|
|
118
|
+
const formData = await c.req.formData()
|
|
119
|
+
|
|
120
|
+
const id = formData.get('id')?.toString()
|
|
121
|
+
const entryId = formData.get('entry_id')?.toString()
|
|
122
|
+
|
|
123
|
+
if (!id) return
|
|
124
|
+
|
|
125
|
+
const transaction = db.database.prepare(deleteBlockWithChildren)
|
|
126
|
+
|
|
127
|
+
transaction.all(id)
|
|
128
|
+
|
|
129
|
+
if (entryId) {
|
|
88
130
|
await stream.writeSSE({
|
|
89
131
|
event: 'datastar-patch-elements',
|
|
90
|
-
data: `elements
|
|
132
|
+
data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
|
|
133
|
+
})
|
|
134
|
+
} else {
|
|
135
|
+
await stream.writeSSE({
|
|
136
|
+
event: 'datastar-patch-elements',
|
|
137
|
+
data: `elements ${stripNewlines(AdminPanel())}`,
|
|
91
138
|
})
|
|
92
139
|
}
|
|
93
140
|
})
|
|
94
141
|
})
|
|
95
142
|
|
|
143
|
+
app.post('/sort-order', async (c) => {
|
|
144
|
+
const id = c.req.query('id')
|
|
145
|
+
const sortOrder = c.req.query('sort-order')
|
|
146
|
+
|
|
147
|
+
if (!id || !sortOrder) return
|
|
148
|
+
|
|
149
|
+
const transaction = db.database.prepare(sql`
|
|
150
|
+
update blocks
|
|
151
|
+
set
|
|
152
|
+
sort_order = ?
|
|
153
|
+
where
|
|
154
|
+
id = ?
|
|
155
|
+
`)
|
|
156
|
+
|
|
157
|
+
transaction.run(sortOrder, id)
|
|
158
|
+
|
|
159
|
+
return c.json({ status: 200, message: 'success' })
|
|
160
|
+
})
|
|
161
|
+
|
|
96
162
|
return app
|
|
97
163
|
}
|
package/api/index.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import block from './block.ts'
|
|
2
|
+
import apiKey from './api-key.ts'
|
|
3
|
+
import type { Structure } from '../types.ts'
|
|
4
|
+
import backup from './backup.ts'
|
|
2
5
|
|
|
6
|
+
export const api = (structure: Structure) => {
|
|
7
|
+
const app = block(structure)
|
|
8
|
+
|
|
9
|
+
app.route('/', apiKey())
|
|
10
|
+
app.route('/', backup())
|
|
11
|
+
|
|
12
|
+
return app
|
|
13
|
+
}
|
package/api/mcp.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
|
+
import { Hono } from 'hono'
|
|
4
|
+
import { sql } from '../utils/sql.ts'
|
|
5
|
+
import { db } from '@alstar/db'
|
|
6
|
+
import { bearerAuth } from 'hono/bearer-auth'
|
|
7
|
+
import crypto from 'node:crypto'
|
|
8
|
+
|
|
9
|
+
export default () => {
|
|
10
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
11
|
+
|
|
12
|
+
app.use(
|
|
13
|
+
'/*',
|
|
14
|
+
bearerAuth({
|
|
15
|
+
verifyToken: async (token, c) => {
|
|
16
|
+
const hash = crypto.createHash('sha256')
|
|
17
|
+
|
|
18
|
+
hash.update(token)
|
|
19
|
+
|
|
20
|
+
const digest = hash.digest().toString('base64')
|
|
21
|
+
|
|
22
|
+
const exists = db.database
|
|
23
|
+
.prepare(sql`
|
|
24
|
+
select
|
|
25
|
+
value
|
|
26
|
+
from
|
|
27
|
+
api_keys
|
|
28
|
+
where
|
|
29
|
+
value = ?
|
|
30
|
+
`)
|
|
31
|
+
.get(digest)
|
|
32
|
+
|
|
33
|
+
return !!exists
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
app.get('/entry', async (c) => {
|
|
39
|
+
return c.json({
|
|
40
|
+
status: 'success',
|
|
41
|
+
message: 'Response from MCP server!',
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
app.post('/entry', async (c) => {
|
|
46
|
+
return c.json({
|
|
47
|
+
status: 'success',
|
|
48
|
+
message: 'New entry created!',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return app
|
|
53
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import { logo } from './icons.ts'
|
|
3
|
+
import Entries from './Entries.ts'
|
|
4
|
+
import * as icons from './icons.ts'
|
|
5
|
+
import { studioStructure } from '../index.ts'
|
|
6
|
+
import { getOrCreateRow } from '../utils/get-or-create-row.ts'
|
|
7
|
+
|
|
8
|
+
export default () => {
|
|
9
|
+
const entries = Object.entries(studioStructure)
|
|
10
|
+
|
|
11
|
+
return html`
|
|
12
|
+
<div class="admin-panel" id="admin_panel">
|
|
13
|
+
<h1>
|
|
14
|
+
<a href="/admin" aria-label="Go to dashboard"> ${logo} </a>
|
|
15
|
+
</h1>
|
|
16
|
+
|
|
17
|
+
<aside style="width: 100%;">
|
|
18
|
+
${entries.map(([name, block]) => {
|
|
19
|
+
if (block.type === 'single') {
|
|
20
|
+
const data = getOrCreateRow({ parentId: null, name, field: block })
|
|
21
|
+
|
|
22
|
+
return html`
|
|
23
|
+
<section id="entries">
|
|
24
|
+
<ul>
|
|
25
|
+
<li>
|
|
26
|
+
<a
|
|
27
|
+
href="/admin/entry/${data.id}"
|
|
28
|
+
id="block_link_${data.id}"
|
|
29
|
+
>
|
|
30
|
+
${block.label}
|
|
31
|
+
</a>
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
</section>
|
|
35
|
+
`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return html`
|
|
39
|
+
<form
|
|
40
|
+
data-on-submit="@post('/admin/api/block', { contentType: 'form' })"
|
|
41
|
+
style="display: flex; align-items: center; gap: 1rem;"
|
|
42
|
+
>
|
|
43
|
+
<input type="hidden" name="name" value="${name}" />
|
|
44
|
+
<button
|
|
45
|
+
class="ghost"
|
|
46
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
47
|
+
data-tooltip="New ${block.label}"
|
|
48
|
+
data-placement="right"
|
|
49
|
+
>
|
|
50
|
+
${icons.newDocument}
|
|
51
|
+
</button>
|
|
52
|
+
<p style="user-select: none;"><small>${block.label}</small></p>
|
|
53
|
+
</form>
|
|
54
|
+
|
|
55
|
+
${Entries({ name })}
|
|
56
|
+
`
|
|
57
|
+
})}
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<footer>
|
|
61
|
+
<a
|
|
62
|
+
role="button"
|
|
63
|
+
href="/admin/settings"
|
|
64
|
+
class="ghost"
|
|
65
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
66
|
+
data-tooltip="Settings"
|
|
67
|
+
data-placement="right"
|
|
68
|
+
>
|
|
69
|
+
${icons.cog}
|
|
70
|
+
</a>
|
|
71
|
+
</footer>
|
|
72
|
+
</div>
|
|
73
|
+
`
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
export default () => {
|
|
4
|
+
return html`<article>
|
|
5
|
+
<header>Backup</header>
|
|
6
|
+
<form data-on-submit="@post('/admin/api/backup', { contentType: 'form' })">
|
|
7
|
+
<button type="submit">Backup database</button>
|
|
8
|
+
</form>
|
|
9
|
+
</article>`
|
|
10
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { query } from '../queries/index.ts'
|
|
2
|
+
import type { BlocksFieldDefStructure } from '../types.ts'
|
|
3
|
+
import { BlockInstance } from '../utils/define.ts'
|
|
4
|
+
import { getOrCreateRow } from '../utils/get-or-create-row.ts'
|
|
5
|
+
import { html } from '../utils/html.ts'
|
|
6
|
+
import * as icons from './icons.ts'
|
|
7
|
+
import Render from './Render.ts'
|
|
8
|
+
|
|
9
|
+
export default (props: {
|
|
10
|
+
entryId: number
|
|
11
|
+
parentId: number
|
|
12
|
+
name: string
|
|
13
|
+
structure: BlocksFieldDefStructure
|
|
14
|
+
id?: number
|
|
15
|
+
sortOrder?: number
|
|
16
|
+
}) => {
|
|
17
|
+
const { entryId, parentId, name, structure, id, sortOrder = 0 } = props
|
|
18
|
+
|
|
19
|
+
const data = getOrCreateRow({
|
|
20
|
+
parentId,
|
|
21
|
+
name,
|
|
22
|
+
field: structure,
|
|
23
|
+
sortOrder,
|
|
24
|
+
id,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!data) return html`<p>No block</p>`
|
|
28
|
+
|
|
29
|
+
const entries = Object.entries(structure.children)
|
|
30
|
+
|
|
31
|
+
const rows = query.blocks({ parent_id: data.id })
|
|
32
|
+
|
|
33
|
+
return html`
|
|
34
|
+
<div class="blocks-field">
|
|
35
|
+
<header>
|
|
36
|
+
<p>${structure.label}</p>
|
|
37
|
+
|
|
38
|
+
<details class="dropdown">
|
|
39
|
+
<summary>Add</summary>
|
|
40
|
+
<ul>
|
|
41
|
+
${entries.map(([name, block]) => {
|
|
42
|
+
return html`
|
|
43
|
+
<li>
|
|
44
|
+
<form
|
|
45
|
+
data-on-submit="@post('/admin/api/new-block', { contentType: 'form' })"
|
|
46
|
+
>
|
|
47
|
+
<button type="submit" class="ghost">${block.label}</button>
|
|
48
|
+
<input type="hidden" name="type" value="${block.type}" />
|
|
49
|
+
<input type="hidden" name="name" value="${name}" />
|
|
50
|
+
<input type="hidden" name="label" value="${block.label}" />
|
|
51
|
+
<input type="hidden" name="parent_id" value="${data.id}" />
|
|
52
|
+
<input type="hidden" name="entry_id" value="${entryId}" />
|
|
53
|
+
<input
|
|
54
|
+
type="hidden"
|
|
55
|
+
name="sort_order"
|
|
56
|
+
value="${rows.length}"
|
|
57
|
+
/>
|
|
58
|
+
</form>
|
|
59
|
+
</li>
|
|
60
|
+
`
|
|
61
|
+
})}
|
|
62
|
+
</ul>
|
|
63
|
+
</details>
|
|
64
|
+
</header>
|
|
65
|
+
|
|
66
|
+
<hr style="margin-top: 0;" />
|
|
67
|
+
|
|
68
|
+
<sortable-list data-id="${data.id}">
|
|
69
|
+
${rows.map((row, idx) => {
|
|
70
|
+
const [name, struct] =
|
|
71
|
+
entries.find(([name]) => name === row.name) || []
|
|
72
|
+
|
|
73
|
+
if (!name || !struct) return html`<p>No name</p>`
|
|
74
|
+
|
|
75
|
+
return html`
|
|
76
|
+
<article data-id="${row.id}">
|
|
77
|
+
<header>
|
|
78
|
+
${struct.label}
|
|
79
|
+
|
|
80
|
+
<aside>
|
|
81
|
+
<button
|
|
82
|
+
data-handle-for="${data.id}"
|
|
83
|
+
class="ghost handle text-secondary"
|
|
84
|
+
style="cursor: grab"
|
|
85
|
+
>
|
|
86
|
+
${icons.bars}
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<form
|
|
90
|
+
data-on-submit="@delete('/admin/api/block', { contentType: 'form' })"
|
|
91
|
+
>
|
|
92
|
+
<button
|
|
93
|
+
type="submit"
|
|
94
|
+
class="ghost text-secondary"
|
|
95
|
+
data-tooltip="Remove"
|
|
96
|
+
data-placement="top"
|
|
97
|
+
aria-label="Delete block"
|
|
98
|
+
>
|
|
99
|
+
${icons.x}
|
|
100
|
+
</button>
|
|
101
|
+
<input type="hidden" name="id" value="${row.id}" />
|
|
102
|
+
<input type="hidden" name="entry_id" value="${entryId}" />
|
|
103
|
+
</form>
|
|
104
|
+
</aside>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
${Render({
|
|
108
|
+
entryId,
|
|
109
|
+
parentId:
|
|
110
|
+
struct.instanceOf === BlockInstance ? row.id : data.id,
|
|
111
|
+
id: row.id,
|
|
112
|
+
structure: struct,
|
|
113
|
+
name: name,
|
|
114
|
+
})}
|
|
115
|
+
</article>
|
|
116
|
+
`
|
|
117
|
+
})}
|
|
118
|
+
</sortable-list>
|
|
119
|
+
</div>
|
|
120
|
+
`
|
|
121
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { BlockDef } from '../types.ts'
|
|
3
|
+
import Render from './Render.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
id?: number
|
|
9
|
+
structure: BlockDef
|
|
10
|
+
}) => {
|
|
11
|
+
const { entryId, parentId, structure } = props
|
|
12
|
+
|
|
13
|
+
const entries = Object.entries(structure.fields)
|
|
14
|
+
|
|
15
|
+
return html`${entries.map(([name, field]) => {
|
|
16
|
+
try {
|
|
17
|
+
return Render({ entryId, parentId, structure: field, name })
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return html`<p>Cound not render: "${name}"</p>`
|
|
20
|
+
}
|
|
21
|
+
})}`
|
|
22
|
+
}
|
package/components/Entries.ts
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
import { html } from 'hono/html'
|
|
2
2
|
import { query } from '../index.ts'
|
|
3
3
|
import * as icons from './icons.ts'
|
|
4
|
+
import type { BlockDef } from '../types.ts'
|
|
4
5
|
|
|
5
|
-
export default () => {
|
|
6
|
-
const entries = query.blocks({
|
|
6
|
+
export default ({ name }: { name: string }) => {
|
|
7
|
+
const entries = query.blocks({ parent_id: null, status: 'enabled', name })
|
|
7
8
|
|
|
8
9
|
return html`
|
|
9
10
|
<section id="entries">
|
|
10
11
|
<ul>
|
|
11
12
|
${entries?.map((block) => {
|
|
12
13
|
const title = query.block({
|
|
13
|
-
|
|
14
|
+
parent_id: block.id.toString(),
|
|
14
15
|
name: 'title',
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
return html`
|
|
18
19
|
<li>
|
|
19
20
|
<a href="/admin/entry/${block.id}" id="block_link_${block.id}">
|
|
20
|
-
${
|
|
21
|
+
${title?.value || 'Untitled'}
|
|
21
22
|
</a>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
data-
|
|
25
|
-
class="ghost"
|
|
26
|
-
style="padding: 0"
|
|
23
|
+
|
|
24
|
+
<form
|
|
25
|
+
data-on-submit="@delete('/admin/api/block', { contentType: 'form' })"
|
|
27
26
|
>
|
|
28
|
-
{
|
|
29
|
-
|
|
27
|
+
<input type="hidden" name="id" value="${block.id}" />
|
|
28
|
+
<button
|
|
29
|
+
data-tooltip="Remove"
|
|
30
|
+
data-placement="right"
|
|
31
|
+
class="ghost text-secondary"
|
|
32
|
+
style="padding: 0"
|
|
33
|
+
type="submit"
|
|
34
|
+
>
|
|
35
|
+
${icons.trash}
|
|
36
|
+
</button>
|
|
37
|
+
</form>
|
|
30
38
|
</li>
|
|
31
39
|
`
|
|
32
40
|
})}
|