Browse Source

#171

feature/171
みてるぞ 2 weeks ago
parent
commit
35e5af2f9a
8 changed files with 79 additions and 55 deletions
  1. +7
    -5
      backend/app/controllers/posts_controller.rb
  2. +3
    -3
      backend/app/services/version_recorder.rb
  3. +13
    -9
      frontend/src/components/PostEditForm.tsx
  4. +8
    -8
      frontend/src/components/PostFormTagsArea.tsx
  5. +2
    -2
      frontend/src/components/common/DateTimeField.tsx
  6. +10
    -7
      frontend/src/components/dialogues/DialogueProvider.tsx
  7. +29
    -16
      frontend/src/components/ui/button.tsx
  8. +7
    -5
      frontend/src/components/ui/dialog.tsx

+ 7
- 5
backend/app/controllers/posts_controller.rb 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 unless force
base_version_no = parse_base_version_no
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?

version_no
if version_no&.positive?
version_no
else
nil
end
end end


def post_snapshot_from_version version def post_snapshot_from_version version


+ 3
- 3
backend/app/services/version_recorder.rb 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
frontend/src/components/PostEditForm.tsx 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,
originalCreatedFrom, originalCreatedBefore },
{ baseVersionNo: post.versionNo })
setDisabled (false)
try
{
await update ({ id: post.id, title, tags, parentPostIds,
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}
onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
disabled={disabled}>
更新 更新
</Button> </Button>
</div>)
</form>)
}) satisfies FC<Props> }) satisfies FC<Props>

+ 8
- 8
frontend/src/components/PostFormTagsArea.tsx 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 =
& { tags: string
setTags: (tags: string) => void }
& ComponentPropsWithoutRef<'textarea'>
type Props = Omit<ComponentPropsWithoutRef<'textarea'>, 'value' | 'onChange' | 'onBlur'> & {
tags: string
setTags: (tags: string) => void }




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)
}}
{...rest}/>
onBlur?.(ev)
}}/>
{focused && ( {focused && (
<TagSearchBox <TagSearchBox
suggestions={suggestionsVsbl && suggestions.length > 0 suggestions={suggestionsVsbl && suggestions.length > 0


+ 2
- 2
frontend/src/components/common/DateTimeField.tsx View File

@@ -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}
{...rest}/>)
onBlur={onBlur}/>)
}) satisfies FC<Props> }) satisfies FC<Props>

+ 10
- 7
frontend/src/components/dialogues/DialogueProvider.tsx View File

@@ -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>
<DialogTitle>{active.options.title}</DialogTitle>

{active.options.description && (
<DialogDescription asChild>
<div>{active.options.description}</div>
</DialogDescription>)}
<DialogContent className="px-6 pb-6 pt-7">
<DialogHeader className="pl-8">
<DialogTitle>{active.options.title}</DialogTitle>

{active.options.description && (
<DialogDescription asChild>
<div>{active.options.description}</div>
</DialogDescription>)}
</DialogHeader>


<DialogFooter> <DialogFooter>
{active.kind === 'confirm' && ( {active.kind === 'confirm' && (


+ 29
- 16
frontend/src/components/ui/button.tsx 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(
"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",
const buttonVariants = cva (
[
'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",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',

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",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
}, },
}, },
defaultVariants: { defaultVariants: {
variant: "default",
size: "default",
variant: 'default',
size: 'default',
}, },
}
)
})


export interface ButtonProps export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>, extends React.ButtonHTMLAttributes<HTMLButtonElement>,


+ 7
- 5
frontend/src/components/ui/dialog.tsx 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',
'text-muted-foreground opacity-70 transition-opacity',
'hover:bg-accent hover:text-accent-foreground hover:opacity-100',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2')}>
'absolute left-4 top-4 rounded-full p-1',
'text-slate-500 transition-colors',
'hover:bg-slate-200 hover:text-slate-900',
'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>


Loading…
Cancel
Save