/** * UploadWizard – refactored orchestrator * * A 3-step upload wizard that delegates: * - Machine state → useUploadMachine * - File validation → useFileValidation * - Vision AI tags → useVisionTags * - Step rendering → Step1FileUpload / Step2Details / Step3Publish * - Reusable UI → ContentTypeSelector / CategorySelector / UploadSidebar … */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { AnimatePresence, motion, useReducedMotion } from 'framer-motion' import useUploadMachine, { machineStates } from '../../hooks/upload/useUploadMachine' import useFileValidation from '../../hooks/upload/useFileValidation' import useVisionTags from '../../hooks/upload/useVisionTags' import StudioStatusBar from './StudioStatusBar' import UploadOverlay from './UploadOverlay' import UploadActions from './UploadActions' import PublishPanel from './PublishPanel' import Step1FileUpload from './steps/Step1FileUpload' import Step2Details from './steps/Step2Details' import Step3Publish from './steps/Step3Publish' import { buildCategoryTree, getContentTypeValue, getProcessingTransparencyLabel, } from '../../lib/uploadUtils' // ─── Wizard step config ─────────────────────────────────────────────────────── const wizardSteps = [ { key: 'upload', label: 'Upload' }, { key: 'details', label: 'Details' }, { key: 'publish', label: 'Publish' }, ] function createInitialMetadata(initialGroupSlug = '', currentUserId = null, contributorOptionsByGroup = {}, eligibleWorlds = []) { const normalizedGroupSlug = String(initialGroupSlug || '').trim() const contributors = Array.isArray(contributorOptionsByGroup?.[normalizedGroupSlug]) ? contributorOptionsByGroup[normalizedGroupSlug] : [] const defaultPrimaryAuthor = contributors.some((user) => Number(user.id) === Number(currentUserId)) ? Number(currentUserId) : Number(contributors[0]?.id || 0) || null return { title: '', rootCategoryId: '', subCategoryId: '', tags: [], description: '', isMature: false, rightsAccepted: false, contentType: '', group: normalizedGroupSlug, primaryAuthorUserId: defaultPrimaryAuthor, contributorUserIds: [], contributorCredits: {}, worldSubmissions: Array.isArray(eligibleWorlds) ? eligibleWorlds.map((world) => ({ ...world, selected: Boolean(world.selected), note: world.note || '' })) : [], } } function normalizeContributorCredits(contributorIds = [], contributorCredits = {}) { const normalized = {} const ids = Array.isArray(contributorIds) ? contributorIds.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0) : [] ids.forEach((id) => { const current = contributorCredits?.[id] || contributorCredits?.[String(id)] || {} normalized[id] = { creditRole: typeof current.creditRole === 'string' ? current.creditRole : '', isPrimary: Boolean(current.isPrimary), } }) const leadIds = Object.entries(normalized) .filter(([, value]) => value.isPrimary) .map(([id]) => Number(id)) if (leadIds.length > 1) { leadIds.slice(1).forEach((id) => { normalized[id] = { ...normalized[id], isPrimary: false, } }) } return normalized } // ─── Helpers ───────────────────────────────────────────────────────────────── function isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors) { if (!primaryFile) return false if (primaryErrors.length > 0) return false if (isArchive && screenshotErrors.length > 0) return false return true } // ─── Component ──────────────────────────────────────────────────────────────── export default function UploadWizard({ onValidationStateChange, initialDraftId = null, chunkSize, chunkRequestTimeoutMs, contentTypes = [], suggestedTags = [], eligibleWorlds = [], groupOptions = [], contributorOptionsByGroup = {}, initialGroupSlug = '', currentUserId = null, }) { const [notices, setNotices] = useState([]) // ── UI state ────────────────────────────────────────────────────────────── const [activeStep, setActiveStep] = useState(1) const [showRestoredBanner, setShowRestoredBanner] = useState(Boolean(initialDraftId)) const [isUploadLocked, setIsUploadLocked] = useState(false) const [resolvedArtworkId, setResolvedArtworkId] = useState(() => { const parsed = Number(initialDraftId) return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null }) // ── Publish options (Studio) ────────────────────────────────────────────── const [publishMode, setPublishMode] = useState('now') // 'now' | 'schedule' const [scheduledAt, setScheduledAt] = useState(null) // UTC ISO or null const [visibility, setVisibility] = useState('public') // 'public'|'unlisted'|'private' const userTimezone = useMemo(() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' } }, []) // ── File + screenshot state ─────────────────────────────────────────────── const [primaryFile, setPrimaryFile] = useState(null) const [screenshots, setScreenshots] = useState([]) const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0) // ── Metadata state ──────────────────────────────────────────────────────── const [metadata, setMetadata] = useState(() => createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup, eligibleWorlds)) // ── Refs ────────────────────────────────────────────────────────────────── const prefersReducedMotion = useReducedMotion() const stepContentRef = useRef(null) const stepHeadingRef = useRef(null) const hasAutoAdvancedRef = useRef(false) const quickTransition = prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' } // ── File validation hook ────────────────────────────────────────────────── const { primaryType, primaryErrors, primaryWarnings, fileMetadata, primaryPreviewUrl, screenshotErrors, screenshotPerFileErrors, } = useFileValidation(primaryFile, screenshots) const isArchive = primaryType === 'archive' const canStartUpload = isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors) useEffect(() => { if (!Array.isArray(screenshots) || screenshots.length === 0) { setSelectedScreenshotIndex(0) return } setSelectedScreenshotIndex((prev) => { if (!Number.isFinite(prev) || prev < 0) return 0 return Math.min(prev, screenshots.length - 1) }) }, [screenshots]) // ── Machine hook ────────────────────────────────────────────────────────── const { machine, runUploadFlow, handleCancel, handlePublish, handleRetry, resetMachine, abortAllRequests, clearPolling, } = useUploadMachine({ primaryFile, screenshots, selectedScreenshotIndex, canStartUpload, primaryType, isArchive, initialDraftId, metadata, chunkSize, chunkRequestTimeoutMs, onArtworkCreated: (id) => setResolvedArtworkId(id), onNotice: (notice) => { if (!notice?.message) return const normalizedType = ['success', 'warning', 'error'].includes(String(notice.type || '').toLowerCase()) ? String(notice.type).toLowerCase() : 'error' const id = `${Date.now()}-${Math.random().toString(16).slice(2)}` setNotices((prev) => [...prev, { id, type: normalizedType, message: String(notice.message) }]) window.setTimeout(() => { setNotices((prev) => prev.filter((item) => item.id !== id)) }, 4500) }, }) // ── Upload-ready flag (needed before vision hook) ───────────────────────── const uploadReady = ( machine.state === machineStates.ready_to_publish || machine.processingStatus === 'ready' || machine.state === machineStates.complete ) // ── Vision tags hook – fires on upload completion, not step change ────────── // Starts fetching AI tag suggestions while the user is still on Step 1, // so results are ready (or partially ready) by the time Step 2 opens. const { visionSuggestedTags } = useVisionTags(resolvedArtworkId, uploadReady) // ── Category tree computation ───────────────────────────────────────────── const categoryTreeByType = useMemo(() => { const result = {} const list = Array.isArray(contentTypes) ? contentTypes : [] list.forEach((type) => { const value = getContentTypeValue(type) if (!value) return result[value] = buildCategoryTree([type]) }) return result }, [contentTypes]) const filteredCategoryTree = useMemo(() => { const selected = String(metadata.contentType || '') if (!selected) return [] return categoryTreeByType[selected] || [] }, [categoryTreeByType, metadata.contentType]) const selectedGroupOption = useMemo(() => { const selectedSlug = String(metadata.group || '') if (!selectedSlug) return null return (Array.isArray(groupOptions) ? groupOptions : []).find((group) => String(group.slug || '') === selectedSlug) || null }, [groupOptions, metadata.group]) const reviewSubmissionMode = Boolean( selectedGroupOption && !selectedGroupOption?.permissions?.can_publish_artworks && selectedGroupOption?.permissions?.can_submit_artwork_for_review ) const publishActionLabel = reviewSubmissionMode ? 'Submit for review' : (publishMode === 'schedule' ? 'Schedule publish' : 'Publish now') const currentContributorOptions = useMemo(() => { const selectedSlug = String(metadata.group || '') return Array.isArray(contributorOptionsByGroup?.[selectedSlug]) ? contributorOptionsByGroup[selectedSlug] : [] }, [contributorOptionsByGroup, metadata.group]) const allRootCategoryOptions = useMemo(() => { const items = [] Object.entries(categoryTreeByType).forEach(([contentTypeValue, roots]) => { roots.forEach((root) => items.push({ ...root, contentTypeValue })) }) return items }, [categoryTreeByType]) const selectedRootCategory = useMemo(() => { const id = String(metadata.rootCategoryId || '') if (!id) return null return ( filteredCategoryTree.find((r) => String(r.id) === id) || allRootCategoryOptions.find((r) => String(r.id) === id) || null ) }, [filteredCategoryTree, allRootCategoryOptions, metadata.rootCategoryId]) const requiresSubCategory = Boolean( selectedRootCategory && Array.isArray(selectedRootCategory.children) && selectedRootCategory.children.length > 0 ) useEffect(() => { const selectedSlug = String(metadata.group || '') if (!selectedSlug) { if (metadata.primaryAuthorUserId || metadata.contributorUserIds.length > 0 || Object.keys(metadata.contributorCredits || {}).length > 0) { setMetadata((current) => ({ ...current, primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} })) } return } const validGroup = (Array.isArray(groupOptions) ? groupOptions : []).some((group) => String(group.slug || '') === selectedSlug) if (!validGroup) { setMetadata((current) => ({ ...current, group: '', primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} })) return } const validContributorIds = currentContributorOptions.map((user) => Number(user.id)).filter((id) => Number.isFinite(id) && id > 0) const nextPrimaryAuthorId = validContributorIds.includes(Number(metadata.primaryAuthorUserId)) ? Number(metadata.primaryAuthorUserId) : (validContributorIds.includes(Number(currentUserId)) ? Number(currentUserId) : (validContributorIds[0] || null)) const nextContributorIds = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : []) .map((id) => Number(id)) .filter((id) => validContributorIds.includes(id) && id !== nextPrimaryAuthorId) const nextContributorCredits = normalizeContributorCredits(nextContributorIds, metadata.contributorCredits) const currentPrimary = metadata.primaryAuthorUserId ? Number(metadata.primaryAuthorUserId) : null const currentContributors = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : []).map((id) => Number(id)) const contributorsChanged = nextContributorIds.length !== currentContributors.length || nextContributorIds.some((id, index) => id !== currentContributors[index]) const contributorCreditsChanged = JSON.stringify(nextContributorCredits) !== JSON.stringify(normalizeContributorCredits(currentContributors, metadata.contributorCredits)) if (currentPrimary !== nextPrimaryAuthorId || contributorsChanged || contributorCreditsChanged) { setMetadata((current) => ({ ...current, primaryAuthorUserId: nextPrimaryAuthorId, contributorUserIds: nextContributorIds, contributorCredits: nextContributorCredits, })) } }, [groupOptions, currentContributorOptions, currentUserId, metadata.group, metadata.primaryAuthorUserId, metadata.contributorUserIds, metadata.contributorCredits]) // ── Metadata validation ─────────────────────────────────────────────────── const metadataErrors = useMemo(() => { const errors = {} if (!String(metadata.title || '').trim()) errors.title = 'Title is required.' if (!metadata.contentType) errors.contentType = 'Content type is required.' if (!metadata.rootCategoryId) errors.category = 'Root category is required.' if (metadata.rootCategoryId && requiresSubCategory && !metadata.subCategoryId) { errors.category = 'Subcategory is required for the selected category.' } if (!metadata.rightsAccepted) errors.rights = 'Rights confirmation is required.' return errors }, [metadata, requiresSubCategory]) const detailsValid = Object.keys(metadataErrors).length === 0 // ── Merged AI + manual suggested tags ──────────────────────────────────── const mergedSuggestedTags = useMemo(() => { const map = new Map() const addTag = (item) => { if (!item) return const key = String(item?.slug || item?.tag || item?.name || item).trim().toLowerCase() if (!key || map.has(key)) return map.set(key, typeof item === 'string' ? item : { id: item.id ?? key, name: item.name || item.tag || item.slug || key, slug: item.slug || item.tag || key, usage_count: Number(item.usage_count || 0), is_ai: Boolean(item.is_ai || item.source === 'ai'), source: item.source || (item.is_ai ? 'ai' : 'manual'), }) } ;(Array.isArray(suggestedTags) ? suggestedTags : []).forEach(addTag) ;(Array.isArray(visionSuggestedTags) ? visionSuggestedTags : []).forEach(addTag) return Array.from(map.values()) }, [suggestedTags, visionSuggestedTags]) // ── Derived flags ───────────────────────────────────────────────────────── const highestUnlockedStep = uploadReady ? (detailsValid ? 3 : 2) : 1 const showProgress = ![machineStates.idle, machineStates.cancelled].includes(machine.state) const processingLabel = getProcessingTransparencyLabel(machine.processingStatus, machine.state) const showOverlay = ['initializing', 'uploading', 'finishing', 'processing', 'error'].includes(machine.state) const hasTitle = Boolean(String(metadata.title || '').trim()) const hasCompleteCategory = Boolean( metadata.rootCategoryId && (!requiresSubCategory || metadata.subCategoryId) ) const hasTag = Array.isArray(metadata.tags) && metadata.tags.length > 0 const hasRequiredScreenshot = !isArchive || screenshots.length > 0 const canPublish = useMemo(() => ( uploadReady && hasTitle && hasCompleteCategory && hasTag && hasRequiredScreenshot && metadata.rightsAccepted && machine.state !== machineStates.publishing ), [uploadReady, hasTitle, hasCompleteCategory, hasTag, hasRequiredScreenshot, metadata.rightsAccepted, machine.state]) const canScheduleSubmit = useMemo(() => { if (!canPublish) return false if (reviewSubmissionMode) return true if (publishMode === 'schedule') return Boolean(scheduledAt) return true }, [canPublish, reviewSubmissionMode, publishMode, scheduledAt]) const publishActionEnabled = activeStep === 3 && canScheduleSubmit // ── Validation surface for parent ──────────────────────────────────────── const validationErrors = useMemo( () => [...primaryErrors, ...screenshotErrors], [primaryErrors, screenshotErrors] ) useEffect(() => { if (typeof onValidationStateChange === 'function') { onValidationStateChange({ isValid: canStartUpload, validationErrors, isArchive }) } }, [canStartUpload, validationErrors, isArchive, onValidationStateChange]) // ── Auto-advance to step 2 after upload complete ────────────────────────── useEffect(() => { if (uploadReady && activeStep === 1 && !hasAutoAdvancedRef.current) { hasAutoAdvancedRef.current = true setIsUploadLocked(true) setActiveStep(2) } }, [uploadReady, activeStep]) useEffect(() => { if (uploadReady) setIsUploadLocked(true) }, [uploadReady]) // ── Step scroll + focus ─────────────────────────────────────────────────── useEffect(() => { if (!stepContentRef.current) return stepContentRef.current.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth', block: 'start', }) window.setTimeout(() => { stepHeadingRef.current?.focus?.({ preventScroll: true }) }, 0) }, [activeStep, prefersReducedMotion]) // ── Cleanup ─────────────────────────────────────────────────────────────── useEffect(() => { return () => { abortAllRequests() clearPolling() } }, [abortAllRequests, clearPolling]) // ── Metadata helpers ────────────────────────────────────────────────────── const setMeta = useCallback((patch) => setMetadata((prev) => ({ ...prev, ...patch })), []) // ── Full reset ──────────────────────────────────────────────────────────── const handleReset = useCallback(() => { resetMachine() setPrimaryFile(null) setScreenshots([]) setSelectedScreenshotIndex(0) setMetadata(createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup, eligibleWorlds)) setIsUploadLocked(false) hasAutoAdvancedRef.current = false setPublishMode('now') setScheduledAt(null) setVisibility('public') setResolvedArtworkId(() => { const parsed = Number(initialDraftId) return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null }) setActiveStep(1) }, [resetMachine, initialDraftId, initialGroupSlug, currentUserId, contributorOptionsByGroup, eligibleWorlds]) const goToStep = useCallback((step) => { if (step >= 1 && step <= highestUnlockedStep) setActiveStep(step) }, [highestUnlockedStep]) // ── Step content renderer ───────────────────────────────────────────────── const renderStepContent = () => { // Complete / success screen if (machine.state === machineStates.complete) { const wasScheduled = machine.lastAction === 'schedule' const studioArtworksUrl = '/studio/artworks' const artworkUrl = resolvedArtworkId ? `/art/${resolvedArtworkId}${machine.slug ? `/${machine.slug}` : ''}` : '/' const studioArtworkUrl = resolvedArtworkId ? `/studio/artworks/${resolvedArtworkId}/edit` : studioArtworksUrl return ( {wasScheduled ? '🕐' : '🎉'}

