feat: un/assigned commits on version update

This commit is contained in:
Lars Hampe 2024-11-10 20:06:10 +01:00
parent d6cb69ec3b
commit b7f0713d6f
9 changed files with 393 additions and 147 deletions

View File

@ -1,7 +1,12 @@
import { changelog, changelog_version, db } from '@boring.tools/database'
import {
changelog,
changelog_commit,
changelog_version,
db,
} from '@boring.tools/database'
import { VersionByIdParams, VersionOutput } from '@boring.tools/schema'
import { createRoute } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm'
import { and, desc, eq } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import type changelogVersionApi from '.'
@ -37,7 +42,9 @@ export const registerVersionById = (api: typeof changelogVersionApi) => {
const versionResult = await db.query.changelog_version.findFirst({
where: eq(changelog_version.id, id),
with: {
commits: true,
commits: {
orderBy: () => desc(changelog_commit.createdAt),
},
},
})

View File

@ -1,7 +1,12 @@
import { changelog, changelog_version, db } from '@boring.tools/database'
import {
changelog,
changelog_commit,
changelog_version,
db,
} from '@boring.tools/database'
import { VersionUpdateInput, VersionUpdateOutput } from '@boring.tools/schema'
import { createRoute, type z } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm'
import { and, eq, inArray, notInArray } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import type changelogVersionApi from '.'
@ -74,6 +79,18 @@ export const registerVersionUpdate = (api: typeof changelogVersionApi) => {
.where(and(eq(changelog_version.id, id)))
.returning()
if (payload.commitIds) {
await db
.update(changelog_commit)
.set({ versionId: null })
.where(notInArray(changelog_commit.id, payload.commitIds))
await db
.update(changelog_commit)
.set({ versionId: versionUpdateResult.id })
.where(inArray(changelog_commit.id, payload.commitIds))
}
if (findChangelog.pageId) {
redis.del(findChangelog.pageId)
}

View File

@ -2,6 +2,11 @@ import { VersionUpdateInput } from '@boring.tools/schema'
import {
Button,
Calendar,
Card,
CardContent,
CardHeader,
CardTitle,
Checkbox,
Form,
FormControl,
FormField,
@ -12,12 +17,17 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
ScrollArea,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
cn,
} from '@boring.tools/ui'
import { zodResolver } from '@hookform/resolvers/zod'
@ -38,6 +48,7 @@ import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import type { z } from 'zod'
import {
useChangelogCommitList,
useChangelogVersionById,
useChangelogVersionUpdate,
} from '../hooks/useChangelog'
@ -56,6 +67,7 @@ const Component = () => {
const { data, error, isPending, refetch } = useChangelogVersionById({
id: versionId,
})
const commitResult = useChangelogCommitList({ id })
const form = useForm<z.infer<typeof VersionUpdateInput>>({
resolver: zodResolver(VersionUpdateInput),
@ -76,7 +88,10 @@ const Component = () => {
useEffect(() => {
if (data) {
mdxEditorRef.current?.setMarkdown(data.markdown)
form.reset(data)
form.reset({
...data,
commitIds: data.commits?.map((commit) => commit.id),
})
}
}, [data, form])
@ -95,102 +110,27 @@ const Component = () => {
<div className="flex flex-col gap-5">
<Separator />
{!isPending && data && (
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 max-w-screen-md"
>
<FormField
control={form.control}
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<FormControl>
<Input placeholder="v1.0.1" {...field} />
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="markdown"
render={({ field }) => (
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<MDXEditor
className="dark-theme"
contentEditableClassName="prose dark:prose-invert max-w-none"
markdown={''}
ref={mdxEditorRef}
onChange={field.onChange}
onBlur={field.onBlur}
plugins={[
headingsPlugin(),
listsPlugin(),
thematicBreakPlugin(),
quotePlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<BlockTypeSelect />
<BoldItalicUnderlineToggles />
<ListsToggle />
<UndoRedo />
</>
),
}),
]}
/>
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-5 items-center">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex gap-4 w-full"
>
<Card className="w-full">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Details</CardTitle>
</div>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="status"
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select your version status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="draft">
<div className="flex gap-2 items-center">
<VersionStatus status={'draft'} />
<span>Draft</span>
</div>
</SelectItem>
<SelectItem value="review">
<div className="flex gap-2 items-center">
<VersionStatus status={'review'} />
<span>Review</span>
</div>
</SelectItem>
<SelectItem value="published">
<div className="flex gap-2 items-center">
<VersionStatus status={'published'} />
<span>Published</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<FormLabel>Version</FormLabel>
<FormControl>
<Input placeholder="v1.0.1" {...field} />
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
@ -198,62 +138,286 @@ const Component = () => {
<FormField
control={form.control}
name="releasedAt"
name="markdown"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mb-2">Released at</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
size={'lg'}
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground',
)}
>
{field.value ? (
format(field.value, 'PPP')
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value as Date}
onSelect={(date) => field.onChange(date)}
weekStartsOn={1}
/>
</PopoverContent>
</Popover>
<FormItem>
<FormLabel>Notes</FormLabel>
<FormControl>
<MDXEditor
className="dark-theme"
contentEditableClassName="prose dark:prose-invert max-w-none"
markdown={''}
ref={mdxEditorRef}
onChange={field.onChange}
onBlur={field.onBlur}
plugins={[
headingsPlugin(),
listsPlugin(),
thematicBreakPlugin(),
quotePlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<BlockTypeSelect />
<BoldItalicUnderlineToggles />
<ListsToggle />
<UndoRedo />
</>
),
}),
]}
/>
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex gap-5">
<Button
type="button"
variant={'ghost'}
onClick={() =>
navigate({ to: '/changelog/$id', params: { id } })
}
>
Cancel
</Button>
<Button type="submit">Update</Button>
</div>
</form>
</Form>
<div className="flex gap-5 items-center">
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select your version status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="draft">
<div className="flex gap-2 items-center">
<VersionStatus status={'draft'} />
<span>Draft</span>
</div>
</SelectItem>
<SelectItem value="review">
<div className="flex gap-2 items-center">
<VersionStatus status={'review'} />
<span>Review</span>
</div>
</SelectItem>
<SelectItem value="published">
<div className="flex gap-2 items-center">
<VersionStatus status={'published'} />
<span>Published</span>
</div>
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<ChangelogVersionDelete id={id} versionId={versionId} />
</div>
<FormField
control={form.control}
name="releasedAt"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel className="mb-2">Released at</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant={'outline'}
size={'lg'}
className={cn(
'w-[240px] pl-3 text-left font-normal',
!field.value && 'text-muted-foreground',
)}
>
{field.value ? (
format(field.value, 'PPP')
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field.value as Date}
onSelect={(date) => field.onChange(date)}
weekStartsOn={1}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex gap-5">
<Button
type="button"
variant={'ghost'}
onClick={() =>
navigate({ to: '/changelog/$id', params: { id } })
}
>
Cancel
</Button>
<Button type="submit">Update</Button>
</div>
<ChangelogVersionDelete id={id} versionId={versionId} />
</CardContent>
</Card>
<div className="w-full">
<Card className="w-full max-w-screen-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Commits ({data.commits?.length})</CardTitle>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="assigned" className="w-full">
<TabsList>
<TabsTrigger value="assigned">Assigend</TabsTrigger>
<TabsTrigger value="unassigned">Unassigned</TabsTrigger>
</TabsList>
<TabsContent value="assigned">
<ScrollArea className="w-full h-[350px]">
<div className="flex flex-col gap-2">
{data?.commits?.map((commit) => {
return (
<FormField
key={commit.id}
control={form.control}
name={'commitIds'}
render={({ field }) => {
return (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
<FormControl>
<Checkbox
value={commit.id}
checked={field.value?.includes(
commit.id,
)}
onCheckedChange={() => {
const exist = field.value?.includes(
commit.id,
)
if (exist) {
return field.onChange(
field.value?.filter(
(value) =>
value !== commit.id,
),
)
}
return field.onChange([
...(field.value as string[]),
commit.id,
])
}}
/>
</FormControl>
<div className="space-y-1 leading-none w-full">
<FormLabel className="flex gap-2 w-full">
<span className="text-muted-foreground font-mono">
{commit.commit}{' '}
</span>
<span className="w-full">
{commit.subject}
</span>
<span>
{format(
new Date(commit.commiter.date),
'dd.MM.',
)}
</span>
</FormLabel>
</div>
</FormItem>
)
}}
/>
)
})}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="unassigned">
<ScrollArea className="w-full h-[350px]">
<div className="flex flex-col gap-2">
{commitResult.data?.map((commit) => {
return (
<FormField
key={commit.id}
control={form.control}
name={'commitIds'}
render={({ field }) => {
return (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
<FormControl>
<Checkbox
value={commit.id}
checked={field.value?.includes(
commit.id,
)}
onCheckedChange={() => {
const exist = field.value?.includes(
commit.id,
)
if (exist) {
return field.onChange(
field.value?.filter(
(value) =>
value !== commit.id,
),
)
}
return field.onChange([
...(field.value as string[]),
commit.id,
])
}}
/>
</FormControl>
<div className="space-y-1 leading-none w-full">
<FormLabel className="flex gap-2 w-full">
<span className="text-muted-foreground font-mono">
{commit.commit}
</span>
<span className="w-full">
{commit.subject}
</span>
<span>
{format(
new Date(commit.commiter.date),
'dd.MM.',
)}
</span>
</FormLabel>
</div>
</FormItem>
)
}}
/>
)
})}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</form>
</Form>
)}
</div>
)

BIN
bun.lockb

Binary file not shown.

View File

@ -1,4 +1,5 @@
import { z } from '@hono/zod-openapi'
import { CommitOutput } from '../commit'
export const VersionOutput = z
.object({
@ -20,5 +21,6 @@ export const VersionOutput = z
status: z.enum(['draft', 'review', 'published']).default('draft').openapi({
example: 'draft',
}),
commits: z.array(CommitOutput).optional(),
})
.openapi('Version')

View File

@ -11,6 +11,7 @@ export const VersionUpdateInput = z
.default('draft')
.optional(),
releasedAt: z.date().or(z.string()).optional().nullable(),
commitIds: z.array(z.string()).optional(),
})
.openapi({})
export const VersionUpdateParams = z

View File

@ -19,6 +19,7 @@
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",

View File

@ -30,3 +30,4 @@ export * from './command'
export * from './dialog'
export * from './scroll-area'
export * from './table'
export * from './tabs'

53
packages/ui/src/tabs.tsx Normal file
View File

@ -0,0 +1,53 @@
import * as TabsPrimitive from '@radix-ui/react-tabs'
import * as React from 'react'
import { cn } from './lib/cn'
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }