feat: add version create with commit select

This commit is contained in:
Lars Hampe 2024-10-29 21:25:21 +01:00
parent 5467e78596
commit 625b463287
15 changed files with 500 additions and 198 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 { VersionCreateInput, VersionCreateOutput } from '@boring.tools/schema'
import { createRoute, type z } from '@hono/zod-openapi'
import { and, eq } from 'drizzle-orm'
import { and, eq, inArray } from 'drizzle-orm'
import { HTTPException } from 'hono/http-exception'
import semver from 'semver'
@ -84,5 +89,10 @@ export const createFunc = async ({
})
.returning()
await db
.update(changelog_commit)
.set({ versionId: versionCreateResult.id })
.where(inArray(changelog_commit.id, payload.commitIds))
return versionCreateResult
}

View File

@ -0,0 +1,44 @@
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
import { useParams } from '@tanstack/react-router'
import { format } from 'date-fns'
import { useChangelogCommitList } from '../../hooks/useChangelog'
export const ChangelogCommitList = () => {
const { id } = useParams({ from: '/changelog/$id' })
const { data } = useChangelogCommitList({ id, limit: 50 })
if (data) {
return (
<Card className="w-full max-w-screen-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Commits ({data.length})</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
{data?.map((commit) => {
return (
<div
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
key={commit.id}
>
<span className="font-mono font-bold text-muted-foreground">
{commit.commit}
</span>
<p className="w-full">{commit.subject}</p>
<span className="text-xs">
{format(new Date(commit.author.date), 'dd.MM.yyyy')}
</span>
</div>
)
})}
</div>
</CardContent>
</Card>
)
}
return <div className="flex flex-col gap-5">Not found </div>
}

View File

@ -0,0 +1,25 @@
import { Link, useParams } from '@tanstack/react-router'
import { HandIcon, WorkflowIcon } from 'lucide-react'
export const ChangelogVersionCreateStep01 = () => {
const { id } = useParams({ from: '/changelog/$id' })
return (
<div className="flex gap-10 mt-3">
<div className="border rounded border-muted p-5 flex items-center justify-center w-full flex-col">
<WorkflowIcon />
Automatic
<small className="uppercase text-muted-foreground text-xs">
Coming soon
</small>
</div>
<Link
className="flex-col hover:border-accent border rounded border-muted p-5 flex items-center justify-center w-full"
to="/changelog/$id/versionCreate"
params={{ id }}
>
<HandIcon />
Manual
</Link>
</div>
)
}

View File

@ -0,0 +1,32 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@boring.tools/ui'
import { PlusCircleIcon } from 'lucide-react'
import { ChangelogVersionCreateStep01 } from './Step01'
export const ChangelogVersionCreate = () => {
return (
<Dialog>
<DialogTrigger>
<PlusCircleIcon />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>How would you like to create your version?</DialogTitle>
<DialogDescription>
You can create your version manually. You have to make every entry
yourself. However, if you want to create your changelog attachment
from your commit messages, select the automatic option.
</DialogDescription>
</DialogHeader>
<ChangelogVersionCreateStep01 />
</DialogContent>
</Dialog>
)
}

View File

@ -0,0 +1,46 @@
import { Card, CardContent, CardHeader, CardTitle } from '@boring.tools/ui'
import { Link, useParams } from '@tanstack/react-router'
import { useChangelogById } from '../../hooks/useChangelog'
import { ChangelogVersionCreate } from './Version/Create'
import { VersionStatus } from './VersionStatus'
export const ChangelogVersionList = () => {
const { id } = useParams({ from: '/changelog/$id' })
const { data } = useChangelogById({ id })
if (data) {
return (
<Card className="w-full max-w-screen-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Versions ({data.versions?.length})</CardTitle>
<ChangelogVersionCreate />
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
{data.versions?.map((version) => {
return (
<Link
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
to="/changelog/$id/version/$versionId"
params={{
id,
versionId: version.id,
}}
key={version.id}
>
<VersionStatus status={version.status} />
{version.version}
</Link>
)
})}
</div>
</CardContent>
</Card>
)
}
return <div className="flex flex-col gap-5">Not found</div>
}

View File

