This commit is contained in:
@@ -173,8 +173,8 @@ 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?
|
||||||
|
|
||||||
force = truthy_param?(params[:force])
|
force = bool?(:force)
|
||||||
merge = truthy_param?(params[: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 = nil
|
||||||
@@ -201,15 +201,14 @@ class PostsController < ApplicationController
|
|||||||
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
|
end
|
||||||
incoming_snapshot = post_incoming_snapshot(post,
|
incoming_snapshot = post_incoming_snapshot(title:,
|
||||||
title:,
|
|
||||||
original_created_from:,
|
original_created_from:,
|
||||||
original_created_before:,
|
original_created_before:,
|
||||||
tag_names:,
|
tag_names:,
|
||||||
parent_post_ids:)
|
parent_post_ids:)
|
||||||
|
|
||||||
snapshot_to_apply =
|
snapshot_to_apply =
|
||||||
if post.version_no == base_version_no || force
|
if force || post.version_no == base_version_no || current_snapshot == base_snapshot
|
||||||
incoming_snapshot
|
incoming_snapshot
|
||||||
else
|
else
|
||||||
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
|
changes = post_snapshot_changes(base_snapshot, current_snapshot, incoming_snapshot)
|
||||||
@@ -448,30 +447,36 @@ class PostsController < ApplicationController
|
|||||||
version_no
|
version_no
|
||||||
end
|
end
|
||||||
|
|
||||||
def truthy_param?(value) = ActiveModel::Type::Boolean.new.cast(value)
|
|
||||||
|
|
||||||
def post_snapshot_from_version version
|
def post_snapshot_from_version version
|
||||||
{ 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.filter { !(_1.start_with?('nico:')) }.sort,
|
tag_names: editable_tag_names_from_version(version),
|
||||||
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
|
parent_post_ids: snapshot_parent_post_ids_from_version(version) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def editable_tag_names_from_version version
|
||||||
|
version.tags.to_s.split.reject { |name| name.downcase.start_with?('nico:') }.sort
|
||||||
|
end
|
||||||
|
|
||||||
def post_snapshot_from_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),
|
||||||
tag_names: post.tags.joins(:tag_name).order('tag_names.name').pluck('tag_names.name'),
|
tag_names: editable_tag_names_from_post(post),
|
||||||
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
parent_post_ids: post.parent_posts.order(:id).pluck(:id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_incoming_snapshot post, title:, original_created_from:, original_created_before:,
|
def editable_tag_names_from_post post
|
||||||
tag_names:, parent_post_ids:
|
post.tags.not_nico.joins(:tag_name).order('tag_names.name').pluck('tag_names.name')
|
||||||
|
end
|
||||||
|
|
||||||
|
def post_incoming_snapshot title:, original_created_from:, original_created_before:,
|
||||||
|
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(tag_names),
|
||||||
parent_post_ids: parent_post_ids.sort }
|
parent_post_ids: parent_post_ids.sort }
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -494,44 +499,10 @@ class PostsController < ApplicationController
|
|||||||
value.to_s
|
value.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def incoming_tag_names_for_snapshot post, raw_tag_names
|
def incoming_tag_names_for_snapshot raw_tag_names
|
||||||
manual_names = normalised_manual_tag_names_for_snapshot(raw_tag_names)
|
tags = Tag.normalise_tags!(raw_tag_names, with_tagme: false)
|
||||||
|
|
||||||
existing_tags =
|
Tag.expand_parent_tags(tags).map(&:name).uniq.sort
|
||||||
Tag
|
|
||||||
.joins(:tag_name)
|
|
||||||
.where(tag_names: { name: manual_names })
|
|
||||||
.to_a
|
|
||||||
|
|
||||||
expanded_names = Tag.expand_parent_tags(existing_tags).map(&:name)
|
|
||||||
|
|
||||||
(manual_names + expanded_names).uniq.sort
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalised_manual_tag_names_for_snapshot raw_tag_names
|
|
||||||
if raw_tag_names.any? { |name| name.downcase.start_with?('nico:') }
|
|
||||||
raise Tag::NicoTagNormalisationError
|
|
||||||
end
|
|
||||||
|
|
||||||
pairs = raw_tag_names.map do |raw_name|
|
|
||||||
prefix, category =
|
|
||||||
Tag::CATEGORY_PREFIXES.find { |p, _| raw_name.downcase.start_with?(p) } || ['', nil]
|
|
||||||
|
|
||||||
name = TagName.canonicalise(raw_name.sub(/\A#{ Regexp.escape(prefix) }/i, '')).first
|
|
||||||
|
|
||||||
[name, category]
|
|
||||||
end
|
|
||||||
|
|
||||||
names = pairs.map(&:first)
|
|
||||||
|
|
||||||
has_deerjikist = pairs.any? do |name, category|
|
|
||||||
category == :deerjikist ||
|
|
||||||
Tag.joins(:tag_name).where(category: :deerjikist, tag_names: { name: }).exists?
|
|
||||||
end
|
|
||||||
|
|
||||||
names << Tag.no_deerjikist.name unless has_deerjikist
|
|
||||||
|
|
||||||
names.uniq.sort
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
def post_conflict_json post:, base_version_no:, base_snapshot:,
|
||||||
@@ -618,29 +589,53 @@ class PostsController < ApplicationController
|
|||||||
original_created_from: snapshot[:original_created_from],
|
original_created_from: snapshot[:original_created_from],
|
||||||
original_created_before: snapshot[:original_created_before])
|
original_created_before: snapshot[:original_created_before])
|
||||||
|
|
||||||
tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false)
|
editable_tags = Tag.normalise_tags!(snapshot[:tag_names], with_tagme: false)
|
||||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: current_user)
|
TagVersioning.record_tag_snapshots!(editable_tags, created_by_user: current_user)
|
||||||
|
|
||||||
|
readonly_tags = post.tags.nico.to_a
|
||||||
|
|
||||||
|
tags = readonly_tags + editable_tags
|
||||||
tags = Tag.expand_parent_tags(tags)
|
tags = Tag.expand_parent_tags(tags)
|
||||||
sync_post_tags!(post, tags)
|
|
||||||
|
|
||||||
|
sync_post_tags!(post, tags)
|
||||||
sync_parent_posts!(post, snapshot[:parent_post_ids])
|
sync_parent_posts!(post, snapshot[:parent_post_ids])
|
||||||
|
|
||||||
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
PostVersionRecorder.record!(post:, event_type: :update, created_by_user: current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot
|
def merge_post_snapshots base_snapshot, current_snapshot, incoming_snapshot
|
||||||
[:title, :original_created_from, :original_created_before, :tag_names, :parent_post_ids].map {
|
[:title, :original_created_from, :original_created_before].map {
|
||||||
[_1, merge_scaler_snapshot_value(base_snapshot[_1],
|
[_1, merge_scalar_snapshot_value(base_snapshot[_1],
|
||||||
current_snapshot[_1],
|
current_snapshot[_1],
|
||||||
incoming_snapshot[_1])]
|
incoming_snapshot[_1])]
|
||||||
}.to_h
|
}.to_h.merge([:tag_names, :parent_post_ids].map {
|
||||||
|
[_1, merge_set_snapshot_value(base_snapshot[_1],
|
||||||
|
current_snapshot[_1],
|
||||||
|
incoming_snapshot[_1])]
|
||||||
|
}.to_h)
|
||||||
end
|
end
|
||||||
|
|
||||||
def merge_scaler_snapshot_value base, current, mine
|
def merge_scalar_snapshot_value base, current, mine
|
||||||
return mine if current == base
|
return mine if current == base
|
||||||
return current if mine == base || current == mine
|
return current if mine == base || current == mine
|
||||||
|
|
||||||
raise ArgumentError, '競合してゐる項目はマージできません.'
|
raise ArgumentError, '競合してゐる項目はマージできません.'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def merge_set_snapshot_value base, current, mine
|
||||||
|
base = base.to_a
|
||||||
|
current = current.to_a
|
||||||
|
mine = mine.to_a
|
||||||
|
|
||||||
|
added_by_current = current - base
|
||||||
|
removed_by_current = base - current
|
||||||
|
added_by_me = mine - base
|
||||||
|
removed_by_me = base - mine
|
||||||
|
|
||||||
|
merged = base + added_by_current + added_by_me
|
||||||
|
merged -= removed_by_current
|
||||||
|
merged -= removed_by_me
|
||||||
|
|
||||||
|
merged.uniq.sort
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
has_one_attached :thumbnail
|
has_one_attached :thumbnail
|
||||||
|
|
||||||
|
attribute :version_no, :integer, default: 1
|
||||||
|
|
||||||
before_validation :normalise_url
|
before_validation :normalise_url
|
||||||
|
|
||||||
validates :url, presence: true, uniqueness: true
|
validates :url, presence: true, uniqueness: true
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class Tag < ApplicationRecord
|
|||||||
belongs_to :tag_name
|
belongs_to :tag_name
|
||||||
delegate :wiki_page, to: :tag_name
|
delegate :wiki_page, to: :tag_name
|
||||||
|
|
||||||
|
attribute :version_no, :integer, default: 1
|
||||||
|
|
||||||
delegate :name, to: :tag_name, allow_nil: true
|
delegate :name, to: :tag_name, allow_nil: true
|
||||||
validates :tag_name, presence: true
|
validates :tag_name, presence: true
|
||||||
|
|
||||||
@@ -136,7 +138,6 @@ 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,6 +15,8 @@ class WikiPage < ApplicationRecord
|
|||||||
|
|
||||||
has_many :wiki_versions
|
has_many :wiki_versions
|
||||||
|
|
||||||
|
attribute :version_no, :integer, default: 1
|
||||||
|
|
||||||
belongs_to :tag_name
|
belongs_to :tag_name
|
||||||
validates :tag_name, presence: true
|
validates :tag_name, presence: true
|
||||||
validates :body, presence: true
|
validates :body, presence: true
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC, FormEvent } from 'react'
|
||||||
|
|
||||||
import type { Post, Tag } from '@/types'
|
import type { Post, Tag } from '@/types'
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ type Props = { post: Post
|
|||||||
|
|
||||||
|
|
||||||
export default (({ post, onSave }: Props) => {
|
export default (({ post, onSave }: Props) => {
|
||||||
|
const [disabled, setDisabled] = useState (false)
|
||||||
const [originalCreatedBefore, setOriginalCreatedBefore] =
|
const [originalCreatedBefore, setOriginalCreatedBefore] =
|
||||||
useState<string | null> (post.originalCreatedBefore)
|
useState<string | null> (post.originalCreatedBefore)
|
||||||
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
const [originalCreatedFrom, setOriginalCreatedFrom] =
|
||||||
@@ -61,7 +62,9 @@ export default (({ post, onSave }: Props) => {
|
|||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
if (e.response.status !== 409)
|
const response = (e as any)?.response
|
||||||
|
|
||||||
|
if (response?.status !== 409)
|
||||||
{
|
{
|
||||||
toast ({ description: '更新はできなかったよ……' })
|
toast ({ description: '更新はできなかったよ……' })
|
||||||
return
|
return
|
||||||
@@ -74,7 +77,7 @@ export default (({ post, onSave }: Props) => {
|
|||||||
<p>ほかの耕作員が先に更新してゐます.</p>
|
<p>ほかの耕作員が先に更新してゐます.</p>
|
||||||
<p>現在の変更をどう扱ひますか?</p>
|
<p>現在の変更をどう扱ひますか?</p>
|
||||||
</div>),
|
</div>),
|
||||||
choices: [{ value: 'merge', label: '差分をマージ' },
|
choices: [...(response?.data?.mergeable ? [{ value: 'merge', label: '差分をマージ' }] : []),
|
||||||
{ value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
|
{ value: 'overwrite', label: '強制上書き', variant: 'danger' }] })
|
||||||
|
|
||||||
if (action === 'merge')
|
if (action === 'merge')
|
||||||
@@ -96,12 +99,14 @@ export default (({ post, onSave }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async e => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
e.preventDefault ()
|
e.preventDefault ()
|
||||||
|
|
||||||
|
setDisabled (true)
|
||||||
await update ({ id: post.id, title, tags, parentPostIds,
|
await update ({ id: post.id, title, tags, parentPostIds,
|
||||||
originalCreatedFrom, originalCreatedBefore },
|
originalCreatedFrom, originalCreatedBefore },
|
||||||
{ baseVersionNo: post.versionNo })
|
{ baseVersionNo: post.versionNo })
|
||||||
|
setDisabled (false)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
@@ -113,10 +118,12 @@ export default (({ post, onSave }: Props) => {
|
|||||||
{/* タイトル */}
|
{/* タイトル */}
|
||||||
<div>
|
<div>
|
||||||
<Label>タイトル</Label>
|
<Label>タイトル</Label>
|
||||||
<input type="text"
|
<input
|
||||||
className="w-full border rounded p-2"
|
type="text"
|
||||||
value={title ?? ''}
|
disabled={disabled}
|
||||||
onChange={ev => setTitle (ev.target.value)}/>
|
className="w-full border rounded p-2"
|
||||||
|
value={title ?? ''}
|
||||||
|
onChange={ev => setTitle (ev.target.value)}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 親投稿 */}
|
{/* 親投稿 */}
|
||||||
@@ -124,24 +131,32 @@ export default (({ post, onSave }: Props) => {
|
|||||||
<Label>親投稿</Label>
|
<Label>親投稿</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
disabled={disabled}
|
||||||
value={parentPostIds}
|
value={parentPostIds}
|
||||||
onChange={e => setParentPostIds (e.target.value)}
|
onChange={e => setParentPostIds (e.target.value)}
|
||||||
className="w-full border p-2 rounded"/>
|
className="w-full border p-2 rounded"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* タグ */}
|
{/* タグ */}
|
||||||
<PostFormTagsArea tags={tags} setTags={setTags}/>
|
<PostFormTagsArea
|
||||||
|
disabled={disabled}
|
||||||
|
tags={tags}
|
||||||
|
setTags={setTags}/>
|
||||||
|
|
||||||
{/* オリジナルの作成日時 */}
|
{/* オリジナルの作成日時 */}
|
||||||
<PostOriginalCreatedTimeField
|
<PostOriginalCreatedTimeField
|
||||||
|
disabled={disabled}
|
||||||
originalCreatedFrom={originalCreatedFrom}
|
originalCreatedFrom={originalCreatedFrom}
|
||||||
setOriginalCreatedFrom={setOriginalCreatedFrom}
|
setOriginalCreatedFrom={setOriginalCreatedFrom}
|
||||||
originalCreatedBefore={originalCreatedBefore}
|
originalCreatedBefore={originalCreatedBefore}
|
||||||
setOriginalCreatedBefore={setOriginalCreatedBefore}/>
|
setOriginalCreatedBefore={setOriginalCreatedBefore}/>
|
||||||
|
|
||||||
{/* 送信 */}
|
{/* 送信 */}
|
||||||
<Button onClick={handleSubmit}
|
<Button
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
type="submit"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
|
||||||
更新
|
更新
|
||||||
</Button>
|
</Button>
|
||||||
</div>)
|
</div>)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Label from '@/components/common/Label'
|
|||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
|
|
||||||
import type { FC, SyntheticEvent } from 'react'
|
import type { ComponentPropsWithoutRef, FC, SyntheticEvent } from 'react'
|
||||||
|
|
||||||
import type { Tag } from '@/types'
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@ 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 =
|
||||||
tags: string
|
& { tags: string
|
||||||
setTags: (tags: string) => void }
|
setTags: (tags: string) => void }
|
||||||
|
& ComponentPropsWithoutRef<'textarea'>
|
||||||
|
|
||||||
|
|
||||||
export default (({ tags, setTags }: Props) => {
|
export default (({ tags, setTags, ...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 })
|
||||||
@@ -87,7 +88,8 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
setFocused (false)
|
setFocused (false)
|
||||||
setSuggestionsVsbl (false)
|
setSuggestionsVsbl (false)
|
||||||
}}/>
|
}}
|
||||||
|
{...rest}/>
|
||||||
{focused && (
|
{focused && (
|
||||||
<TagSearchBox
|
<TagSearchBox
|
||||||
suggestions={suggestionsVsbl && suggestions.length > 0
|
suggestions={suggestionsVsbl && suggestions.length > 0
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import { Button } from '@/components/ui/button'
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
disabled?: boolean
|
||||||
originalCreatedFrom: string | null
|
originalCreatedFrom: string | null
|
||||||
setOriginalCreatedFrom: (x: string | null) => void
|
setOriginalCreatedFrom: (x: string | null) => void
|
||||||
originalCreatedBefore: string | null
|
originalCreatedBefore: string | null
|
||||||
setOriginalCreatedBefore: (x: string | null) => void }
|
setOriginalCreatedBefore: (x: string | null) => void }
|
||||||
|
|
||||||
|
|
||||||
export default (({ originalCreatedFrom,
|
export default (({ disabled,
|
||||||
|
originalCreatedFrom,
|
||||||
setOriginalCreatedFrom,
|
setOriginalCreatedFrom,
|
||||||
originalCreatedBefore,
|
originalCreatedBefore,
|
||||||
setOriginalCreatedBefore }: Props) => (
|
setOriginalCreatedBefore }: Props) => (
|
||||||
@@ -21,6 +23,7 @@ export default (({ originalCreatedFrom,
|
|||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
<DateTimeField
|
<DateTimeField
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
|
disabled={disabled ?? false}
|
||||||
value={originalCreatedFrom ?? undefined}
|
value={originalCreatedFrom ?? undefined}
|
||||||
onChange={setOriginalCreatedFrom}
|
onChange={setOriginalCreatedFrom}
|
||||||
onBlur={ev => {
|
onBlur={ev => {
|
||||||
@@ -40,6 +43,7 @@ export default (({ originalCreatedFrom,
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className="bg-gray-600 text-white rounded"
|
className="bg-gray-600 text-white rounded"
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOriginalCreatedFrom (null)
|
setOriginalCreatedFrom (null)
|
||||||
}}>
|
}}>
|
||||||
@@ -51,6 +55,7 @@ export default (({ originalCreatedFrom,
|
|||||||
<div className="w-80">
|
<div className="w-80">
|
||||||
<DateTimeField
|
<DateTimeField
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
|
disabled={disabled}
|
||||||
value={originalCreatedBefore ?? undefined}
|
value={originalCreatedBefore ?? undefined}
|
||||||
onChange={setOriginalCreatedBefore}/>
|
onChange={setOriginalCreatedBefore}/>
|
||||||
より前
|
より前
|
||||||
@@ -58,6 +63,7 @@ export default (({ originalCreatedFrom,
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className="bg-gray-600 text-white rounded"
|
className="bg-gray-600 text-white rounded"
|
||||||
|
disabled={disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOriginalCreatedBefore (null)
|
setOriginalCreatedBefore (null)
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
|||||||
{ name: '一覧', to: '/posts' },
|
{ name: '一覧', to: '/posts' },
|
||||||
{ name: '検索', to: '/posts/search' },
|
{ name: '検索', to: '/posts/search' },
|
||||||
{ name: '追加', to: '/posts/new' },
|
{ name: '追加', to: '/posts/new' },
|
||||||
{ name: '履歴', to: '/posts/changes' },
|
{ name: '全体履歴', to: '/posts/changes' },
|
||||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
|
||||||
{ name: 'タグ', to: '/tags', subMenu: [
|
{ name: 'タグ', to: '/tags', subMenu: [
|
||||||
{ name: 'マスタ', to: '/tags' },
|
{ name: 'マスタ', to: '/tags' },
|
||||||
{ name: 'ニコニコ連携', to: '/tags/nico' },
|
{ name: 'ニコニコ連携', to: '/tags/nico' },
|
||||||
{ name: '履歴', to: '/tags/changes' },
|
{ name: '全体履歴', to: '/tags/changes' },
|
||||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' },
|
||||||
{ component: <Separator/>, visible: tagFlg },
|
{ component: <Separator/>, visible: tagFlg },
|
||||||
{ name: `広場 (${ postCount || 0 })`,
|
{ name: `広場 (${ postCount || 0 })`,
|
||||||
@@ -53,7 +53,7 @@ export const menuOutline = ({ tag, wikiId, user, pathName }: {
|
|||||||
{ name: '一覧', to: '/materials' },
|
{ name: '一覧', to: '/materials' },
|
||||||
{ name: '検索', to: '/materials/search', visible: false },
|
{ name: '検索', to: '/materials/search', visible: false },
|
||||||
{ name: '追加', to: '/materials/new' },
|
{ name: '追加', to: '/materials/new' },
|
||||||
{ name: '履歴', to: '/materials/changes', visible: false },
|
{ name: '全体履歴', to: '/materials/changes', visible: false },
|
||||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] },
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] },
|
||||||
{ name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [
|
{ name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [
|
||||||
{ name: <>第 1 会場</>, to: '/theatres/1' },
|
{ name: <>第 1 会場</>, to: '/theatres/1' },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { FC, FocusEvent } from 'react'
|
import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react'
|
||||||
|
|
||||||
|
|
||||||
const pad = (n: number): string => n.toString ().padStart (2, '0')
|
const pad = (n: number): string => n.toString ().padStart (2, '0')
|
||||||
@@ -18,14 +18,14 @@ const toDateTimeLocalValue = (d: Date) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> & {
|
||||||
value?: string
|
value?: string
|
||||||
onChange?: (isoUTC: string | null) => void
|
onChange?: (isoUTC: string | null) => void
|
||||||
className?: string
|
className?: string
|
||||||
onBlur?: (ev: FocusEvent<HTMLInputElement>) => void }
|
onBlur?: (ev: FocusEvent<HTMLInputElement>) => void }
|
||||||
|
|
||||||
|
|
||||||
export default (({ value, onChange, className, onBlur }: Props) => {
|
export default (({ value, onChange, className, onBlur, ...rest }: Props) => {
|
||||||
const [local, setLocal] = useState ('')
|
const [local, setLocal] = useState ('')
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
@@ -42,5 +42,6 @@ export default (({ value, onChange, className, onBlur }: 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>
|
||||||
|
|||||||
@@ -37,25 +37,27 @@ const DialogContent = React.forwardRef<
|
|||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn (
|
||||||
'fixed left-[50%] top-[50%] z-50 w-[90%] grid max-w-lg',
|
'fixed left-[50%] top-[50%] z-50 grid w-[calc(100%-2rem)] max-w-lg',
|
||||||
'translate-x-[-50%] translate-y-[-50%]',
|
'translate-x-[-50%] translate-y-[-50%]',
|
||||||
'gap-4 border bg-gray-300/80 dark:bg-gray-700/80',
|
'gap-5 rounded-2xl border border-border',
|
||||||
'p-6 shadow-lg duration-200',
|
'bg-background p-6 text-foreground shadow-2xl',
|
||||||
|
'duration-200',
|
||||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||||
'data-[state=closed]:slide-out-to-left-1/2',
|
|
||||||
'data-[state=closed]:slide-out-to-top-[48%]',
|
|
||||||
'data-[state=open]:slide-in-from-left-1/2',
|
|
||||||
'data-[state=open]:slide-in-from-top-[48%] rounded-lg',
|
|
||||||
className)}
|
className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<DialogPrimitive.Close className="absolute right-4 top-4 bg-red-500 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
<DialogPrimitive.Close
|
||||||
<X className="h-3 w-3" />
|
className={cn (
|
||||||
<span className="sr-only">Close</span>
|
'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')}>
|
||||||
|
<X className="h-4 w-4"/>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
|
|||||||
+50
-28
@@ -6,6 +6,56 @@
|
|||||||
|
|
||||||
@layer base
|
@layer base
|
||||||
{
|
{
|
||||||
|
:root
|
||||||
|
{
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 72.2% 50.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark
|
||||||
|
{
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 45%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
|
||||||
body
|
body
|
||||||
{
|
{
|
||||||
@apply overflow-x-clip;
|
@apply overflow-x-clip;
|
||||||
@@ -54,34 +104,6 @@ body
|
|||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1
|
|
||||||
{
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button
|
|
||||||
{
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover
|
|
||||||
{
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible
|
|
||||||
{
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light)
|
@media (prefers-color-scheme: light)
|
||||||
{
|
{
|
||||||
:root
|
:root
|
||||||
|
|||||||
@@ -19,7 +19,22 @@ export default {
|
|||||||
'rainbow-scroll': 'rainbow-scroll .25s linear infinite' },
|
'rainbow-scroll': 'rainbow-scroll .25s linear infinite' },
|
||||||
colors: {
|
colors: {
|
||||||
red: { 925: '#5f1414',
|
red: { 925: '#5f1414',
|
||||||
975: '#230505' } },
|
975: '#230505' },
|
||||||
|
border: 'hsl(var(--border))',
|
||||||
|
input: 'hsl(var(--input))',
|
||||||
|
ring: 'hsl(var(--ring))',
|
||||||
|
background: 'hsl(var(--background))',
|
||||||
|
foreground: 'hsl(var(--foreground))',
|
||||||
|
primary: { DEFAULT: 'hsl(var(--primary))',
|
||||||
|
foreground: 'hsl(var(--primary-foreground))' },
|
||||||
|
secondary: { DEFAULT: 'hsl(var(--secondary))',
|
||||||
|
foreground: 'hsl(var(--secondary-foreground))' },
|
||||||
|
destructive: { DEFAULT: 'hsl(var(--destructive))',
|
||||||
|
foreground: 'hsl(var(--destructive-foreground))' },
|
||||||
|
muted: { DEFAULT: 'hsl(var(--muted))',
|
||||||
|
foreground: 'hsl(var(--muted-foreground))' },
|
||||||
|
accent: { DEFAULT: 'hsl(var(--accent))',
|
||||||
|
foreground: 'hsl(var(--accent-foreground))' } },
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'rainbow-scroll': {
|
'rainbow-scroll': {
|
||||||
'0%': { backgroundPosition: '0% 50%' },
|
'0%': { backgroundPosition: '0% 50%' },
|
||||||
|
|||||||
Reference in New Issue
Block a user