minor fixes
This commit is contained in:
@@ -7,6 +7,7 @@ import Button from '../../components/ui/Button'
|
||||
import Modal from '../../components/ui/Modal'
|
||||
import FormField from '../../components/ui/FormField'
|
||||
import Toggle from '../../components/ui/Toggle'
|
||||
import NovaSelect from '../../components/ui/NovaSelect'
|
||||
import TagPicker from '../../components/tags/TagPicker'
|
||||
import SchedulePublishPicker from '../../components/upload/SchedulePublishPicker'
|
||||
|
||||
@@ -286,6 +287,29 @@ export default function StudioArtworkEdit() {
|
||||
selectedRoot?.name || 'No root category',
|
||||
subCategoryId ? subCategories.find((item) => item.id === subCategoryId)?.name : null,
|
||||
].filter(Boolean)
|
||||
const publishingIdentityOptions = useMemo(() => {
|
||||
const personalOption = {
|
||||
value: '',
|
||||
label: 'Personal profile',
|
||||
icon: <i className="fa-solid fa-user text-[11px] text-sky-200" aria-hidden="true" />,
|
||||
contextLabel: 'Publish under your own creator identity',
|
||||
}
|
||||
|
||||
const groupItems = groupOptions.map((group) => ({
|
||||
value: group.slug,
|
||||
label: group.name,
|
||||
icon: <i className="fa-solid fa-users text-[11px] text-violet-200" aria-hidden="true" />,
|
||||
contextLabel: 'Publish under the shared group identity',
|
||||
}))
|
||||
|
||||
return [personalOption, ...groupItems]
|
||||
}, [groupOptions])
|
||||
const primaryAuthorOptions = useMemo(() => currentContributorOptions.map((user) => ({
|
||||
value: Number(user.id),
|
||||
label: user.name || user.username,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatar_url || null,
|
||||
})), [currentContributorOptions])
|
||||
|
||||
// ── Handlers ───────────────────────────────────────────────────────────────
|
||||
const handleContentTypeChange = (id) => {
|
||||
@@ -1172,37 +1196,60 @@ export default function StudioArtworkEdit() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-white/90">Publishing identity</span>
|
||||
<select
|
||||
value={groupSlug}
|
||||
onChange={(event) => setGroupSlug(event.target.value)}
|
||||
className="mt-2 w-full rounded-xl border border-white/15 bg-black/20 px-3 py-3 text-sm text-white outline-none transition focus:border-sky-300/40"
|
||||
>
|
||||
<option value="">Personal profile</option>
|
||||
{groupOptions.map((group) => (
|
||||
<option key={group.slug} value={group.slug}>{group.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.group?.[0] ? <p className="mt-2 text-xs text-red-400">{errors.group[0]}</p> : null}
|
||||
</label>
|
||||
<NovaSelect
|
||||
label="Publishing identity"
|
||||
value={groupSlug || ''}
|
||||
onChange={(nextValue) => setGroupSlug(String(nextValue || ''))}
|
||||
options={publishingIdentityOptions}
|
||||
searchable={false}
|
||||
placeholder="Choose publishing identity"
|
||||
error={errors.group?.[0]}
|
||||
hint={selectedGroupOption
|
||||
? 'The artwork will be publicly published under the selected group while authorship stays editable below.'
|
||||
: 'Personal publishing keeps the artwork under your own creator profile.'}
|
||||
className="mt-2 bg-black/20"
|
||||
renderOption={(option) => (
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/10 bg-white/[0.04]">
|
||||
{option.icon}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium text-white">{option.label}</span>
|
||||
<span className="block truncate text-[11px] text-slate-500">{option.contextLabel}</span>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
|
||||
{groupSlug ? (
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,0.95fr)_minmax(0,1.05fr)]">
|
||||
<div>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-white/90">Primary author</span>
|
||||
<select
|
||||
value={primaryAuthorUserId || ''}
|
||||
onChange={(event) => setPrimaryAuthorUserId(event.target.value ? Number(event.target.value) : null)}
|
||||
className="mt-2 w-full rounded-xl border border-white/15 bg-black/20 px-3 py-3 text-sm text-white outline-none transition focus:border-sky-300/40"
|
||||
>
|
||||
{currentContributorOptions.map((user) => (
|
||||
<option key={user.id} value={user.id}>{user.name || user.username}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{errors.primary_author_user_id?.[0] ? <p className="mt-2 text-xs text-red-400">{errors.primary_author_user_id[0]}</p> : <p className="mt-2 text-xs text-slate-400">Primary author remains the lead creator shown on the public artwork page.</p>}
|
||||
<NovaSelect
|
||||
label="Primary author"
|
||||
value={primaryAuthorUserId || null}
|
||||
onChange={(nextValue) => setPrimaryAuthorUserId(nextValue ? Number(nextValue) : null)}
|
||||
options={primaryAuthorOptions}
|
||||
placeholder="Choose primary author"
|
||||
searchable={primaryAuthorOptions.length > 6}
|
||||
error={errors.primary_author_user_id?.[0]}
|
||||
hint="Primary author remains the lead creator shown on the public artwork page."
|
||||
className="mt-2 bg-black/20"
|
||||
renderOption={(option) => (
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
{option.avatarUrl ? (
|
||||
<img src={option.avatarUrl} alt="" className="h-7 w-7 shrink-0 rounded-full object-cover" />
|
||||
) : (
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-white/10 bg-white/[0.04] text-slate-400">
|
||||
<i className="fa-solid fa-user text-[11px]" aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium text-white">{option.label}</span>
|
||||
{option.username ? <span className="block truncate text-[11px] text-slate-500">@{option.username}</span> : null}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -1,12 +1,38 @@
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
import GroupStudioPromoCard from '../../components/groups/GroupStudioPromoCard'
|
||||
|
||||
function slugifyGroupValue(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 90)
|
||||
}
|
||||
|
||||
function resolveMediaPreviewUrl(path, filesCdnUrl) {
|
||||
const trimmed = String(path || '').trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('blob:') || trimmed.startsWith('data:') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return `${String(filesCdnUrl || '').replace(/\/$/, '')}/${trimmed.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
export default function StudioGroupCreate() {
|
||||
const { props } = usePage()
|
||||
const filesCdnUrl = props?.cdn?.files_url || ''
|
||||
const avatarInputRef = useRef(null)
|
||||
const bannerInputRef = useRef(null)
|
||||
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false)
|
||||
const [form, setForm] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
@@ -25,6 +51,8 @@ export default function StudioGroupCreate() {
|
||||
})
|
||||
const [avatarPreview, setAvatarPreview] = useState('')
|
||||
const [bannerPreview, setBannerPreview] = useState('')
|
||||
const resolvedAvatarPreview = useMemo(() => avatarPreview || resolveMediaPreviewUrl(form.avatar_path, filesCdnUrl), [avatarPreview, form.avatar_path, filesCdnUrl])
|
||||
const resolvedBannerPreview = useMemo(() => bannerPreview || resolveMediaPreviewUrl(form.banner_path, filesCdnUrl), [bannerPreview, form.banner_path, filesCdnUrl])
|
||||
|
||||
const updateLink = (index, key, value) => {
|
||||
setForm((current) => ({
|
||||
@@ -72,6 +100,23 @@ export default function StudioGroupCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleNameChange = (event) => {
|
||||
const nextName = event.target.value
|
||||
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
name: nextName,
|
||||
slug: slugManuallyEdited ? current.slug : slugifyGroupValue(nextName),
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSlugChange = (event) => {
|
||||
const nextSlug = slugifyGroupValue(event.target.value)
|
||||
|
||||
setSlugManuallyEdited(nextSlug !== '')
|
||||
setForm((current) => ({ ...current, slug: nextSlug }))
|
||||
}
|
||||
|
||||
return (
|
||||
<StudioLayout title={props.title} subtitle={props.description}>
|
||||
<div className="mx-auto mb-6 max-w-5xl">
|
||||
@@ -94,11 +139,11 @@ export default function StudioGroupCreate() {
|
||||
<div className="grid gap-5">
|
||||
<label className="grid gap-2 text-sm text-slate-200">
|
||||
<span>Name</span>
|
||||
<input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value, slug: current.slug || event.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
||||
<input value={form.name} onChange={handleNameChange} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-slate-200">
|
||||
<span>Slug</span>
|
||||
<input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
||||
<input value={form.slug} onChange={handleSlugChange} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
|
||||
</label>
|
||||
<label className="grid gap-2 text-sm text-slate-200">
|
||||
<span>Short description</span>
|
||||
@@ -126,7 +171,7 @@ export default function StudioGroupCreate() {
|
||||
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
|
||||
<span className="text-sm font-semibold text-white">Avatar / logo</span>
|
||||
<div className="flex h-28 w-28 items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
|
||||
{avatarPreview || form.avatar_path ? <img src={avatarPreview || form.avatar_path} alt="Avatar preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-image text-slate-500" />}
|
||||
{resolvedAvatarPreview ? <img src={resolvedAvatarPreview} alt="Avatar preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-image text-slate-500" />}
|
||||
</div>
|
||||
<input ref={avatarInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('avatar_file', setAvatarPreview)} className="hidden" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -141,7 +186,7 @@ export default function StudioGroupCreate() {
|
||||
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
|
||||
<span className="text-sm font-semibold text-white">Cover image</span>
|
||||
<div className="flex h-28 w-full items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
|
||||
{bannerPreview || form.banner_path ? <img src={bannerPreview || form.banner_path} alt="Cover preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-panorama text-slate-500" />}
|
||||
{resolvedBannerPreview ? <img src={resolvedBannerPreview} alt="Cover preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-panorama text-slate-500" />}
|
||||
</div>
|
||||
<input ref={bannerInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('banner_file', setBannerPreview)} className="hidden" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -2,9 +2,24 @@ import React, { useMemo, useRef, useState } from 'react'
|
||||
import { router, usePage } from '@inertiajs/react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
|
||||
function resolveMediaPreviewUrl(path, filesCdnUrl) {
|
||||
const trimmed = String(path || '').trim()
|
||||
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('blob:') || trimmed.startsWith('data:') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return `${String(filesCdnUrl || '').replace(/\/$/, '')}/${trimmed.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
export default function StudioGroupSettings() {
|
||||
const { props } = usePage()
|
||||
const group = props.studioGroup || {}
|
||||
const filesCdnUrl = props?.cdn?.files_url || ''
|
||||
const featuredArtworkOptions = Array.isArray(props.featuredArtworkOptions) ? props.featuredArtworkOptions : []
|
||||
const avatarInputRef = useRef(null)
|
||||
const bannerInputRef = useRef(null)
|
||||
@@ -27,6 +42,8 @@ export default function StudioGroupSettings() {
|
||||
})
|
||||
const [avatarPreview, setAvatarPreview] = useState('')
|
||||
const [bannerPreview, setBannerPreview] = useState('')
|
||||
const resolvedAvatarPreview = useMemo(() => avatarPreview || resolveMediaPreviewUrl(form.avatar_path || group.avatar_url, filesCdnUrl), [avatarPreview, form.avatar_path, group.avatar_url, filesCdnUrl])
|
||||
const resolvedBannerPreview = useMemo(() => bannerPreview || resolveMediaPreviewUrl(form.banner_path || group.banner_url, filesCdnUrl), [bannerPreview, form.banner_path, group.banner_url, filesCdnUrl])
|
||||
|
||||
const selectedFeaturedArtwork = useMemo(
|
||||
() => featuredArtworkOptions.find((item) => Number(item.id) === Number(form.featured_artwork_id)) || null,
|
||||
@@ -105,7 +122,7 @@ export default function StudioGroupSettings() {
|
||||
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
|
||||
<span className="text-sm font-semibold text-white">Avatar / logo</span>
|
||||
<div className="flex h-28 w-28 items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
|
||||
{avatarPreview || form.avatar_path || group.avatar_url ? <img src={avatarPreview || form.avatar_path || group.avatar_url} alt="Avatar preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-image text-slate-500" />}
|
||||
{resolvedAvatarPreview ? <img src={resolvedAvatarPreview} alt="Avatar preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-image text-slate-500" />}
|
||||
</div>
|
||||
<input ref={avatarInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('avatar_file', setAvatarPreview)} className="hidden" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -117,7 +134,7 @@ export default function StudioGroupSettings() {
|
||||
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
|
||||
<span className="text-sm font-semibold text-white">Cover image</span>
|
||||
<div className="flex h-28 w-full items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
|
||||
{bannerPreview || form.banner_path || group.banner_url ? <img src={bannerPreview || form.banner_path || group.banner_url} alt="Cover preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-panorama text-slate-500" />}
|
||||
{resolvedBannerPreview ? <img src={resolvedBannerPreview} alt="Cover preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-panorama text-slate-500" />}
|
||||
</div>
|
||||
<input ref={bannerInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('banner_file', setBannerPreview)} className="hidden" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -17,6 +17,19 @@ const phases = {
|
||||
error: 'error',
|
||||
}
|
||||
|
||||
const DEFAULT_CHUNK_REQUEST_TIMEOUT_MS = 45000
|
||||
const MIN_CHUNK_SIZE_BYTES = 256 * 1024
|
||||
|
||||
function formatChunkSize(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return '0 KB'
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes % (1024 * 1024) === 0 ? 0 : 1)} MB`
|
||||
return `${Math.max(1, Math.round(bytes / 1024))} KB`
|
||||
}
|
||||
|
||||
function isRequestTooLarge(error) {
|
||||
return Number(error?.response?.status || 0) === 413
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
phase: phases.idle,
|
||||
sessionId: null,
|
||||
@@ -163,9 +176,14 @@ function getTypeKey(ct) {
|
||||
return String(ct.name || '').toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '')
|
||||
}
|
||||
|
||||
function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
function useUploadMachine({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs, userId }) {
|
||||
const [state, dispatch] = useReducer(reducer, { ...initialState, draftId })
|
||||
const pollRef = useRef(null)
|
||||
const adaptiveChunkSizeRef = useRef(Math.max(1, Number(chunkSize || 0)))
|
||||
const effectiveChunkRequestTimeoutMs = (() => {
|
||||
const parsed = Number(chunkRequestTimeoutMs)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_CHUNK_REQUEST_TIMEOUT_MS
|
||||
})()
|
||||
|
||||
const extractErrorMessage = useCallback((error, fallback) => {
|
||||
const message = error?.response?.data?.message
|
||||
@@ -379,6 +397,7 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
|
||||
try {
|
||||
const res = await window.axios.post('/api/uploads/chunk', payload, {
|
||||
timeout: effectiveChunkRequestTimeoutMs,
|
||||
headers: uploadToken ? { 'X-Upload-Token': uploadToken } : undefined,
|
||||
})
|
||||
|
||||
@@ -389,13 +408,16 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
if (isRequestTooLarge(error)) {
|
||||
throw error
|
||||
}
|
||||
if (attempt < MAX_CHUNK_RETRIES) {
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS * (attempt + 1)))
|
||||
return uploadChunk(sessionId, uploadToken, blob, offset, totalSize, attempt + 1)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}, [])
|
||||
}, [effectiveChunkRequestTimeoutMs])
|
||||
|
||||
const uploadFile = useCallback(async (sessionId, uploadToken, file) => {
|
||||
dispatch({ type: 'UPLOAD_START' })
|
||||
@@ -415,7 +437,8 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
if (offset > totalSize) offset = 0
|
||||
|
||||
while (offset < totalSize) {
|
||||
const nextOffset = Math.min(offset + chunkSize, totalSize)
|
||||
const activeChunkSize = Math.max(MIN_CHUNK_SIZE_BYTES, Number(adaptiveChunkSizeRef.current || chunkSize || MIN_CHUNK_SIZE_BYTES))
|
||||
const nextOffset = Math.min(offset + activeChunkSize, totalSize)
|
||||
const chunk = file.slice(offset, nextOffset)
|
||||
|
||||
try {
|
||||
@@ -425,6 +448,14 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
offset = nextOffset
|
||||
}
|
||||
} catch (error) {
|
||||
if (isRequestTooLarge(error) && activeChunkSize > MIN_CHUNK_SIZE_BYTES) {
|
||||
const nextChunkSize = Math.max(MIN_CHUNK_SIZE_BYTES, Math.floor(activeChunkSize / 2))
|
||||
if (nextChunkSize < activeChunkSize) {
|
||||
adaptiveChunkSizeRef.current = nextChunkSize
|
||||
pushNotice('warning', `Server rejected ${formatChunkSize(activeChunkSize)} chunks. Retrying with ${formatChunkSize(nextChunkSize)} chunks.`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
const notice = mapUploadErrorNotice(error, 'File upload failed. Please retry.')
|
||||
dispatch({ type: 'UPLOAD_ERROR', error: notice.message })
|
||||
pushMappedNotice(notice)
|
||||
@@ -587,7 +618,7 @@ function useUploadMachine({ draftId, filesCdnUrl, chunkSize, userId }) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function UploadPage({ draftId, filesCdnUrl, chunkSize }) {
|
||||
export default function UploadPage({ draftId, filesCdnUrl, chunkSize, chunkRequestTimeoutMs }) {
|
||||
const { props } = usePage()
|
||||
|
||||
const windowFlags = window?.SKINBASE_FLAGS || {}
|
||||
@@ -618,6 +649,7 @@ export default function UploadPage({ draftId, filesCdnUrl, chunkSize }) {
|
||||
<UploadWizard
|
||||
initialDraftId={draftId ?? null}
|
||||
chunkSize={chunkSize}
|
||||
chunkRequestTimeoutMs={chunkRequestTimeoutMs}
|
||||
contentTypes={Array.isArray(props?.content_types) ? props.content_types : []}
|
||||
suggestedTags={Array.isArray(props?.suggested_tags) ? props.suggested_tags : []}
|
||||
groupOptions={Array.isArray(props?.group_options) ? props.group_options : []}
|
||||
@@ -689,7 +721,13 @@ export default function UploadPage({ draftId, filesCdnUrl, chunkSize }) {
|
||||
const userId = props?.auth?.user?.id ?? null
|
||||
const suggestedTags = Array.isArray(props?.suggested_tags) ? props.suggested_tags : []
|
||||
const safeChunkSize = Math.max(1, Number(chunkSize || 0))
|
||||
const { state, dispatch, previewUrl, startUpload, cancelUpload } = useUploadMachine({ draftId, filesCdnUrl, chunkSize: safeChunkSize, userId })
|
||||
const { state, dispatch, previewUrl, startUpload, cancelUpload } = useUploadMachine({
|
||||
draftId,
|
||||
filesCdnUrl,
|
||||
chunkSize: safeChunkSize,
|
||||
chunkRequestTimeoutMs,
|
||||
userId,
|
||||
})
|
||||
const fileInputRef = useRef(null)
|
||||
const [confirmCancel, setConfirmCancel] = useState(false)
|
||||
const [contentTypes, setContentTypes] = useState([])
|
||||
|
||||
Reference in New Issue
Block a user