This commit is contained in:
2026-05-10 05:32:08 +09:00
parent 5b50642756
commit 35e5af2f9a
8 changed files with 78 additions and 54 deletions
+7 -5
View File
@@ -177,8 +177,8 @@ class PostsController < ApplicationController
merge = bool?(:merge) merge = bool?(:merge)
return head :bad_request if force && merge return head :bad_request if force && merge
base_version_no = nil base_version_no = parse_base_version_no
base_version_no = parse_base_version_no unless force return head :bad_request if !(force) && !(base_version_no)
title = params[:title].presence title = params[:title].presence
tag_names = params[:tags].to_s.split tag_names = params[:tags].to_s.split
@@ -442,9 +442,11 @@ class PostsController < ApplicationController
def parse_base_version_no def parse_base_version_no
version_no = Integer(params[:base_version_no], exception: false) version_no = Integer(params[:base_version_no], exception: false)
raise ArgumentError, 'base_version_no は必須です.' unless version_no&.positive? if version_no&.positive?
version_no
version_no else
nil
end
end end
def post_snapshot_from_version version def post_snapshot_from_version version
+3 -3
View File
@@ -16,7 +16,7 @@ class VersionRecorder
@record = record_class.unscoped.lock.find(@record.id) @record = record_class.unscoped.lock.find(@record.id)
latest = latest_version latest = latest_version
validate_version_sequence! latest validate_version_sequence!(latest)
attrs = snapshot_attributes attrs = snapshot_attributes
@@ -27,7 +27,7 @@ class VersionRecorder
version = version_class.create!( version = version_class.create!(
base_attributes(latest).merge(record_key => @record).merge(attrs)) base_attributes(latest).merge(record_key => @record).merge(attrs))
update_record_version_no! version.version_no update_record_version_no!(version.version_no)
version version
end end
@@ -47,7 +47,7 @@ class VersionRecorder
end end
def update_record_version_no! version_no def update_record_version_no! version_no
@record.update_columns version_no: version_no @record.update_columns(version_no:)
@record.version_no = version_no @record.version_no = version_no
end end
+13 -9
View File
@@ -103,10 +103,16 @@ export default (({ post, onSave }: Props) => {
e.preventDefault () e.preventDefault ()
setDisabled (true) setDisabled (true)
await update ({ id: post.id, title, tags, parentPostIds, try
originalCreatedFrom, originalCreatedBefore }, {
{ baseVersionNo: post.versionNo }) await update ({ id: post.id, title, tags, parentPostIds,
setDisabled (false) originalCreatedFrom, originalCreatedBefore },
{ baseVersionNo: post.versionNo })
}
finally
{
setDisabled (false)
}
} }
useEffect (() => { useEffect (() => {
@@ -114,7 +120,7 @@ export default (({ post, onSave }: Props) => {
}, [post]) }, [post])
return ( return (
<div className="max-w-xl pt-2 space-y-4"> <form onSubmit={handleSubmit} className="max-w-xl pt-2 space-y-4">
{/* タイトル */} {/* タイトル */}
<div> <div>
<Label></Label> <Label></Label>
@@ -154,10 +160,8 @@ export default (({ post, onSave }: Props) => {
{/* 送信 */} {/* 送信 */}
<Button <Button
type="submit" type="submit"
disabled={disabled} disabled={disabled}>
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
</Button> </Button>
</div>) </form>)
}) satisfies FC<Props> }) satisfies FC<Props>
+8 -8
View File
@@ -31,13 +31,12 @@ const replaceToken = (value: string, start: number, end: number, text: string) =
`${ value.slice (0, start) }${ text }${ value.slice (end) }` `${ value.slice (0, start) }${ text }${ value.slice (end) }`
type Props = type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | 'onBlur'> & {
& { tags: string tags: string
setTags: (tags: string) => void } setTags: (tags: string) => void }
& ComponentPropsWithoutRef<'textarea'>
export default (({ tags, setTags, ...rest }: Props) => { export default (({ tags, setTags, onBlur, ...rest }: Props) => {
const ref = useRef<HTMLTextAreaElement> (null) const ref = useRef<HTMLTextAreaElement> (null)
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 }) const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
@@ -77,6 +76,7 @@ export default (({ tags, setTags, ...rest }: Props) => {
<div className="relative w-full"> <div className="relative w-full">
<Label></Label> <Label></Label>
<TextArea <TextArea
{...rest}
ref={ref} ref={ref}
value={tags} value={tags}
onChange={ev => setTags (ev.target.value)} onChange={ev => setTags (ev.target.value)}
@@ -85,11 +85,11 @@ export default (({ tags, setTags, ...rest }: Props) => {
await recompute (pos) await recompute (pos)
}} }}
onFocus={() => setFocused (true)} onFocus={() => setFocused (true)}
onBlur={() => { onBlur={ev => {
setFocused (false) setFocused (false)
setSuggestionsVsbl (false) setSuggestionsVsbl (false)
}} onBlur?.(ev)
{...rest}/> }}/>
{focused && ( {focused && (
<TagSearchBox <TagSearchBox
suggestions={suggestionsVsbl && suggestions.length > 0 suggestions={suggestionsVsbl && suggestions.length > 0
@@ -34,6 +34,7 @@ export default (({ value, onChange, className, onBlur, ...rest }: Props) => {
return ( return (
<input <input
{...rest}
className={cn ('border rounded p-2', className)} className={cn ('border rounded p-2', className)}
type="datetime-local" type="datetime-local"
value={local} value={local}
@@ -42,6 +43,5 @@ export default (({ value, onChange, className, onBlur, ...rest }: Props) => {
setLocal (v) setLocal (v)
onChange?.(v ? (new Date (v)).toISOString () : null) onChange?.(v ? (new Date (v)).toISOString () : null)
}} }}
onBlur={onBlur} onBlur={onBlur}/>)
{...rest}/>)
}) satisfies FC<Props> }) satisfies FC<Props>
@@ -5,6 +5,7 @@ import { Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogFooter, DialogFooter,
DialogHeader,
DialogTitle } from '@/components/ui/dialog' DialogTitle } from '@/components/ui/dialog'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
@@ -118,13 +119,15 @@ export default (({ children }: Props) => {
closeActive (active?.kind !== 'confirm' && null) closeActive (active?.kind !== 'confirm' && null)
}}> }}>
{active && ( {active && (
<DialogContent> <DialogContent className="px-6 pb-6 pt-7">
<DialogTitle>{active.options.title}</DialogTitle> <DialogHeader className="pl-8">
<DialogTitle>{active.options.title}</DialogTitle>
{active.options.description && ( {active.options.description && (
<DialogDescription asChild> <DialogDescription asChild>
<div>{active.options.description}</div> <div>{active.options.description}</div>
</DialogDescription>)} </DialogDescription>)}
</DialogHeader>
<DialogFooter> <DialogFooter>
{active.kind === 'confirm' && ( {active.kind === 'confirm' && (
+29 -16
View File
@@ -4,34 +4,47 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva (
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", [
'inline-flex items-center justify-center gap-2 whitespace-nowrap',
'rounded-md text-sm font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400',
'disabled:pointer-events-none disabled:opacity-50',
'[&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
].join (' '),
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default:
'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-300',
destructive: destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90", 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-600',
outline: outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground", 'border border-slate-300 bg-white text-slate-900 hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800',
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline", ghost:
'text-slate-900 hover:bg-slate-100 dark:text-slate-100 dark:hover:bg-slate-800',
link:
'text-blue-700 underline-offset-4 hover:underline dark:text-blue-300',
}, },
size: { size: {
default: "h-10 px-4 py-2", default: 'h-10 px-4 py-2',
sm: "h-9 rounded-md px-3", sm: 'h-9 rounded-md px-3',
lg: "h-11 rounded-md px-8", lg: 'h-11 rounded-md px-8',
icon: "h-10 w-10", icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default", variant: 'default',
size: "default", size: 'default',
}, },
} })
)
export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+7 -5
View File
@@ -50,14 +50,16 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {children}
<DialogPrimitive.Close <DialogPrimitive.Close
className={cn ( className={cn (
'absolute right-4 top-4 rounded-full p-1', 'absolute left-4 top-4 rounded-full p-1',
'text-muted-foreground opacity-70 transition-opacity', 'text-slate-500 transition-colors',
'hover:bg-accent hover:text-accent-foreground hover:opacity-100', 'hover:bg-slate-200 hover:text-slate-900',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2')}> 'dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-50',
'focus:outline-none focus:ring-2 focus:ring-slate-400')}>
<X className="h-4 w-4"/> <X className="h-4 w-4"/>
<span className="sr-only">Close</span> <span className="sr-only"></span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>
</DialogPortal> </DialogPortal>