@ -2,6 +2,7 @@ import type {
ChangelogCreateInput,
ChangelogOutput,
ChangelogUpdateInput,
CommitOutput,
VersionCreateInput,
VersionOutput,
VersionUpdateInput,
@ -19,6 +20,8 @@ type Version = z.infer<typeof VersionOutput>
type VersionCreate = z.infer<typeof VersionCreateInput>
type VersionUpdate = z.infer<typeof VersionUpdateInput>
type Commit = z.infer<typeof CommitOutput>
export const useChangelogList = () => {
const { getToken } = useAuth()
return useQuery({
@ -32,6 +35,23 @@ export const useChangelogList = () => {
})
}
export const useChangelogCommitList = ({
id,
limit,
offset,
}: { id: string; limit?: number; offset?: number }) => {
const { getToken } = useAuth()
return useQuery({
queryKey: ['changelogCommitList'],
queryFn: async (): Promise<ReadonlyArray<Commit>> =>
await queryFetch({
path: `changelog/commit?changelogId=${id}&limit=${limit}&offset=${offset}`,
method: 'get',
token: await getToken(),
}),
})
}
export const useChangelogById = ({ id }: { id: string }) => {
const { getToken } = useAuth()

View File

@ -1,58 +1,12 @@
import {
Button,
Card,
CardContent,
CardHeader,
CardTitle,
} from '@boring.tools/ui'
import { Link, createLazyFileRoute } from '@tanstack/react-router'
import { PlusCircleIcon } from 'lucide-react'
import { VersionStatus } from '../components/Changelog/VersionStatus'
import { useChangelogById } from '../hooks/useChangelog'
import { createLazyFileRoute } from '@tanstack/react-router'
import { ChangelogCommitList } from '../components/Changelog/CommitList'
import { ChangelogVersionList } from '../components/Changelog/VersionList'
const Component = () => {
const { id } = Route.useParams()
const { data, isPending } = useChangelogById({ id })
return (
<div className="flex flex-col gap-5">
{!isPending && data && (
<div>
<Card className="w-full max-w-screen-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Versions ({data.versions?.length})</CardTitle>
<Link to="/changelog/$id/versionCreate" params={{ id }}>
<Button variant={'ghost'} size={'icon'}>
<PlusCircleIcon strokeWidth={1.5} className="w-5 h-5" />
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-1">
{data.versions?.map((version) => {
return (
<Link
className="hover:bg-muted py-1 px-2 rounded transition flex gap-2 items-center"
to="/changelog/$id/version/$versionId"
params={{
id,
versionId: version.id,
}}
key={version.id}
>
<VersionStatus status={version.status} />
{version.version}
</Link>
)
})}
</div>
</CardContent>
</Card>
</div>
)}
<div className="flex gap-5 flex-wrap">
<ChangelogVersionList />
<ChangelogCommitList />
</div>
)
}

View File

@ -2,8 +2,14 @@ import { VersionCreateInput } from '@boring.tools/schema'
import {
Button,
Calendar,
Card,
CardContent,
CardHeader,
CardTitle,
Checkbox,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@ -12,6 +18,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
ScrollArea,
Select,
SelectContent,
SelectItem,
@ -37,7 +44,10 @@ import { createLazyFileRoute } from '@tanstack/react-router'
import { useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import type { z } from 'zod'
import { useChangelogVersionCreate } from '../hooks/useChangelog'
import {
useChangelogCommitList,
useChangelogVersionCreate,
} from '../hooks/useChangelog'
import '@mdxeditor/editor/style.css'
import { format } from 'date-fns'
import { CalendarIcon } from 'lucide-react'
@ -46,7 +56,9 @@ import { VersionStatus } from '../components/Changelog/VersionStatus'
const Component = () => {
const { id } = Route.useParams()
const navigate = useNavigate({ from: `/changelog/${id}/versionCreate` })
const changelogCommit = useChangelogCommitList({ id })
const versionCreate = useChangelogVersionCreate()
const { data } = useChangelogCommitList({ id })
const form = useForm<z.infer<typeof VersionCreateInput>>({
resolver: zodResolver(VersionCreateInput),
defaultValues: {
@ -54,9 +66,18 @@ const Component = () => {
version: '',
markdown: '',
status: 'draft',
commitIds: [],
},
})
const selectAllCommits = () => {
const commitIds = data?.map((commit) => commit.id)
if (!commitIds) {
return form.setValue('commitIds', [])
}
form.setValue('commitIds', commitIds)
}
const onSubmit = (values: z.infer<typeof VersionCreateInput>) => {
versionCreate.mutate(values, {
onSuccess(data) {
@ -66,149 +87,213 @@ const Component = () => {
}
return (
<div className="flex flex-col gap-5">
<Separator />
<h1 className="text-xl mb-2">New version</h1>
<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} autoFocus />
</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={field.value}
plugins={[
headingsPlugin(),
listsPlugin(),
thematicBreakPlugin(),
quotePlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<BlockTypeSelect />
<BoldItalicUnderlineToggles />
<ListsToggle />
<UndoRedo />
</>
),
}),
]}
{...field}
/>
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
<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}
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full">
<h1 className="text-2xl">New version</h1>
<div className="grid grid-cols-6 gap-5 w-full max-w-screen-xl">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Details</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-5">
<FormField
control={form.control}
name="version"
render={({ field }) => (
<FormItem>
<FormLabel>Version</FormLabel>
<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>
)}
/>
<Input placeholder="v1.0.1" {...field} autoFocus />
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
<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}
<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>
)}
/>
<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>
<FormField
control={form.control}
name="markdown"
render={({ field }) => (
<FormItem>
<FormLabel>Changes</FormLabel>
<FormControl>
<MDXEditor
className="dark-theme h-56"
contentEditableClassName="prose dark:prose-invert max-w-none"
markdown={field.value}
plugins={[
headingsPlugin(),
listsPlugin(),
thematicBreakPlugin(),
quotePlugin(),
toolbarPlugin({
toolbarContents: () => (
<>
<BlockTypeSelect />
<BoldItalicUnderlineToggles />
<ListsToggle />
<UndoRedo />
</>
),
}),
]}
{...field}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>
</div>
</FormControl>{' '}
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<div className="flex gap-5">
<Card className="col-span-2">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Associated commits</CardTitle>
<Button
variant={'ghost'}
size={'sm'}
onClick={selectAllCommits}
>
Add all commits
</Button>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="w-full h-[350px]">
<div className="flex flex-col gap-2">
{changelogCommit.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
// checked={field.value}
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,
commit.id,
])
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>{commit.subject}</FormLabel>
</div>
</FormItem>
)
}}
/>
)
})}
</div>
</ScrollArea>
</CardContent>
</Card>
<div className="flex gap-5 mt-5 w-full justify-end items-end col-span-6">
<Button
type="button"
variant={'ghost'}
@ -218,9 +303,9 @@ const Component = () => {
</Button>
<Button type="submit">Create</Button>
</div>
</form>
</Form>
</div>
</div>
</form>
</Form>
)
}