{wasScheduled ? 'Artwork scheduled!' : 'Your artwork is live!'}

{wasScheduled ? scheduledAt ? `Will publish on ${new Intl.DateTimeFormat('en-GB', { timeZone: userTimezone, weekday: 'short', day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(scheduledAt))}` : 'Your artwork is scheduled for future publishing.' : 'It has been published and is now visible to the community.'}

{!wasScheduled && ( View artwork )} View in studio Edit artwork in studio
) } if (activeStep === 1) { return ( ) } if (activeStep === 2) { return ( setMeta({ contentType: value, rootCategoryId: '', subCategoryId: '' })} onRootCategoryChange={(rootId) => setMeta({ rootCategoryId: rootId, subCategoryId: '' })} onSubCategoryChange={(subId) => setMeta({ subCategoryId: subId })} groupOptions={groupOptions} currentContributorOptions={currentContributorOptions} onGroupChange={(groupSlug) => setMeta({ group: groupSlug })} onPrimaryAuthorChange={(authorId) => setMeta({ primaryAuthorUserId: authorId ? Number(authorId) : null })} onContributorToggle={(contributorId) => setMetadata((current) => { const normalizedId = Number(contributorId) const nextIds = new Set((Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id)).filter((id) => id !== Number(current.primaryAuthorUserId))) if (nextIds.has(normalizedId)) { nextIds.delete(normalizedId) } else { nextIds.add(normalizedId) } const contributorUserIds = Array.from(nextIds).filter((id) => id !== Number(current.primaryAuthorUserId)) return { ...current, contributorUserIds, contributorCredits: normalizeContributorCredits(contributorUserIds, current.contributorCredits), } })} onContributorRoleChange={(contributorId, creditRole) => setMetadata((current) => { const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id)) if (!contributorUserIds.includes(Number(contributorId))) return current const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits) return { ...current, contributorCredits: { ...contributorCredits, [Number(contributorId)]: { ...(contributorCredits[Number(contributorId)] || { isPrimary: false }), creditRole, }, }, } })} onContributorPrimaryChange={(contributorId) => setMetadata((current) => { const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id)) const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits) contributorUserIds.forEach((id) => { contributorCredits[id] = { ...(contributorCredits[id] || { creditRole: '' }), isPrimary: id === Number(contributorId), } }) return { ...current, contributorCredits, } })} suggestedTags={mergedSuggestedTags} publishMode={publishMode} scheduledAt={scheduledAt} timezone={userTimezone} onPublishModeChange={setPublishMode} onScheduleAt={setScheduledAt} onChangeTitle={(value) => setMeta({ title: value })} onChangeTags={(value) => setMeta({ tags: value })} onChangeDescription={(value) => setMeta({ description: value })} onToggleRights={(value) => setMeta({ rightsAccepted: Boolean(value) })} /> ) } return ( setMetadata((current) => ({ ...current, worldSubmissions: (Array.isArray(current.worldSubmissions) ? current.worldSubmissions : []).map((world) => ( Number(world.id) === Number(worldId) && !world.selection_locked ? { ...world, selected: !world.selected } : world )), }))} onChangeWorldSubmissionNote={(worldId, note) => setMetadata((current) => ({ ...current, worldSubmissions: (Array.isArray(current.worldSubmissions) ? current.worldSubmissions : []).map((world) => ( Number(world.id) === Number(worldId) ? { ...world, note } : world )), }))} /> ) } // ── Action bar helpers ──────────────────────────────────────────────────── const disableReason = (() => { if (activeStep === 1) return validationErrors[0] || machine.error || 'Complete upload requirements first.' if (activeStep === 2) return metadataErrors.title || metadataErrors.contentType || metadataErrors.category || metadataErrors.rights || metadataErrors.tags || 'Complete required metadata.' return machine.error || 'Publish is available when upload is ready and rights are confirmed.' })() const publishActionTitle = activeStep < 3 ? 'Continue to the final publish step to choose Worlds and publish.' : disableReason // ───────────────────────────────────────────────────────────────────────── return (
{notices.length > 0 && (
{notices.map((notice) => (
{notice.message}
))}
)} {/* Restored draft banner */} {showRestoredBanner && (
Draft restored. Continue from your previous upload session.
)} {/* ── Studio Status Bar (sticky step header + progress) ────────────── */} {/* ── Main body: two-column on desktop ─────────────────────────────── */}
{/* Left / main column: step content */}
{/* Step content + centered progress overlay */}
{renderStepContent()} handleRetry(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })} onReset={handleReset} />
{/* Wizard action bar (nav: back/next/start/retry) */} {machine.state !== machineStates.complete && (
1 && machine.state !== machineStates.complete} canReset={Boolean(primaryFile || screenshots.length || metadata.title || metadata.description || metadata.tags.length)} canCancel={activeStep === 1 && [ machineStates.initializing, machineStates.uploading, machineStates.finishing, machineStates.processing, ].includes(machine.state)} canRetry={machine.state === machineStates.error} isUploading={[machineStates.uploading, machineStates.initializing].includes(machine.state)} isProcessing={[machineStates.processing, machineStates.finishing].includes(machine.state)} isPublishing={machine.state === machineStates.publishing} isCancelling={machine.isCancelling} disableReason={disableReason} onStart={runUploadFlow} onContinue={() => detailsValid && setActiveStep(3)} onPublish={() => publishActionEnabled && handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })} onBack={() => setActiveStep((s) => Math.max(1, s - 1))} onCancel={handleCancel} onReset={handleReset} onRetry={() => handleRetry(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })} onSaveDraft={() => {}} showSaveDraft={activeStep === 2} resetLabel={isUploadLocked ? 'Reset upload' : 'Reset'} publishLabel={publishActionLabel} mobileSticky />
)}
{/* Right column: PublishPanel (sticky sidebar on lg+, Step 2+ only) */} {(primaryFile || resolvedArtworkId) && machine.state !== machineStates.complete && activeStep > 1 && (
setMeta({ rightsAccepted: Boolean(checked) })} onPublish={() => handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })} onCancel={handleCancel} onGoToStep={goToStep} allRootCategoryOptions={allRootCategoryOptions} />
)}
) }