@stacksjs/defaults 0.70.88 → 0.70.90
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/app/Actions/Blog/BlogDestroyAction.ts +25 -0
- package/app/Actions/Blog/BlogIndexAction.ts +18 -0
- package/app/Actions/Blog/BlogShowAction.ts +25 -0
- package/app/Actions/Blog/BlogStoreAction.ts +21 -0
- package/app/Actions/Blog/BlogUpdateAction.ts +23 -0
- package/app/Actions/Blog/payload.ts +56 -0
- package/app/Actions/Cms/PostStoreAction.ts +10 -5
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +38 -0
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +53 -39
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +61 -0
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +40 -17
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +58 -0
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +46 -31
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +48 -0
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +25 -0
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +43 -15
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +49 -0
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +37 -0
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +64 -83
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +50 -0
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +54 -0
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +30 -0
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +39 -13
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +60 -0
- package/app/Actions/Dashboard/Content/comment-input.ts +35 -0
- package/app/Actions/Dashboard/Content/content-input.ts +65 -0
- package/app/Actions/Dashboard/Content/post-input.ts +54 -0
- package/app/Models/Content/Post.ts +4 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/FileViewer/MediaCard.stx +119 -0
- package/resources/components/Dashboard/FileViewer/MediaInfoPanel.stx +143 -0
- package/resources/components/Dashboard/SidebarModern.stx +1 -0
- package/resources/functions/dashboard/files-tree.ts +162 -0
- package/resources/functions/dashboard/sidebar.ts +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action, BlogAdminError, deleteBlogPost } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
|
|
5
|
+
export default new Action({
|
|
6
|
+
name: 'Blog Destroy',
|
|
7
|
+
description: 'Deletes a markdown post from content/blog.',
|
|
8
|
+
method: 'DELETE',
|
|
9
|
+
async handle(request: RequestInstance) {
|
|
10
|
+
try {
|
|
11
|
+
const slug = String(request.getParam('slug') ?? '')
|
|
12
|
+
|
|
13
|
+
if (!deleteBlogPost(slug))
|
|
14
|
+
return response.json({ message: 'Post not found.' }, 404)
|
|
15
|
+
|
|
16
|
+
return response.json({ message: 'Post deleted.', slug })
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof BlogAdminError)
|
|
20
|
+
return response.json({ message: error.message }, error.status)
|
|
21
|
+
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Action, listBlogPosts } from '@stacksjs/actions'
|
|
2
|
+
import { response } from '@stacksjs/router'
|
|
3
|
+
|
|
4
|
+
export default new Action({
|
|
5
|
+
name: 'Blog Index',
|
|
6
|
+
description: 'Lists every markdown post in content/blog, drafts included.',
|
|
7
|
+
method: 'GET',
|
|
8
|
+
async handle() {
|
|
9
|
+
const posts = listBlogPosts()
|
|
10
|
+
|
|
11
|
+
return response.json({
|
|
12
|
+
posts,
|
|
13
|
+
publishedCount: posts.filter(p => !p.draft).length,
|
|
14
|
+
draftCount: posts.filter(p => p.draft).length,
|
|
15
|
+
featuredCount: posts.filter(p => p.featured).length,
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action, BlogAdminError, getBlogPost } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
|
|
5
|
+
export default new Action({
|
|
6
|
+
name: 'Blog Show',
|
|
7
|
+
description: 'Returns one markdown post from content/blog, body included.',
|
|
8
|
+
method: 'GET',
|
|
9
|
+
async handle(request: RequestInstance) {
|
|
10
|
+
try {
|
|
11
|
+
const post = getBlogPost(String(request.getParam('slug') ?? ''))
|
|
12
|
+
|
|
13
|
+
if (!post)
|
|
14
|
+
return response.json({ message: 'Post not found.' }, 404)
|
|
15
|
+
|
|
16
|
+
return response.json(post)
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error instanceof BlogAdminError)
|
|
20
|
+
return response.json({ message: error.message }, error.status)
|
|
21
|
+
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action, BlogAdminError, saveBlogPost } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { blogPayload } from './payload'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'Blog Store',
|
|
8
|
+
description: 'Creates a markdown post in content/blog.',
|
|
9
|
+
method: 'POST',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
return response.json(saveBlogPost(blogPayload(request)), 201)
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof BlogAdminError)
|
|
16
|
+
return response.json({ message: error.message }, error.status)
|
|
17
|
+
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action, BlogAdminError, saveBlogPost } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { blogPayload } from './payload'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'Blog Update',
|
|
8
|
+
description: 'Updates a markdown post in content/blog, renaming the file when the slug changes.',
|
|
9
|
+
method: 'PATCH',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const originalSlug = String(request.getParam('slug') ?? '')
|
|
13
|
+
|
|
14
|
+
return response.json(saveBlogPost(blogPayload(request, originalSlug)))
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof BlogAdminError)
|
|
18
|
+
return response.json({ message: error.message }, error.status)
|
|
19
|
+
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { BlogPostInput } from '@stacksjs/actions'
|
|
2
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `true` only for values that actually mean true over the wire.
|
|
6
|
+
*
|
|
7
|
+
* A JSON body gives real booleans, but a form post gives `"true"` / `"on"` and
|
|
8
|
+
* an unchecked box gives nothing at all. Without this the string `"false"` would
|
|
9
|
+
* be truthy and every post would save as a draft.
|
|
10
|
+
*/
|
|
11
|
+
function bool(value: unknown): boolean {
|
|
12
|
+
if (typeof value === 'boolean')
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
return value === 'true' || value === 'on' || value === 1 || value === '1'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function str(value: unknown): string {
|
|
19
|
+
return value == null ? '' : String(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The slug the caller wants the post to HAVE, read from the request body only.
|
|
24
|
+
*
|
|
25
|
+
* `request.get('slug')` cannot be used here: the update route is `/blog/{slug}`,
|
|
26
|
+
* and `get()` resolves the route param, which shadows the body field of the same
|
|
27
|
+
* name. That silently turned every rename into a no-op, because the "new" slug
|
|
28
|
+
* always came back as the current one.
|
|
29
|
+
*/
|
|
30
|
+
function bodySlug(request: RequestInstance): string {
|
|
31
|
+
const body = (request.jsonBody ?? request.formBody) as Record<string, unknown> | undefined
|
|
32
|
+
|
|
33
|
+
return str(body?.slug)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Maps a request body onto the blog-admin input shape.
|
|
38
|
+
*
|
|
39
|
+
* `originalSlug` is passed in by the update action (from the route param) rather
|
|
40
|
+
* than read from the body, so a create can never be mistaken for a rename.
|
|
41
|
+
*/
|
|
42
|
+
export function blogPayload(request: RequestInstance, originalSlug = ''): BlogPostInput {
|
|
43
|
+
return {
|
|
44
|
+
slug: bodySlug(request),
|
|
45
|
+
originalSlug,
|
|
46
|
+
title: str(request.get('title')),
|
|
47
|
+
description: str(request.get('description')),
|
|
48
|
+
date: str(request.get('date')),
|
|
49
|
+
author: str(request.get('author')),
|
|
50
|
+
authorBio: str(request.get('authorBio')),
|
|
51
|
+
poster: str(request.get('poster')),
|
|
52
|
+
featured: bool(request.get('featured')),
|
|
53
|
+
draft: bool(request.get('draft')),
|
|
54
|
+
body: str(request.get('body')),
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -13,21 +13,26 @@ export default new Action({
|
|
|
13
13
|
async handle(request) {
|
|
14
14
|
await request.validate()
|
|
15
15
|
|
|
16
|
+
const user = await request.user()
|
|
17
|
+
|
|
18
|
+
if (!user)
|
|
19
|
+
return response.unauthorized('Authentication required')
|
|
20
|
+
|
|
16
21
|
const author = await authors.findOrCreate({
|
|
17
|
-
name:
|
|
18
|
-
email:
|
|
22
|
+
name: user.name,
|
|
23
|
+
email: user.email,
|
|
19
24
|
})
|
|
20
25
|
|
|
26
|
+
const status = request.get('status') || 'draft'
|
|
27
|
+
|
|
21
28
|
const data = {
|
|
22
29
|
author_id: author.id,
|
|
23
30
|
title: request.get('title'),
|
|
24
31
|
excerpt: request.get('excerpt'),
|
|
25
|
-
slug: request.get('slug'),
|
|
26
32
|
content: request.get('content'),
|
|
27
|
-
status
|
|
33
|
+
status,
|
|
28
34
|
poster: request.get('poster'),
|
|
29
35
|
views: 0,
|
|
30
|
-
published_at: Date.now(),
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
const model = await posts.store(data)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { rowExists, rowId, timestamp } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `DELETE /api/dashboard/authors/{id}` — deletes a CMS author from the dashboard.
|
|
9
|
+
*
|
|
10
|
+
* Posts written by the author are kept and unassigned rather than deleted:
|
|
11
|
+
* losing an author should not silently take their posts with it. The schema
|
|
12
|
+
* declares no foreign key on `posts.author_id`, so without this the rows would
|
|
13
|
+
* be left pointing at an id that no longer resolves.
|
|
14
|
+
*/
|
|
15
|
+
export default new Action({
|
|
16
|
+
name: 'AuthorDestroyAction',
|
|
17
|
+
description: 'Deletes a CMS author from the dashboard.',
|
|
18
|
+
method: 'DELETE',
|
|
19
|
+
async handle(request: RequestInstance) {
|
|
20
|
+
const id = rowId(request)
|
|
21
|
+
|
|
22
|
+
if (!id)
|
|
23
|
+
return response.json({ message: 'A valid author id is required.' }, 422)
|
|
24
|
+
|
|
25
|
+
if (!await rowExists('authors', id))
|
|
26
|
+
return response.json({ message: 'Author not found.' }, 404)
|
|
27
|
+
|
|
28
|
+
await db
|
|
29
|
+
.updateTable('posts')
|
|
30
|
+
.set({ author_id: null, updated_at: timestamp() } as any)
|
|
31
|
+
.where('author_id', '=', id)
|
|
32
|
+
.execute()
|
|
33
|
+
|
|
34
|
+
await db.deleteFrom('authors').where('id', '=', id).execute()
|
|
35
|
+
|
|
36
|
+
return response.json({ message: 'Author deleted.', id })
|
|
37
|
+
},
|
|
38
|
+
})
|
|
@@ -1,51 +1,65 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{ id: 5, name: 'Sarah Williams', email: 'sarah@example.com', role: 'Author', avatar: 'https://i.pravatar.cc/150?img=5', bio: 'Backend engineer focused on API design and databases.', postCount: 6, totalViews: 22000, totalComments: 47, created_at: '2023-06-30' },
|
|
14
|
-
]
|
|
4
|
+
interface AuthorRow {
|
|
5
|
+
id: number
|
|
6
|
+
name: string | null
|
|
7
|
+
email: string | null
|
|
8
|
+
bio: string | null
|
|
9
|
+
avatar: string | null
|
|
10
|
+
created_at: string | null
|
|
11
|
+
updated_at: string | null
|
|
12
|
+
}
|
|
15
13
|
|
|
14
|
+
/**
|
|
15
|
+
* `GET /api/dashboard/authors` — backs `views/dashboard/content/authors/index.stx`.
|
|
16
|
+
*
|
|
17
|
+
* Reads straight from the `authors` table via `db` rather than the ORM model:
|
|
18
|
+
* `Author` from `@stacksjs/orm` exposes no query methods today, so the previous
|
|
19
|
+
* `Author.all()` call threw on every request — and the action caught it and
|
|
20
|
+
* served five hardcoded "Jane Doe" rows, which made an empty (or broken)
|
|
21
|
+
* dashboard look populated. There is deliberately no fallback now: a failure
|
|
22
|
+
* here must surface as a failure.
|
|
23
|
+
*
|
|
24
|
+
* `postCount` is derived here rather than read off the row. The `authors` table
|
|
25
|
+
* has no counter column, and nothing would maintain one if it did.
|
|
26
|
+
*/
|
|
16
27
|
export default new Action({
|
|
17
28
|
name: 'AuthorIndexAction',
|
|
18
|
-
description: 'Returns authors
|
|
29
|
+
description: 'Returns CMS authors for the dashboard.',
|
|
19
30
|
method: 'GET',
|
|
31
|
+
apiResponse: true,
|
|
20
32
|
async handle() {
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
const [rows, postAuthors] = await Promise.all([
|
|
34
|
+
db.selectFrom('authors').selectAll().orderBy('created_at', 'desc').execute() as unknown as Promise<AuthorRow[]>,
|
|
35
|
+
db.selectFrom('posts').select(['author_id']).execute() as unknown as Promise<Array<{ author_id: number | null }>>,
|
|
36
|
+
])
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
|
|
38
|
+
const postCounts = new Map<number, number>()
|
|
39
|
+
for (const post of postAuthors) {
|
|
40
|
+
if (post.author_id == null)
|
|
41
|
+
continue
|
|
28
42
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
id: Number(a.get('id') || idx + 1),
|
|
33
|
-
name,
|
|
34
|
-
email: String(a.get('email') || ''),
|
|
35
|
-
role: String(a.get('role') || 'Writer'),
|
|
36
|
-
avatar: String(a.get('avatar') || `https://i.pravatar.cc/150?img=${idx + 1}`),
|
|
37
|
-
bio: String(a.get('bio') || ''),
|
|
38
|
-
postCount: Number(a.get('post_count') || a.get('posts') || 0),
|
|
39
|
-
totalViews: Number(a.get('total_views') || 0),
|
|
40
|
-
totalComments: Number(a.get('total_comments') || 0),
|
|
41
|
-
created_at: String(a.get('created_at') || ''),
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
return { authors }
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return { authors: mockAuthors }
|
|
43
|
+
const id = Number(post.author_id)
|
|
44
|
+
postCounts.set(id, (postCounts.get(id) || 0) + 1)
|
|
49
45
|
}
|
|
46
|
+
|
|
47
|
+
const authors = rows.map(row => ({
|
|
48
|
+
id: Number(row.id),
|
|
49
|
+
name: String(row.name || ''),
|
|
50
|
+
email: String(row.email || ''),
|
|
51
|
+
bio: String(row.bio || ''),
|
|
52
|
+
avatar: String(row.avatar || ''),
|
|
53
|
+
postCount: postCounts.get(Number(row.id)) || 0,
|
|
54
|
+
created_at: row.created_at || null,
|
|
55
|
+
updated_at: row.updated_at || null,
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
// Posts whose author no longer exists are not counted: the stat means
|
|
59
|
+
// "posts reachable from this list", which is what the page's Assigned Posts
|
|
60
|
+
// tile claims to show.
|
|
61
|
+
const assignedPostCount = authors.reduce((sum, author) => sum + author.postCount, 0)
|
|
62
|
+
|
|
63
|
+
return { authors, assignedPostCount }
|
|
50
64
|
},
|
|
51
65
|
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { randomUUIDv7 } from 'bun'
|
|
6
|
+
import { findRow, insertedId, str, timestamp } from './content-input'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `POST /api/dashboard/authors` — creates a CMS author from the dashboard.
|
|
10
|
+
*
|
|
11
|
+
* Does not reuse `Actions/Cms/AuthorStoreAction`: that runs the `Author` model
|
|
12
|
+
* validation, which requires a name of at least five characters and rejects the
|
|
13
|
+
* empty bio/avatar the dashboard dialog submits.
|
|
14
|
+
*
|
|
15
|
+
* The email check is done here because `authors_email_name_index` is not a
|
|
16
|
+
* unique index — nothing in the schema stops a duplicate.
|
|
17
|
+
*/
|
|
18
|
+
export default new Action({
|
|
19
|
+
name: 'AuthorStoreAction',
|
|
20
|
+
description: 'Creates a CMS author from the dashboard.',
|
|
21
|
+
method: 'POST',
|
|
22
|
+
async handle(request: RequestInstance) {
|
|
23
|
+
const name = str(request.get('name')).trim()
|
|
24
|
+
const email = str(request.get('email')).trim()
|
|
25
|
+
|
|
26
|
+
if (!name)
|
|
27
|
+
return response.json({ message: 'Name is required.' }, 422)
|
|
28
|
+
|
|
29
|
+
if (!email)
|
|
30
|
+
return response.json({ message: 'Email is required.' }, 422)
|
|
31
|
+
|
|
32
|
+
const duplicate = await db
|
|
33
|
+
.selectFrom('authors')
|
|
34
|
+
.select(['id'])
|
|
35
|
+
.where('email', '=', email)
|
|
36
|
+
.executeTakeFirst()
|
|
37
|
+
|
|
38
|
+
if (duplicate)
|
|
39
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
40
|
+
|
|
41
|
+
const now = timestamp()
|
|
42
|
+
|
|
43
|
+
const result = await db
|
|
44
|
+
.insertInto('authors')
|
|
45
|
+
.values({
|
|
46
|
+
uuid: randomUUIDv7(),
|
|
47
|
+
name,
|
|
48
|
+
email,
|
|
49
|
+
created_at: now,
|
|
50
|
+
updated_at: now,
|
|
51
|
+
} as any)
|
|
52
|
+
.executeTakeFirst()
|
|
53
|
+
|
|
54
|
+
const id = insertedId(result)
|
|
55
|
+
|
|
56
|
+
if (!id)
|
|
57
|
+
return response.json({ message: 'Could not create author.' }, 500)
|
|
58
|
+
|
|
59
|
+
return response.json(await findRow('authors', id), 201)
|
|
60
|
+
},
|
|
61
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { rowExists, rowId } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `DELETE /api/dashboard/categories/{id}` — deletes a CMS category.
|
|
9
|
+
*
|
|
10
|
+
* A plain row delete: this schema has no `categorizable_models` pivot table, so
|
|
11
|
+
* there is nothing to cascade to.
|
|
12
|
+
*/
|
|
13
|
+
export default new Action({
|
|
14
|
+
name: 'CategoryDestroyAction',
|
|
15
|
+
description: 'Deletes a CMS category from the dashboard.',
|
|
16
|
+
method: 'DELETE',
|
|
17
|
+
async handle(request: RequestInstance) {
|
|
18
|
+
const id = rowId(request)
|
|
19
|
+
|
|
20
|
+
if (!id)
|
|
21
|
+
return response.json({ message: 'A valid category id is required.' }, 422)
|
|
22
|
+
|
|
23
|
+
if (!await rowExists('categories', id))
|
|
24
|
+
return response.json({ message: 'Category not found.' }, 404)
|
|
25
|
+
|
|
26
|
+
await db.deleteFrom('categories').where('id', '=', id).execute()
|
|
27
|
+
|
|
28
|
+
return response.json({ message: 'Category deleted.', id })
|
|
29
|
+
},
|
|
30
|
+
})
|
|
@@ -1,27 +1,50 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
|
-
import {
|
|
2
|
+
import { db } from '@stacksjs/database'
|
|
3
3
|
|
|
4
|
+
interface CategoryRow {
|
|
5
|
+
id: number
|
|
6
|
+
name: string | null
|
|
7
|
+
slug: string | null
|
|
8
|
+
description: string | null
|
|
9
|
+
is_active: number | null
|
|
10
|
+
created_at: string | null
|
|
11
|
+
updated_at: string | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `GET /api/dashboard/categories` — backs `views/dashboard/content/categories/index.stx`.
|
|
16
|
+
*
|
|
17
|
+
* Reads the `categories` table via `db`. The previous `Category.all()` call
|
|
18
|
+
* threw on every request (the ORM model exposes no query methods) and the catch
|
|
19
|
+
* turned that into an empty list, so a broken read was indistinguishable from a
|
|
20
|
+
* CMS with no categories. It also mapped `parent_id` and `post_count`, neither
|
|
21
|
+
* of which this table has.
|
|
22
|
+
*
|
|
23
|
+
* Note this is the same `categories` table the commerce `Category` model owns —
|
|
24
|
+
* CMS and commerce categories are not separated in the schema today.
|
|
25
|
+
*/
|
|
4
26
|
export default new Action({
|
|
5
27
|
name: 'CategoryIndexAction',
|
|
6
|
-
description: 'Returns categories
|
|
28
|
+
description: 'Returns CMS categories for the dashboard.',
|
|
7
29
|
method: 'GET',
|
|
30
|
+
apiResponse: true,
|
|
8
31
|
async handle() {
|
|
9
|
-
|
|
10
|
-
|
|
32
|
+
const rows = await db
|
|
33
|
+
.selectFrom('categories')
|
|
34
|
+
.selectAll()
|
|
35
|
+
.orderBy('created_at', 'desc')
|
|
36
|
+
.execute() as unknown as CategoryRow[]
|
|
11
37
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
const categories = rows.map(row => ({
|
|
39
|
+
id: Number(row.id),
|
|
40
|
+
name: String(row.name || ''),
|
|
41
|
+
slug: String(row.slug || ''),
|
|
42
|
+
description: String(row.description || ''),
|
|
43
|
+
is_active: row.is_active == null ? true : Boolean(row.is_active),
|
|
44
|
+
created_at: row.created_at || null,
|
|
45
|
+
updated_at: row.updated_at || null,
|
|
46
|
+
}))
|
|
20
47
|
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return { categories: [] }
|
|
25
|
-
}
|
|
48
|
+
return { categories }
|
|
26
49
|
},
|
|
27
50
|
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { findRow, insertedId, slugify, str, timestamp } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `POST /api/dashboard/categories` — creates a CMS category from the dashboard.
|
|
9
|
+
*
|
|
10
|
+
* The dialog pre-fills the slug from the name but lets it be edited or cleared,
|
|
11
|
+
* so an empty slug is derived here rather than rejected.
|
|
12
|
+
*/
|
|
13
|
+
export default new Action({
|
|
14
|
+
name: 'CategoryStoreAction',
|
|
15
|
+
description: 'Creates a CMS category from the dashboard.',
|
|
16
|
+
method: 'POST',
|
|
17
|
+
async handle(request: RequestInstance) {
|
|
18
|
+
const name = str(request.get('name')).trim()
|
|
19
|
+
const description = str(request.get('description'))
|
|
20
|
+
const slug = slugify(str(request.get('slug')) || name)
|
|
21
|
+
|
|
22
|
+
if (!name)
|
|
23
|
+
return response.json({ message: 'Name is required.' }, 422)
|
|
24
|
+
|
|
25
|
+
if (!slug)
|
|
26
|
+
return response.json({ message: 'Slug could not be derived from the name; enter one.' }, 422)
|
|
27
|
+
|
|
28
|
+
const duplicate = await db
|
|
29
|
+
.selectFrom('categories')
|
|
30
|
+
.select(['id'])
|
|
31
|
+
.where('slug', '=', slug)
|
|
32
|
+
.executeTakeFirst()
|
|
33
|
+
|
|
34
|
+
if (duplicate)
|
|
35
|
+
return response.json({ message: 'A category with that slug already exists.' }, 422)
|
|
36
|
+
|
|
37
|
+
const now = timestamp()
|
|
38
|
+
|
|
39
|
+
const result = await db
|
|
40
|
+
.insertInto('categories')
|
|
41
|
+
.values({
|
|
42
|
+
name,
|
|
43
|
+
slug,
|
|
44
|
+
description,
|
|
45
|
+
is_active: 1,
|
|
46
|
+
created_at: now,
|
|
47
|
+
updated_at: now,
|
|
48
|
+
} as any)
|
|
49
|
+
.executeTakeFirst()
|
|
50
|
+
|
|
51
|
+
const id = insertedId(result)
|
|
52
|
+
|
|
53
|
+
if (!id)
|
|
54
|
+
return response.json({ message: 'Could not create category.' }, 500)
|
|
55
|
+
|
|
56
|
+
return response.json(await findRow('categories', id), 201)
|
|
57
|
+
},
|
|
58
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { response } from '@stacksjs/router'
|
|
5
|
+
import { rowExists, rowId } from './content-input'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `DELETE /api/dashboard/comments/{id}` — deletes a comment from the dashboard.
|
|
9
|
+
*
|
|
10
|
+
* A hard delete. The `trash` status exists for the reversible case, and the
|
|
11
|
+
* page's delete button confirms first.
|
|
12
|
+
*/
|
|
13
|
+
export default new Action({
|
|
14
|
+
name: 'CommentDestroyAction',
|
|
15
|
+
description: 'Deletes a CMS comment from the dashboard.',
|
|
16
|
+
method: 'DELETE',
|
|
17
|
+
async handle(request: RequestInstance) {
|
|
18
|
+
const id = rowId(request)
|
|
19
|
+
|
|
20
|
+
if (!id)
|
|
21
|
+
return response.json({ message: 'A valid comment id is required.' }, 422)
|
|
22
|
+
|
|
23
|
+
if (!await rowExists('comments', id))
|
|
24
|
+
return response.json({ message: 'Comment not found.' }, 404)
|
|
25
|
+
|
|
26
|
+
await db.deleteFrom('comments').where('id', '=', id).execute()
|
|
27
|
+
|
|
28
|
+
return response.json({ message: 'Comment deleted.', id })
|
|
29
|
+
},
|
|
30
|
+
})
|