Skip to content

Commit

Permalink
feat(aih): list tags support, add more list types
Browse files Browse the repository at this point in the history
  • Loading branch information
vojtaholik committed Jan 16, 2025
1 parent b9f9d07 commit b5abc27
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import type { List, ListSchema } from '@/lib/lists'
import { ListTypeSchema } from '@/lib/lists'
import { addTagToPost, removeTagFromPost } from '@/lib/posts-query'
import type { Tag } from '@/lib/tags'
import { Pencil } from 'lucide-react'
import type { UseFormReturn } from 'react-hook-form'
import { z } from 'zod'

import { VideoResource } from '@coursebuilder/core/schemas/video-resource'
import {
Button,
FormControl,
Expand All @@ -28,9 +32,40 @@ import AdvancedTagSelector from '@coursebuilder/ui/resources-crud/tag-selector'

export const ListMetadataFormFields: React.FC<{
form: UseFormReturn<z.infer<typeof ListSchema>>
tagLoader: Promise<Tag[]>
list: List
}> = ({ form, list }) => {
}> = ({ form, list, tagLoader }) => {
const router = useRouter()
const tags = tagLoader ? React.use(tagLoader) : []

const parsedTagsForUiPackage = z
.array(
z.object({
id: z.string(),
fields: z.object({
label: z.string(),
name: z.string(),
}),
}),
)
.parse(tags)

const parsedSelectedTagsForUiPackage = z
.array(
z.object({
tag: z.object({
id: z.string(),
fields: z.object({
label: z.string(),
name: z.string(),
}),
}),
}),
)
.parse(list.tags)

// Get all possible values from the enum
const listTypeOptions = ListTypeSchema.options

return (
<>
Expand All @@ -39,7 +74,6 @@ export const ListMetadataFormFields: React.FC<{
name="id"
render={({ field }) => <Input type="hidden" {...field} />}
/>

<FormField
control={form.control}
name="fields.title"
Expand Down Expand Up @@ -84,14 +118,47 @@ export const ListMetadataFormFields: React.FC<{
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="nextUp">Next Up</SelectItem>
{listTypeOptions.map((type) => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)
}}
/>
{tags?.length > 0 && (
<div className="px-5">
<div className="flex w-full items-baseline justify-between">
<FormLabel className="text-lg font-bold">Tags</FormLabel>
<Button
variant="ghost"
size="sm"
className="flex items-center gap-1 opacity-75 hover:opacity-100"
asChild
>
<Link href="/admin/tags">
<Pencil className="h-3 w-3" /> Edit
</Link>
</Button>
</div>
<AdvancedTagSelector
availableTags={parsedTagsForUiPackage}
selectedTags={
parsedSelectedTagsForUiPackage?.map((tag) => tag.tag) ?? []
}
onTagSelect={async (tag: { id: string }) => {
await addTagToPost(list.id, tag.id)
}}
onTagRemove={async (tagId: string) => {
await removeTagFromPost(list.id, tagId)
}}
/>
</div>
)}
<FormField
control={form.control}
name="fields.description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import { useTheme } from 'next-themes'
import { useForm, type UseFormReturn } from 'react-hook-form'
import { z } from 'zod'

import { ContentResourceSchema } from '@coursebuilder/core/schemas'
import { VideoResource } from '@coursebuilder/core/schemas/video-resource'
import { EditResourcesFormDesktop } from '@coursebuilder/ui/resources-crud/edit-resources-form-desktop'

import { ListMetadataFormFields } from './edit-list-form-metadata'
Expand All @@ -43,9 +41,13 @@ export type EditListFormProps = {
children?: React.ReactNode
availableWorkflows?: { value: string; label: string; default?: boolean }[]
theme?: string
tagLoader: Promise<Tag[]>
}

export function EditListForm({ list }: Omit<EditListFormProps, 'form'>) {
export function EditListForm({
list,
tagLoader,
}: Omit<EditListFormProps, 'form'>) {
const { forcedTheme: theme } = useTheme()
const session = useSession()
const form = useForm<z.infer<typeof ListSchema>>({
Expand Down Expand Up @@ -76,7 +78,7 @@ export function EditListForm({ list }: Omit<EditListFormProps, 'form'>) {
}
}}
resourceSchema={ListSchema}
getResourcePath={(slug) => `/lists`}
getResourcePath={(slug) => `/${slug}`}
updateResource={updateList}
// autoUpdateResource={autoUpdatePost}
form={form}
Expand Down Expand Up @@ -111,7 +113,7 @@ export function EditListForm({ list }: Omit<EditListFormProps, 'form'>) {
]}
>
<React.Suspense fallback={<div>loading</div>}>
<ListMetadataFormFields form={form} list={list} />
<ListMetadataFormFields tagLoader={tagLoader} form={form} list={list} />
</React.Suspense>
</EditResourcesFormDesktop>
)
Expand Down
11 changes: 9 additions & 2 deletions apps/ai-hero/src/app/(content)/lists/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Metadata, ResolvingMetadata } from 'next'
import { notFound, redirect } from 'next/navigation'
import { getList } from '@/lib/lists-query'
import { getPost } from '@/lib/posts-query'
import { getTags } from '@/lib/tags-query'
import { getServerAuthSession } from '@/server/auth'
import { subject } from '@casl/ability'

Expand Down Expand Up @@ -35,7 +36,7 @@ export default async function ListEditPage(props: {
params: Promise<{ slug: string }>
}) {
const params = await props.params

const tagLoader = getTags()
const { ability } = await getServerAuthSession()
const list = await getList(params.slug)

Expand All @@ -47,5 +48,11 @@ export default async function ListEditPage(props: {
redirect(`/${list?.fields?.slug}`)
}

return <EditListForm key={list.fields.slug} list={{ ...list }} />
return (
<EditListForm
tagLoader={tagLoader}
key={list.fields.slug}
list={{ ...list }}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,42 @@

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { ListTypeSchema } from '@/lib/lists'
import { createList } from '@/lib/lists-query'

import { Button, Input, Label, Textarea } from '@coursebuilder/ui'
import {
Button,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Textarea,
} from '@coursebuilder/ui'

export function CreateListForm() {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [listType, setListType] = useState('nextUp')
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
setIsLoading(true)
await createList({ title, description })
await createList({ title, description, listType })
setTitle('')
setDescription('')
setListType('')
router.refresh()
} finally {
setIsLoading(false)
}
}
const listTypeOptions = ListTypeSchema.options

return (
<form
Expand All @@ -44,6 +58,29 @@ export function CreateListForm() {
required
/>
</div>
<div>
<Label htmlFor="listType" className="mb-1 block text-sm font-medium">
Type
</Label>
<Select
name="listType"
onValueChange={(value) => {
setListType(value)
}}
defaultValue={listType || 'nextUp'}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select list type..." />
</SelectTrigger>
<SelectContent>
{listTypeOptions.map((type) => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="description" className="mb-1 block text-sm font-medium">
Description (optional)
Expand Down
21 changes: 19 additions & 2 deletions apps/ai-hero/src/lib/lists-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import { revalidatePath, revalidateTag } from 'next/cache'
import { courseBuilderAdapter, db } from '@/db'
import { contentResource, contentResourceResource } from '@/db/schema'
import {
contentResource,
contentResourceResource,
contentResourceTag as contentResourceTagTable,
} from '@/db/schema'
import { getServerAuthSession } from '@/server/auth'
import { guid } from '@/utils/guid'
import { subject } from '@casl/ability'
Expand All @@ -15,6 +19,7 @@ import { deletePostInTypeSense, upsertPostToTypeSense } from './typesense-query'

export async function createList(input: {
title: string
listType: string
description?: string
}) {
const { session, ability } = await getServerAuthSession()
Expand All @@ -30,7 +35,7 @@ export async function createList(input: {
fields: {
title: input.title,
description: input.description,
type: 'nextUp',
type: input.listType,
state: 'draft',
visibility: 'unlisted',
slug: `${slugify(input.title)}~${guid()}`,
Expand Down Expand Up @@ -60,6 +65,12 @@ export async function getAllLists() {
},
orderBy: asc(contentResourceResource.position),
},
tags: {
with: {
tag: true,
},
orderBy: asc(contentResourceTagTable.position),
},
},
orderBy: desc(contentResource.createdAt),
})
Expand All @@ -86,6 +97,12 @@ export async function getList(listIdOrSlug: string) {
},
orderBy: asc(contentResourceResource.position),
},
tags: {
with: {
tag: true,
},
orderBy: asc(contentResourceTagTable.position),
},
},
})

Expand Down
6 changes: 4 additions & 2 deletions apps/ai-hero/src/lib/lists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { z } from 'zod'

import { ContentResourceSchema } from '@coursebuilder/core/schemas'

import { PostSchema, PostStateSchema, PostVisibilitySchema } from './posts'
import { PostStateSchema, PostTagsChema, PostVisibilitySchema } from './posts'

export const ListTypeSchema = z.enum(['nextUp'])
export const ListTypeSchema = z.enum(['nextUp', 'tutorial', 'workshop'])

export const ListSchema = ContentResourceSchema.merge(
z.object({
Expand All @@ -21,6 +21,7 @@ export const ListSchema = ContentResourceSchema.merge(
gitpod: z.string().nullish(),
}),
resources: z.array(z.any()),
tags: PostTagsChema,
}),
)

Expand All @@ -39,6 +40,7 @@ export const ListUpdateSchema = z.object({
github: z.string().nullish(),
}),
videoResourceId: z.string().optional().nullable(),
tags: PostTagsChema,
})

export type ListUpdate = z.infer<typeof ListUpdateSchema>
2 changes: 1 addition & 1 deletion apps/ai-hero/src/utils/get-nextup-resource-from-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function getNextUpResourceFromList(
list: List,
currentResourceId: string,
) {
if (list?.fields?.type !== 'nextUp') return null
// if (list?.fields?.type !== 'nextUp') return null

const currentResourceIndexFromList = list?.resources.findIndex(
(r) => r.resource.id === currentResourceId,
Expand Down

0 comments on commit b5abc27

Please sign in to comment.