This commit is contained in:
@@ -44,7 +44,7 @@ class PostsController < ApplicationController
|
|||||||
filtered_posts
|
filtered_posts
|
||||||
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
.joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id")
|
||||||
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
.reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all"))
|
||||||
.preload(tags: [:materials, { tag_name: :wiki_page }])
|
.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||||
.with_attached_thumbnail
|
.with_attached_thumbnail
|
||||||
|
|
||||||
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
q = q.where('posts.url LIKE ?', "%#{ url }%") if url
|
||||||
@@ -95,7 +95,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def random
|
def random
|
||||||
post = filtered_posts.preload(tags: [:materials, { tag_name: :wiki_page }])
|
post = filtered_posts.preload(tags: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||||
.order('RAND()')
|
.order('RAND()')
|
||||||
.first
|
.first
|
||||||
return head :not_found unless post
|
return head :not_found unless post
|
||||||
@@ -104,7 +104,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
post = Post.includes(tags: [:materials, { tag_name: :wiki_page }]).find_by(id: params[:id])
|
post = Post.includes(tags: [:deerjikists, :materials, { tag_name: :wiki_page }]).find_by(id: params[:id])
|
||||||
return head :not_found unless post
|
return head :not_found unless post
|
||||||
|
|
||||||
render json: PostRepr.base(post, current_user)
|
render json: PostRepr.base(post, current_user)
|
||||||
@@ -173,8 +173,12 @@ class PostsController < ApplicationController
|
|||||||
return head :unauthorized unless current_user
|
return head :unauthorized unless current_user
|
||||||
return head :forbidden unless current_user.gte_member?
|
return head :forbidden unless current_user.gte_member?
|
||||||
|
|
||||||
base_version_no = parse_base_version_no
|
|
||||||
force = truthy_param?(params[:force])
|
force = truthy_param?(params[:force])
|
||||||
|
merge = truthy_param?(params[:merge])
|
||||||
|
return head :bad_request if force && merge
|
||||||
|
|
||||||
|
base_version_no = nil
|
||||||
|
base_version_no = parse_base_version_no unless force
|
||||||
|
|
||||||
title = params[:title].presence
|
title = params[:title].presence
|
||||||
tag_names = params[:tags].to_s.split
|
tag_names = params[:tags].to_s.split
|
||||||
@@ -186,12 +190,17 @@ class PostsController < ApplicationController
|
|||||||
conflict_json = nil
|
conflict_json = nil
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
post = Post.find(params[:id].to_i)
|
post = Post.lock.find(params[:id].to_i)
|
||||||
|
|
||||||
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
base_version = nil
|
||||||
|
base_snapshot = nil
|
||||||
|
current_snapshot = nil
|
||||||
|
unless force
|
||||||
|
base_version = post.post_versions.find_by!(version_no: base_version_no)
|
||||||
|
|
||||||
base_snapshot = post_snapshot_from_version(base_version)
|
base_snapshot = post_snapshot_from_version(base_version)
|
||||||
current_snapshot = post_snapshot_from_record(post)
|
current_snapshot = post_snapshot_from_record(post)
|
||||||
|
end
|
||||||
incoming_snapshot = post_incoming_snapshot(post,
|
incoming_snapshot = post_incoming_snapshot(post,
|
||||||
title:,
|
title:,
|
||||||
original_created_from:,
|
original_created_from:,
|
||||||
@@ -199,29 +208,28 @@ class PostsController < ApplicationController
|
|||||||
tag_names:,
|
tag_names:,
|
||||||
parent_post_ids:)
|
parent_post_ids:)
|
||||||
|
|
||||||
if !(force) && post.version_no != base_version_no
|
snapshot_to_apply =
|
||||||
conflict_json = post_conflict_json(post:,
|
if post.version_no == base_version_no || force
|
||||||
base_version_no:,
|
incoming_snapshot
|
||||||
base_snapshot:,
|
else
|
||||||
current_snapshot:,
|
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
|
||||||
incoming_snapshot:)
|
conflicts = changes.select { |change| change[:conflict] }
|
||||||
raise ActiveRecord::Rollback
|
|
||||||
end
|
|
||||||
|
|
||||||
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
if merge && conflicts.empty?
|
||||||
|
merge_post_snapshots(base_snapshot, current_snapshot, incoming_snapshot)
|
||||||
|
else
|
||||||
|
conflict_json = post_conflict_json(post:,
|
||||||
|
base_version_no:,
|
||||||
|
base_snapshot:,
|
||||||
|
current_snapshot:,
|
||||||
|
incoming_snapshot:,
|
||||||
|
changes:,
|
||||||
|
conflicts:)
|
||||||
|
raise ActiveRecord::Rollback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
post.update!(title:, original_created_from:, original_created_before:)
|
apply_post_snapshot!(post, snapshot_to_apply)
|
||||||
|
|
||||||
normalised_tags = Tag.normalise_tags!(tag_names, with_tagme: false)
|
|
||||||
TagVersioning.record_tag_snapshots!(normalised_tags, created_by_user: current_user)
|
|
||||||
|
|
||||||
tags = post.tags.nico.to_a + normalised_tags
|
|
||||||
tags = Tag.expand_parent_tags(tags)
|
|
||||||
sync_post_tags!(post, tags)
|
|
||||||
|
|
||||||
sync_parent_posts!(post, parent_post_ids)
|
|
||||||
|
|
||||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return render json: conflict_json, status: :conflict if conflict_json
|
return render json: conflict_json, status: :conflict if conflict_json
|
||||||
@@ -253,7 +261,7 @@ class PostsController < ApplicationController
|
|||||||
pts = pts.where(post_id: id) if id.present?
|
pts = pts.where(post_id: id) if id.present?
|
||||||
pts = pts.where(tag_id:) if tag_id.present?
|
pts = pts.where(tag_id:) if tag_id.present?
|
||||||
pts = pts.includes(:post, :created_user, :deleted_user,
|
pts = pts.includes(:post, :created_user, :deleted_user,
|
||||||
tag: [:materials, { tag_name: :wiki_page }])
|
tag: [:deerjikists, :materials, { tag_name: :wiki_page }])
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
pts.each do |pt|
|
pts.each do |pt|
|
||||||
@@ -446,11 +454,11 @@ class PostsController < ApplicationController
|
|||||||
{ title: version.title,
|
{ title: version.title,
|
||||||
original_created_from: snapshot_time(version.original_created_from),
|
original_created_from: snapshot_time(version.original_created_from),
|
||||||
original_created_before: snapshot_time(version.original_created_before),
|
original_created_before: snapshot_time(version.original_created_before),
|
||||||
tag_names: version.tags.to_s.split.sort,
|
tag_names: version.tags.to_s.split.filter { !(_1.start_with?('nico:')) }.sort,
|
||||||
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
|
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_snapshot_form_record post
|
def post_snapshot_from_record post
|
||||||
{ title: post.title,
|
{ title: post.title,
|
||||||
original_created_from: snapshot_time(post.original_created_from),
|
original_created_from: snapshot_time(post.original_created_from),
|
||||||
original_created_before: snapshot_time(post.original_created_before),
|
original_created_before: snapshot_time(post.original_created_before),
|
||||||
@@ -460,7 +468,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
def post_incoming_snapshot post, title:, original_created_from:, original_created_before:,
|
def post_incoming_snapshot post, title:, original_created_from:, original_created_before:,
|
||||||
tag_names:, parent_post_ids:
|
tag_names:, parent_post_ids:
|
||||||
{ title:
|
{ title:,
|
||||||
original_created_from: snapshot_time(original_created_from),
|
original_created_from: snapshot_time(original_created_from),
|
||||||
original_created_before: snapshot_time(original_created_before),
|
original_created_before: snapshot_time(original_created_before),
|
||||||
tag_names: incoming_tag_names_for_snapshot(post, tag_names),
|
tag_names: incoming_tag_names_for_snapshot(post, tag_names),
|
||||||
@@ -488,17 +496,16 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
def incoming_tag_names_for_snapshot post, raw_tag_names
|
def incoming_tag_names_for_snapshot post, raw_tag_names
|
||||||
manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names)
|
manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names)
|
||||||
nico_names = post.tags.nico.joins(:tag_name).pluck('tag_names.name')
|
|
||||||
|
|
||||||
existing_tags =
|
existing_tags =
|
||||||
Tag
|
Tag
|
||||||
.joins(:tag_name)
|
.joins(:tag_name)
|
||||||
.where(tag_names: { name: manual_names + nico_names })
|
.where(tag_names: { name: manual_names })
|
||||||
.to_a
|
.to_a
|
||||||
|
|
||||||
expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name)
|
expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name)
|
||||||
|
|
||||||
(manual_names + nico_names + expanded_names).uniq.sort
|
(manual_names + expanded_names).uniq.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalised_manual_tag_names_for_snapshot raw_tag_names
|
def normalised_manual_tag_names_for_snapshot raw_tag_names
|
||||||
@@ -528,10 +535,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
||||||
current_snapshot:, incoming_snapshot:
|
current_snapshot:, incoming_snapshot:, changes:, conflicts:
|
||||||
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
|
|
||||||
conflicts = changes.select { |change| change[:conflict] }
|
|
||||||
|
|
||||||
{ error: 'conflict',
|
{ error: 'conflict',
|
||||||
message: '競合が発生しました.',
|
message: '競合が発生しました.',
|
||||||
post_id: post.id,
|
post_id: post.id,
|
||||||
@@ -548,9 +552,9 @@ class PostsController < ApplicationController
|
|||||||
def post_snapshot_changes base_snapshot, current_snapshot, incoming_snapshot
|
def post_snapshot_changes base_snapshot, current_snapshot, incoming_snapshot
|
||||||
[scalar_snapshot_change(:title, 'タイトル',
|
[scalar_snapshot_change(:title, 'タイトル',
|
||||||
base_snapshot, current_snapshot, incoming_snapshot),
|
base_snapshot, current_snapshot, incoming_snapshot),
|
||||||
scalar_snapshot_change(:original_created_from, '元コンテンツ作成日時(開始)',
|
scalar_snapshot_change(:original_created_from, 'オリジナルの作成日時(以降)',
|
||||||
base_snapshot, current_snapshot, incoming_snapshot),
|
base_snapshot, current_snapshot, incoming_snapshot),
|
||||||
scalar_snapshot_change(:original_created_before, '元コンテンツ作成日時(終了)',
|
scalar_snapshot_change(:original_created_before, 'オリジナルの作成日時(より前)',
|
||||||
base_snapshot, current_snapshot, incoming_snapshot),
|
base_snapshot, current_snapshot, incoming_snapshot),
|
||||||
set_snapshot_change(:tag_names, 'タグ',
|
set_snapshot_change(:tag_names, 'タグ',
|
||||||
base_snapshot, current_snapshot, incoming_snapshot),
|
base_snapshot, current_snapshot, incoming_snapshot),
|
||||||
@@ -606,4 +610,37 @@ class PostsController < ApplicationController
|
|||||||
added_by_me:, removed_by_me:
|
added_by_me:, removed_by_me:
|
||||||
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
(added_by_current & removed_by_me).present? || (removed_by_current & added_by_me).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def apply_post_snapshot! post, snapshot
|
||||||
|
PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user)
|
||||||
|
|
||||||
|
post.update!(title: snapshot[:title],
|
||||||
|
original_created_from: snapshot[:original_created_from],
|
||||||
|
original_created_before: snapshot[:original_created_before])
|
||||||
|
|
||||||
|
tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false)
|
||||||
|
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
||||||
|
|
||||||
|
tags = Tag.expand_parent_tags(tags)
|
||||||
|
sync_post_tags!(post, tags)
|
||||||
|
|
||||||
|
sync_parent_posts!(post, snapshot[:parent_post_ids])
|
||||||
|
|
||||||
|
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot
|
||||||
|
[:title, :original_created_from, :original_created_before, :tag_names, :parent_post_ids].map {
|
||||||
|
[_1, merge_scaler_snapshot_value(base_snapshot[_1],
|
||||||
|
current_snapshot[_1],
|
||||||
|
incoming_snapshot[_1])]
|
||||||
|
}.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_scaler_snapshot_value base, current, mine
|
||||||
|
return mine if current == base
|
||||||
|
return current if mine == base || current == mine
|
||||||
|
|
||||||
|
raise ArgumentError, '競合してゐる項目はマージできません.'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ class Tag < ApplicationRecord
|
|||||||
tn = tn.canonical if tn.canonical_id?
|
tn = tn.canonical if tn.canonical_id?
|
||||||
|
|
||||||
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
Tag.find_undiscard_or_create_by!(tag_name_id: tn.id) do |t|
|
||||||
|
t.version_no = TagVersion.where(tag_id: t.id).order(version_no: :desc).first || 1
|
||||||
t.category = category
|
t.category = category
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
|
|||||||
+15
-10
@@ -8,6 +8,7 @@ import { BrowserRouter,
|
|||||||
|
|
||||||
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
import RouteBlockerOverlay from '@/components/RouteBlockerOverlay'
|
||||||
import TopNav from '@/components/TopNav'
|
import TopNav from '@/components/TopNav'
|
||||||
|
import DialogueProvider from '@/components/dialogues/DialogueProvider'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
import { apiPost, isApiError } from '@/lib/api'
|
import { apiPost, isApiError } from '@/lib/api'
|
||||||
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
|
import DeerjikistDetailPage from '@/pages/deerjikists/DeerjikistDetailPage'
|
||||||
@@ -138,17 +139,21 @@ export default (() => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RouteBlockerOverlay/>
|
<RouteBlockerOverlay/>
|
||||||
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<LayoutGroup>
|
<DialogueProvider>
|
||||||
<motion.div
|
<LayoutGroup>
|
||||||
layout="position"
|
<motion.div
|
||||||
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
layout="position"
|
||||||
className="flex flex-col h-dvh w-full overflow-y-hidden">
|
transition={{ layout: { duration: .2, ease: 'easeOut' } }}
|
||||||
<TopNav user={user}/>
|
className="flex flex-col h-dvh w-full overflow-y-hidden">
|
||||||
<RouteTransitionWrapper user={user} setUser={setUser}/>
|
<TopNav user={user}/>
|
||||||
</motion.div>
|
<RouteTransitionWrapper user={user} setUser={setUser}/>
|
||||||
</LayoutGroup>
|
</motion.div>
|
||||||
<Toaster/>
|
</LayoutGroup>
|
||||||
|
|
||||||
|
<Toaster/>
|
||||||
|
</DialogueProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</>)
|
</>)
|
||||||
}) satisfies FC
|
}) satisfies FC
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
import PostFormTagsArea from '@/components/PostFormTagsArea'
|
||||||
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
import PostOriginalCreatedTimeField from '@/components/PostOriginalCreatedTimeField'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
|
import { useDialogue } from '@/components/dialogues/DialogueProvider'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
import { updatePost } from '@/lib/posts'
|
import { updatePost } from '@/lib/posts'
|
||||||
@@ -41,29 +42,68 @@ export default (({ post, onSave }: Props) => {
|
|||||||
const [tags, setTags] = useState<string> ('')
|
const [tags, setTags] = useState<string> ('')
|
||||||
const [title, setTitle] = useState (post.title)
|
const [title, setTitle] = useState (post.title)
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const dialogue = useDialogue ()
|
||||||
|
|
||||||
|
const update = async (...args: Parameters<typeof updatePost>) => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const data =
|
const data = await updatePost (...args)
|
||||||
await updatePost ({ id: post.id, versionNo: post.versionNo + 1,
|
|
||||||
title, tags, parentPostIds,
|
|
||||||
originalCreatedFrom, originalCreatedBefore })
|
|
||||||
onSave ({ ...post,
|
onSave ({ ...post,
|
||||||
|
versionNo: data.versionNo,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
tags: data.tags,
|
tags: data.tags,
|
||||||
parentPosts: data.parentPosts,
|
parentPosts: data.parentPosts,
|
||||||
childPosts: data.childPosts,
|
childPosts: data.childPosts,
|
||||||
siblingPosts: data.siblingPosts,
|
siblingPosts: data.siblingPosts,
|
||||||
originalCreatedFrom: data.originalCreatedFrom,
|
originalCreatedFrom: data.originalCreatedFrom,
|
||||||
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
originalCreatedBefore: data.originalCreatedBefore } as Post)
|
||||||
toast ({ description: '更新しました.' })
|
toast ({ description: '更新しました.' })
|
||||||
}
|
}
|
||||||
catch
|
catch (e)
|
||||||
{
|
{
|
||||||
toast ({ description: '更新はできなかったよ……' })
|
if (e.response.status !== 409)
|
||||||
|
{
|
||||||
|
toast ({ description: '更新はできなかったよ……' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = await dialogue.choice ({
|
||||||
|
title: '競合が発生しました.',
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<p>ほかの耕作員が先に更新してゐます.</p>
|
||||||
|
<p>現在の変更をどう扱ひますか?</p>
|
||||||
|
</div>),
|
||||||
|
choices: [{ value: 'merge', label: '差分をマージ' },
|
||||||
|
{ value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
|
||||||
|
|
||||||
|
if (action === 'merge')
|
||||||
|
{
|
||||||
|
// TODO: 差分 UI
|
||||||
|
await update ({ id: post.id, title, tags, parentPostIds,
|
||||||
|
originalCreatedFrom, originalCreatedBefore },
|
||||||
|
{ baseVersionNo: post.versionNo, merge: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'overwrite')
|
||||||
|
{
|
||||||
|
await update ({ id: post.id, title, tags, parentPostIds,
|
||||||
|
originalCreatedFrom, originalCreatedBefore },
|
||||||
|
{ baseVersionNo: post.versionNo, force: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async e => {
|
||||||
|
e.preventDefault ()
|
||||||
|
|
||||||
|
await update ({ id: post.id, title, tags, parentPostIds,
|
||||||
|
originalCreatedFrom, originalCreatedBefore },
|
||||||
|
{ baseVersionNo: post.versionNo })
|
||||||
|
}
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
setTags(tagsToStr (post.tags))
|
setTags(tagsToStr (post.tags))
|
||||||
}, [post])
|
}, [post])
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
import type { FC, ReactNode } from 'react'
|
||||||
|
|
||||||
|
type DialogueVariant = 'default' | 'danger'
|
||||||
|
|
||||||
|
type ConfirmOptions = { title: string
|
||||||
|
description?: ReactNode
|
||||||
|
confirmText?: string
|
||||||
|
cancelText?: string
|
||||||
|
variant?: DialogueVariant }
|
||||||
|
|
||||||
|
type AlertOptions = { title: string
|
||||||
|
description?: ReactNode
|
||||||
|
okText?: string }
|
||||||
|
|
||||||
|
type Choice<T extends string> = { value: T
|
||||||
|
label: string
|
||||||
|
variant?: DialogueVariant }
|
||||||
|
|
||||||
|
type ChoiceOptions<T extends string> = { title: string
|
||||||
|
description?: ReactNode
|
||||||
|
choices: Choice<T>[]
|
||||||
|
cancelText?: string }
|
||||||
|
|
||||||
|
type DialogueRequest =
|
||||||
|
| { id: number
|
||||||
|
kind: 'confirm'
|
||||||
|
options: ConfirmOptions
|
||||||
|
resolve: (value: boolean) => void }
|
||||||
|
| { id: number
|
||||||
|
kind: 'alert'
|
||||||
|
options: AlertOptions
|
||||||
|
resolve: () => void }
|
||||||
|
| { id: number
|
||||||
|
kind: 'choice'
|
||||||
|
options: ChoiceOptions<string>
|
||||||
|
resolve: (value: string | null) => void }
|
||||||
|
|
||||||
|
type DialogueAPI =
|
||||||
|
{ confirm: (options: ConfirmOptions) => Promise<boolean>
|
||||||
|
alert: (options: AlertOptions) => Promise<void>
|
||||||
|
choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
|
||||||
|
|
||||||
|
const DialogueContext = createContext<DialogueAPI | null> (null)
|
||||||
|
|
||||||
|
let nextDialogueId = 1
|
||||||
|
|
||||||
|
type Props = { children: ReactNode }
|
||||||
|
|
||||||
|
|
||||||
|
export default (({ children }: Props) => {
|
||||||
|
const [queue, setQueue] = useState<DialogueRequest[]> ([])
|
||||||
|
|
||||||
|
const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
|
||||||
|
const id = nextDialogueId
|
||||||
|
++nextDialogueId
|
||||||
|
|
||||||
|
setQueue (q => [...q, { ...request, id } as DialogueRequest])
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const closeActive = useCallback ((result?: unknown) => {
|
||||||
|
setQueue (q => {
|
||||||
|
const [active, ...rest] = q
|
||||||
|
|
||||||
|
if (!(active))
|
||||||
|
return rest
|
||||||
|
|
||||||
|
switch (active.kind)
|
||||||
|
{
|
||||||
|
case 'confirm':
|
||||||
|
active.resolve (Boolean (result))
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'alert':
|
||||||
|
active.resolve ()
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'choice':
|
||||||
|
active.resolve ((result ?? null) as string | null)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const api = useMemo<DialogueAPI> (() => ({
|
||||||
|
confirm: options => new Promise<boolean> (resolve => {
|
||||||
|
push ({ kind: 'confirm', options, resolve })
|
||||||
|
}),
|
||||||
|
alert: options => new Promise<void> (resolve => {
|
||||||
|
push ({ kind: 'alert', options, resolve })
|
||||||
|
}),
|
||||||
|
choice: options => new Promise (resolve => {
|
||||||
|
push ({ kind: 'choice',
|
||||||
|
options: options as ChoiceOptions<string>,
|
||||||
|
resolve: resolve as (value: string | null) => void })
|
||||||
|
}) }), [push])
|
||||||
|
|
||||||
|
const active = queue[0]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogueContext.Provider value={api}>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={Boolean (active)}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (!(open))
|
||||||
|
closeActive (active?.kind !== 'confirm' && null)
|
||||||
|
}}>
|
||||||
|
{active && (
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>{active.options.title}</DialogTitle>
|
||||||
|
|
||||||
|
{active.options.description && (
|
||||||
|
<DialogDescription asChild>
|
||||||
|
<div>{active.options.description}</div>
|
||||||
|
</DialogDescription>)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{active.kind === 'confirm' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => closeActive (false)}>
|
||||||
|
{active.options.cancelText ?? '取消'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={(active.options.variant === 'danger')
|
||||||
|
? 'destructive'
|
||||||
|
: 'default'}
|
||||||
|
onClick={() => closeActive (true)}>
|
||||||
|
{active.options.confirmText ?? '確定'}
|
||||||
|
</Button>
|
||||||
|
</>)}
|
||||||
|
|
||||||
|
{active.kind === 'alert' && (
|
||||||
|
<Button onClick={() => closeActive ()}>
|
||||||
|
{active.options.okText ?? '確定'}
|
||||||
|
</Button>)}
|
||||||
|
|
||||||
|
{active.kind === 'choice' && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => closeActive (null)}>
|
||||||
|
{active.options.cancelText ?? '取消'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{active.options.choices.map (choice => (
|
||||||
|
<Button
|
||||||
|
key={choice.value}
|
||||||
|
variant={(choice.variant === 'danger')
|
||||||
|
? 'destructive'
|
||||||
|
: 'default'}
|
||||||
|
onClick={() => closeActive (choice.value)}>
|
||||||
|
{choice.label}
|
||||||
|
</Button>))}
|
||||||
|
</>)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>)}
|
||||||
|
</Dialog>
|
||||||
|
</DialogueContext.Provider>)
|
||||||
|
}) satisfies FC<Props>
|
||||||
|
|
||||||
|
|
||||||
|
export const useDialogue = () => {
|
||||||
|
const dialogue = useContext (DialogueContext)
|
||||||
|
|
||||||
|
if (!(dialogue))
|
||||||
|
throw new Error ('useDialogue must be used inside DialogueProvider')
|
||||||
|
|
||||||
|
return dialogue
|
||||||
|
}
|
||||||
@@ -44,21 +44,26 @@ export const fetchPostChanges = async (
|
|||||||
|
|
||||||
export const updatePost = async (
|
export const updatePost = async (
|
||||||
post: { id: number
|
post: { id: number
|
||||||
versionNo: number
|
|
||||||
title: string | null
|
title: string | null
|
||||||
tags: string
|
tags: string
|
||||||
parentPostIds: string
|
parentPostIds: string
|
||||||
originalCreatedFrom: string | null
|
originalCreatedFrom: string | null
|
||||||
originalCreatedBefore: string | null },
|
originalCreatedBefore: string | null },
|
||||||
|
{ baseVersionNo, force, merge }: {
|
||||||
|
baseVersionNo?: number
|
||||||
|
force?: boolean
|
||||||
|
merge?: boolean }
|
||||||
) =>
|
) =>
|
||||||
await apiPut<Post> (
|
await apiPut<Post> (
|
||||||
`/posts/${ post.id }`,
|
`/posts/${ post.id }`,
|
||||||
{ version_no: post.versionNo,
|
{ title: post.title,
|
||||||
title: post.title,
|
|
||||||
tags: post.tags,
|
tags: post.tags,
|
||||||
parent_post_ids: post.parentPostIds,
|
parent_post_ids: post.parentPostIds,
|
||||||
original_created_from: post.originalCreatedFrom,
|
original_created_from: post.originalCreatedFrom,
|
||||||
original_created_before: post.originalCreatedBefore })
|
original_created_before: post.originalCreatedBefore },
|
||||||
|
{ params: { ...(baseVersionNo && { base_version_no: String (baseVersionNo) }),
|
||||||
|
force: force ? '1' : '0',
|
||||||
|
merge: merge ? '1' : '0' } })
|
||||||
|
|
||||||
|
|
||||||
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
|
export const toggleViewedFlg = async (id: string, viewed: boolean): Promise<void> => {
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export default (() => {
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
const id = change.postId
|
const id = change.postId
|
||||||
const versionNo = change.latestVersionNo + 1
|
|
||||||
const title = change.title.current
|
const title = change.title.current
|
||||||
const tags =
|
const tags =
|
||||||
change.tags
|
change.tags
|
||||||
@@ -88,8 +87,9 @@ export default (() => {
|
|||||||
.join (' ')
|
.join (' ')
|
||||||
const originalCreatedFrom = change.originalCreatedFrom.current
|
const originalCreatedFrom = change.originalCreatedFrom.current
|
||||||
const originalCreatedBefore = change.originalCreatedBefore.current
|
const originalCreatedBefore = change.originalCreatedBefore.current
|
||||||
await updatePost ({ id, versionNo, title, tags, parentPostIds,
|
await updatePost ({ id, title, tags, parentPostIds,
|
||||||
originalCreatedFrom, originalCreatedBefore })
|
originalCreatedFrom, originalCreatedBefore },
|
||||||
|
{ force: true })
|
||||||
|
|
||||||
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
qc.invalidateQueries ({ queryKey: postsKeys.root })
|
||||||
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
qc.invalidateQueries ({ queryKey: tagsKeys.root })
|
||||||
|
|||||||
Reference in New Issue
Block a user