Skip to content

feat(aih): edit and create lists #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import * as React from 'react'
import { useRouter } from 'next/navigation'
import type { List, ListSchema } from '@/lib/lists'
import type { UseFormReturn } from 'react-hook-form'
import { z } from 'zod'

import { VideoResource } from '@coursebuilder/core/schemas/video-resource'
import {
Button,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
Textarea,
} from '@coursebuilder/ui'
import { useSocket } from '@coursebuilder/ui/hooks/use-socket'
import { MetadataFieldState } from '@coursebuilder/ui/resources-crud/metadata-fields/metadata-field-state'
import { MetadataFieldVisibility } from '@coursebuilder/ui/resources-crud/metadata-fields/metadata-field-visibility'
import AdvancedTagSelector from '@coursebuilder/ui/resources-crud/tag-selector'

export const ListMetadataFormFields: React.FC<{
form: UseFormReturn<z.infer<typeof ListSchema>>
list: List
}> = ({ form, list }) => {
const router = useRouter()

return (
<>
<FormField
control={form.control}
name="id"
render={({ field }) => <Input type="hidden" {...field} />}
/>

<FormField
control={form.control}
name="fields.title"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Title</FormLabel>
<FormDescription>
A title should summarize the post and explain what it is about
clearly.
</FormDescription>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.slug"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Slug</FormLabel>
<FormDescription>Short with keywords is best.</FormDescription>
<Input {...field} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.description"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">
SEO Description ({`${field.value?.length ?? '0'} / 160`})
</FormLabel>
<FormDescription>
A short snippet that summarizes the post in under 160 characters.
Keywords should be included to support SEO.
</FormDescription>
<Textarea {...field} value={field.value ?? ''} />
{field.value && field.value.length > 160 && (
<FormMessage>
Your description is longer than 160 characters
</FormMessage>
)}
<FormMessage />
</FormItem>
)}
/>
<MetadataFieldVisibility form={form} />
<MetadataFieldState form={form} />
<FormField
control={form.control}
name="fields.github"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">GitHub</FormLabel>
<FormDescription>
Direct link to the GitHub file associated with the post.
</FormDescription>
<Input {...field} value={field.value ?? ''} />
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="fields.gitpod"
render={({ field }) => (
<FormItem className="px-5">
<FormLabel className="text-lg font-bold">Gitpod</FormLabel>
<FormDescription>
Gitpod link to start a new workspace with the post.
</FormDescription>
<Input {...field} value={field.value ?? ''} />
<FormMessage />
</FormItem>
)}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import * as React from 'react'
import { PostMetadataFormFields } from '@/app/(content)/posts/_components/edit-post-form-metadata'
import { ImageResourceUploader } from '@/components/image-uploader/image-resource-uploader'
import { env } from '@/env.mjs'
import { useIsMobile } from '@/hooks/use-is-mobile'
import { sendResourceChatMessage } from '@/lib/ai-chat-query'
import { List, ListSchema } from '@/lib/lists'
import { Post, PostSchema } from '@/lib/posts'
import { autoUpdatePost, updatePost } from '@/lib/posts-query'
import type { Tag } from '@/lib/tags'
import { zodResolver } from '@hookform/resolvers/zod'
import { ImagePlusIcon } from 'lucide-react'
import { useSession } from 'next-auth/react'
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'
import ListResoucesEdit from './list-resources-edit'

// import { MobileEditPostForm } from './edit-post-form-mobile'

const NewPostFormSchema = z.object({
title: z.string().min(2).max(90),
body: z.string().nullish(),
visibility: z.enum(['public', 'unlisted', 'private']),
description: z.string().nullish(),
github: z.string().nullish(),
gitpod: z.string().nullish(),
})

export type EditListFormProps = {
list: List
form: UseFormReturn<z.infer<typeof ListSchema>>
children?: React.ReactNode
availableWorkflows?: { value: string; label: string; default?: boolean }[]
theme?: string
}

export function EditListForm({ list }: Omit<EditListFormProps, 'form'>) {
const { forcedTheme: theme } = useTheme()
const session = useSession()
const form = useForm<z.infer<typeof ListSchema>>({
resolver: zodResolver(NewPostFormSchema),
defaultValues: {
id: list.id,
fields: {
title: list.fields?.title,
body: list.fields?.body,
slug: list.fields?.slug,
visibility: list.fields?.visibility || 'public',
state: list.fields?.state || 'draft',
description: list.fields?.description ?? '',
github: list.fields?.github ?? '',
gitpod: list.fields?.gitpod ?? '',
},
},
})
const isMobile = useIsMobile()

return (
<EditResourcesFormDesktop
resource={list}
resourceSchema={ListSchema}
getResourcePath={(slug) => `/${slug}`}
updateResource={updatePost}
autoUpdateResource={autoUpdatePost}
form={form}
bodyPanelSlot={<ListResoucesEdit list={list} />}
availableWorkflows={[]}
sendResourceChatMessage={sendResourceChatMessage}
hostUrl={env.NEXT_PUBLIC_PARTY_KIT_URL}
user={session?.data?.user}
theme={theme}
onResourceBodyChange={() => {}}
tools={[
{ id: 'assistant' },
{
id: 'media',
icon: () => (
<ImagePlusIcon strokeWidth={1.5} size={24} width={18} height={18} />
),
toolComponent: (
<ImageResourceUploader
key={'image-uploader'}
belongsToResourceId={list.id}
uploadDirectory={`posts`}
/>
),
},
]}
>
<React.Suspense fallback={<div>loading</div>}>
<ListMetadataFormFields form={form} list={list} />
</React.Suspense>
</EditResourcesFormDesktop>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use client'

import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { addPostToList } from '@/lib/lists-query'
import type { TypesenseResource } from '@/lib/typesense'

import { Button } from '@coursebuilder/ui'

import type { TreeAction } from './lesson-list/data/tree'
import { useSelection } from './selection-context'

export default function Hit({
hit,
listId,
updateTreeState,
}: {
hit: TypesenseResource
listId: string
updateTreeState: React.ActionDispatch<[action: TreeAction]>
}) {
const { toggleSelection, isSelected } = useSelection()

return (
<li className={`${isSelected(hit.id) ? 'bg-muted' : ''}`}>
<button
type="button"
className="hover:bg-muted/50 group flex w-full flex-row items-baseline justify-between gap-2 px-5 py-3 sm:py-3"
onClick={() => toggleSelection(hit)}
>
<div className="flex items-center gap-2">
<span className="pr-5 font-medium sm:truncate">{hit.title}</span>
</div>
<div className="text-muted-foreground fon-normal flex flex-shrink-0 items-center gap-3 pl-7 text-xs capitalize opacity-60 sm:pl-0">
<span>{hit.type}</span>
</div>
</button>
</li>
)
}
Loading
Loading