View File

@ -11,7 +11,7 @@ post {
}
auth:bearer {
token: abc123
token: asd123
}
body:json {

View File

@ -0,0 +1,37 @@
meta {
name: List
type: http
seq: 2
}
get {
url: {{API_URL}}/v1/changelog/commit?changelogId=d83fe688-3331-4e64-9af6-318f82e511d4&limit=1
body: none
auth: bearer
}
params:query {
changelogId: d83fe688-3331-4e64-9af6-318f82e511d4
limit: 1
}
auth:bearer {
token: asd123
}
body:json {
[
{
"changelogId": "6a14f436-6596-474b-b615-f6e923582c1b",
"commit": "abc123",
"parent": "abc122",
"subject": "some",
"author": {
"name": "asd",
"email": "hello@hashdot.co",
"date": "somedate"
},
"body": ""
}
]
}

BIN
bun.lockb

Binary file not shown.

View File

@ -9,6 +9,7 @@ export const VersionCreateInput = z
releasedAt: z.date().or(z.string()).optional(),
status: z.enum(['draft', 'review', 'published']).default('draft'),
markdown: z.string(),
commitIds: z.array(z.string()),
})
.openapi({
required: ['changelogId', 'version', 'markdown', 'releasedAt'],

View File

@ -15,6 +15,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",

View File

@ -28,3 +28,4 @@ export * from './global.css'
export * from './breadcrumb'
export * from './command'
export * from './dialog'
export * from './scroll-area'

View File

@ -0,0 +1,46 @@
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import * as React from 'react'
import { cn } from './lib/cn'
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }