| @@ -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) | |||||
| current_snapshot = post_snapshot_from_record(post) | |||||
| base_snapshot = post_snapshot_from_version(base_version) | |||||
| 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 | |||||
| conflict_json = post_conflict_json(post:, | |||||
| base_version_no:, | |||||
| base_snapshot:, | |||||
| current_snapshot:, | |||||
| incoming_snapshot:) | |||||
| raise ActiveRecord::Rollback | |||||
| end | |||||
| PostVersionRecorder.ensure_snapshot!(post, created_by_user: current_user) | |||||
| post.update!(title:, original_created_from:, original_created_before:) | |||||
| 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) | |||||
| snapshot_to_apply = | |||||
| if post.version_no == base_version_no || force | |||||
| incoming_snapshot | |||||
| else | |||||
| changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot) | |||||
| conflicts = changes.select { |change| change[:conflict] } | |||||
| sync_parent_posts!(post, parent_post_ids) | |||||
| 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 | |||||
| PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user) | |||||
| apply_post_snapshot!(post, snapshot_to_apply) | |||||
| 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: | |||||
| changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot) | |||||
| conflicts = changes.select { |change| change[:conflict] } | |||||
| current_snapshot:, incoming_snapshot:, changes:, conflicts: | |||||
| { 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 | ||||
| @@ -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> | |||||
| <motion.div | |||||
| layout="position" | |||||
| transition={{ layout: { duration: .2, ease: 'easeOut' } }} | |||||
| className="flex flex-col h-dvh w-full overflow-y-hidden"> | |||||
| <TopNav user={user}/> | |||||
| <RouteTransitionWrapper user={user} setUser={setUser}/> | |||||
| </motion.div> | |||||
| </LayoutGroup> | |||||
| <Toaster/> | |||||
| <DialogueProvider> | |||||
| <LayoutGroup> | |||||
| <motion.div | |||||
| layout="position" | |||||
| transition={{ layout: { duration: .2, ease: 'easeOut' } }} | |||||
| className="flex flex-col h-dvh w-full overflow-y-hidden"> | |||||
| <TopNav user={user}/> | |||||
| <RouteTransitionWrapper user={user} setUser={setUser}/> | |||||
| </motion.div> | |||||
| </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 = | |||||
| await updatePost ({ id: post.id, versionNo: post.versionNo + 1, | |||||
| title, tags, parentPostIds, | |||||
| originalCreatedFrom, originalCreatedBefore }) | |||||
| const data = await updatePost (...args) | |||||
| 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, | |||||
| originalCreatedFrom, originalCreatedBefore }) | |||||
| await updatePost ({ id, title, tags, parentPostIds, | |||||
| originalCreatedFrom, originalCreatedBefore }, | |||||
| { force: true }) | |||||
| qc.invalidateQueries ({ queryKey: postsKeys.root }) | qc.invalidateQueries ({ queryKey: postsKeys.root }) | ||||
| qc.invalidateQueries ({ queryKey: tagsKeys.root }) | qc.invalidateQueries ({ queryKey: tagsKeys.root }) | ||||