{/* just to balance the space-between */}
@@ -672,7 +680,11 @@ function Dashboard({
)}
-
+ {typeof logo === 'string' || logo == null ? (
+
+ ) : (
+ logo
+ )}
{middleItem && middleItem}
diff --git a/frontend/src/config/tenantRedirect.js b/frontend/src/config/tenantRedirect.js
index a91ce81a..9a820e58 100644
--- a/frontend/src/config/tenantRedirect.js
+++ b/frontend/src/config/tenantRedirect.js
@@ -134,6 +134,8 @@ const WWW_ALLOWED_PATHS = [
'/login',
];
+// /platform-admin/pivot/:tenantKey is covered by the '/platform-admin' prefix check in isPathAllowedOnWww.
+
export function isPathAllowedOnWww(pathname) {
const path = (pathname || '/').split('?')[0] || '/';
return WWW_ALLOWED_PATHS.some(allowed => {
diff --git a/frontend/src/pages/PlatformAdmin/PivotLab/IngestStatusPill.jsx b/frontend/src/pages/PlatformAdmin/PivotLab/IngestStatusPill.jsx
new file mode 100644
index 00000000..2461f355
--- /dev/null
+++ b/frontend/src/pages/PlatformAdmin/PivotLab/IngestStatusPill.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+function IngestStatusPill({ status }) {
+ if (status === 'published') {
+ return
Published ;
+ }
+ if (status === 'staged') {
+ return
Staged ;
+ }
+ if (status === 'draft') {
+ return
Draft ;
+ }
+ return
— ;
+}
+
+export default IngestStatusPill;
diff --git a/frontend/src/pages/PlatformAdmin/PivotLab/PivotCatalogEventEditModal.jsx b/frontend/src/pages/PlatformAdmin/PivotLab/PivotCatalogEventEditModal.jsx
index 57ce5525..4b2020be 100644
--- a/frontend/src/pages/PlatformAdmin/PivotLab/PivotCatalogEventEditModal.jsx
+++ b/frontend/src/pages/PlatformAdmin/PivotLab/PivotCatalogEventEditModal.jsx
@@ -52,7 +52,7 @@ export function catalogEventToEditDraft(event) {
startTimeLocal: isoToDatetimeLocal(event.start_time),
endTimeLocal: isoToDatetimeLocal(event.end_time),
timeSlots: hasShowtimes ? timeSlots : [],
- ingestStatus: event.ingestStatus || 'published',
+ ingestStatus: event.ingestStatus || 'staged',
tags: Array.isArray(event.tags) ? [...event.tags] : [],
movie: event.movie || null,
};
@@ -455,6 +455,7 @@ function PivotCatalogEventEditModal({
onChange={(e) => patchDraft({ ingestStatus: e.target.value })}
>
Published
+
Staged
Draft
diff --git a/frontend/src/pages/PlatformAdmin/PivotLab/PivotImportThumb.jsx b/frontend/src/pages/PlatformAdmin/PivotLab/PivotImportThumb.jsx
new file mode 100644
index 00000000..8331c1a2
--- /dev/null
+++ b/frontend/src/pages/PlatformAdmin/PivotLab/PivotImportThumb.jsx
@@ -0,0 +1,42 @@
+import React, { useEffect, useState } from 'react';
+
+/**
+ * Small cover thumbnail for import / catalog / curation tables.
+ * Shows a dashed placeholder when src is missing or fails to load.
+ */
+function PivotImportThumb({ src, alt }) {
+ const [failed, setFailed] = useState(false);
+
+ useEffect(() => {
+ setFailed(false);
+ }, [src]);
+
+ if (!src || failed) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
setFailed(true)}
+ />
+ );
+}
+
+export default PivotImportThumb;
diff --git a/frontend/src/pages/PlatformAdmin/PivotLab/PivotLabPage.jsx b/frontend/src/pages/PlatformAdmin/PivotLab/PivotLabPage.jsx
index aff49f05..30cc29a3 100644
--- a/frontend/src/pages/PlatformAdmin/PivotLab/PivotLabPage.jsx
+++ b/frontend/src/pages/PlatformAdmin/PivotLab/PivotLabPage.jsx
@@ -20,6 +20,8 @@ import PivotManualImportModal, {
import PivotCatalogEventEditModal, {
catalogEditDraftToOverrides,
} from './PivotCatalogEventEditModal';
+import IngestStatusPill from './IngestStatusPill';
+import PivotImportThumb from './PivotImportThumb';
import {
autoMatchTmdbMovieForEvent,
autoMatchFilmsForImportEntries,
@@ -32,6 +34,10 @@ import './PivotDeckCardPreview.scss';
const EMPTY_LIST = [];
const NO_FETCH_CACHE = { enabled: false };
const PURGE_CONFIRM_TOKEN = 'PURGE';
+// Batch tag suggestion runs one Claude call per event server-side; sending the whole
+// selection in a single request can exceed the production gateway timeout (bare 503).
+// Split it into small chunks so each request stays short and results save incrementally.
+const AI_TAG_CHUNK_SIZE = 4;
const PIVOT_JSON_IMPORT_EXAMPLE = `{
"label": "Brooklyn week crawl",
@@ -464,16 +470,18 @@ function buildDeckPreviewProps({
function isBlockingImportDuplicate(duplicate) {
if (!duplicate) return false;
- return duplicate.matchType !== 'sourceUrl';
+ // sourceUrl and fingerprint matches update an existing catalog event; only collisions
+ // between two rows of the same import batch have nothing to update against.
+ return duplicate.matchType === 'batchSourceUrl' || duplicate.matchType === 'batchFingerprint';
}
function duplicateBadgeLabel(duplicate) {
if (!duplicate) return null;
- if (duplicate.matchType === 'sourceUrl') return 'Will update';
if (duplicate.matchType === 'batchSourceUrl' || duplicate.matchType === 'batchFingerprint') {
return 'Batch duplicate';
}
- return 'Duplicate';
+ // sourceUrl (exact) or fingerprint (fuzzy) → publishing updates the existing row.
+ return 'Will update';
}
function createBatchImportRow(entry, index) {
@@ -511,6 +519,7 @@ function createBatchImportRow(entry, index) {
tags,
timeSlots,
movie,
+ aiTagged: false,
warnings: entry?.warnings || [],
duplicate,
isBlockingDuplicate,
@@ -557,16 +566,6 @@ function formatEventFilmStatus(draft) {
return '—';
}
-function IngestStatusPill({ status }) {
- if (status === 'published') {
- return
Published ;
- }
- if (status === 'draft') {
- return
Draft ;
- }
- return
— ;
-}
-
const LAB_TABS = [
{ key: 'overview', label: 'Overview' },
{ key: 'import', label: 'Import' },
@@ -578,6 +577,7 @@ function PivotLabPage() {
const { addNotification } = useNotification();
const [activeTab, setActiveTab] = useState('overview');
const [batchWeek, setBatchWeek] = useState(() => toIsoWeek());
+ const [forceBatchWeek, setForceBatchWeek] = useState(false);
const [selectedTenantKey, setSelectedTenantKey] = useState('');
const [notesDraft, setNotesDraft] = useState('');
const [notesDirty, setNotesDirty] = useState(false);
@@ -599,6 +599,7 @@ function PivotLabPage() {
const [batchApplyTags, setBatchApplyTags] = useState([]);
const [tmdbMatchLoadingKey, setTmdbMatchLoadingKey] = useState(null);
const [tagSuggestLoadingKey, setTagSuggestLoadingKey] = useState(null);
+ const [batchTagProgress, setBatchTagProgress] = useState(null);
const [tagSeeding, setTagSeeding] = useState(false);
const [importName, setImportName] = useState('');
const [importLocation, setImportLocation] = useState('');
@@ -1000,7 +1001,7 @@ function PivotLabPage() {
if (!selectedTenantKey) {
addNotification({
title: 'Choose a city',
- message: 'Select a pivot city before publishing.',
+ message: 'Select a pivot city before staging.',
type: 'warning',
});
return false;
@@ -1013,6 +1014,7 @@ function PivotLabPage() {
data: {
tenantKey: selectedTenantKey,
batchWeek,
+ forceBatchWeek,
overrides: {
hostName: entry.draft.hostName,
name: entry.draft.name,
@@ -1035,23 +1037,31 @@ function PivotLabPage() {
if (error || !data?.success) {
addNotification({
- title: 'Publish failed',
- message: error || data?.message || 'Could not publish event.',
+ title: 'Stage failed',
+ message: error || data?.message || 'Could not stage event.',
type: 'error',
});
return false;
}
+ const assignedWeek = data.data?.batchWeek || batchWeek;
refetchEvents();
refetchOverview();
addNotification({
- title: 'Published',
- message: `${data.data?.event?.name || entry.draft.name} added to ${selectedTenantKey}.`,
+ title: 'Staged',
+ message: `${data.data?.event?.name || entry.draft.name} added to ${selectedTenantKey} for ${assignedWeek} (not live until Release).`,
type: 'success',
});
return true;
},
- [addNotification, batchWeek, refetchEvents, refetchOverview, selectedTenantKey],
+ [
+ addNotification,
+ batchWeek,
+ forceBatchWeek,
+ refetchEvents,
+ refetchOverview,
+ selectedTenantKey,
+ ],
);
const suggestTagsForManualImport = useCallback(
@@ -1102,6 +1112,34 @@ function PivotLabPage() {
);
}, []);
+ // Fuzzy/exact duplicate detection for JSON entries, matched against the selected city's
+ // catalog so publishing updates existing rows instead of creating duplicates.
+ const annotateJsonEntriesWithDuplicates = useCallback(
+ async (entries) => {
+ if (!selectedTenantKey || !entries.length) {
+ return { entries, duplicateWarnings: [] };
+ }
+
+ const { data, error } = await authenticatedRequest(
+ '/admin/pivot/ingest/annotate-duplicates',
+ {
+ method: 'POST',
+ data: { tenantKey: selectedTenantKey, drafts: entries },
+ },
+ );
+
+ if (error || !data?.success) {
+ return { entries, duplicateWarnings: [] };
+ }
+
+ return {
+ entries: data.data?.drafts || entries,
+ duplicateWarnings: data.data?.duplicateWarnings || [],
+ };
+ },
+ [selectedTenantKey],
+ );
+
const handlePreviewJsonImport = useCallback(async () => {
const result = parsePivotJsonImport(importJsonDraft);
if (result.error) {
@@ -1140,10 +1178,15 @@ function PivotLabPage() {
}
}
+ const { entries: annotatedEntries, duplicateWarnings } =
+ await annotateJsonEntriesWithDuplicates(entries);
+ entries = annotatedEntries;
+
setJsonImportPreview({
label: result.label,
entries,
tmdbMatch: { matched, failed, pending: pendingFilmCount },
+ duplicateWarnings,
});
if (pendingFilmCount) {
@@ -1157,7 +1200,12 @@ function PivotLabPage() {
type: matched ? (failed ? 'warning' : 'success') : 'warning',
});
}
- }, [addNotification, importJsonDraft, syncJsonImportDraftFromEntries]);
+ }, [
+ addNotification,
+ annotateJsonEntriesWithDuplicates,
+ importJsonDraft,
+ syncJsonImportDraftFromEntries,
+ ]);
const handleLoadJsonImport = useCallback(async () => {
const result =
@@ -1201,11 +1249,17 @@ function PivotLabPage() {
}
}
+ const { entries: annotatedEntries, duplicateWarnings } =
+ await annotateJsonEntriesWithDuplicates(entries);
+ entries = annotatedEntries;
+ const updateCount = entries.filter((entry) => entry.duplicate?.willUpdate).length;
+
setImportError('');
setJsonImportPreview({
label: result.label,
entries,
tmdbMatch: { matched, failed, pending: pendingFilmCount },
+ duplicateWarnings,
});
setImportMode('batch');
setImportUrl('');
@@ -1225,6 +1279,11 @@ function PivotLabPage() {
`${failed} film event(s) could not be matched to TMDB — use Retry on those rows.`,
);
}
+ if (updateCount) {
+ loadWarnings.push(
+ `${updateCount} event(s) match existing catalog rows and will update them on publish.`,
+ );
+ }
setImportWarnings(loadWarnings);
setImportDuplicate(null);
setDeckPreviewState(null);
@@ -1235,10 +1294,18 @@ function PivotLabPage() {
message:
matched > 0
? `${entries.length} event(s) loaded · ${matched} film(s) matched from TMDB.`
- : `${entries.length} event(s) ready for review.`,
+ : `${entries.length} event(s) ready for review${
+ updateCount ? ` · ${updateCount} will update existing rows` : ''
+ }.`,
type: 'success',
});
- }, [addNotification, importJsonDraft, jsonImportPreview, syncJsonImportDraftFromEntries]);
+ }, [
+ addNotification,
+ annotateJsonEntriesWithDuplicates,
+ importJsonDraft,
+ jsonImportPreview,
+ syncJsonImportDraftFromEntries,
+ ]);
const handleMatchTmdbForJsonEntry = useCallback(
async (index) => {
@@ -1460,7 +1527,7 @@ function PivotLabPage() {
return;
}
- updateBatchImportRow(rowKey, { tags: result.tags });
+ updateBatchImportRow(rowKey, { tags: result.tags, aiTagged: true });
},
[addNotification, buildTagSuggestPayload, importBatchRows, requestSuggestedTags, updateBatchImportRow],
);
@@ -1468,58 +1535,105 @@ function PivotLabPage() {
const suggestTagsForSelectedBatchRows = useCallback(async () => {
if (!selectedBatchRows.length) return;
+ // Resume-friendly: target selected rows that haven't been through Claude yet. If every
+ // selected row is already AI-tagged, treat the click as a deliberate re-run of all of them.
+ const pending = selectedBatchRows.filter((row) => !row.aiTagged);
+ const targetRows = pending.length ? pending : selectedBatchRows;
+ const total = targetRows.length;
+
setTagSuggestLoadingKey('batch-all');
- const { data, error } = await authenticatedRequest('/admin/pivot/ingest/suggest-tags', {
- method: 'POST',
- data: {
- events: selectedBatchRows.map((row) =>
- buildTagSuggestPayload({
- name: row.name,
- description: row.description,
- location: row.location,
- organizerName: row.organizerName,
- sourceTags: row.sourceTags,
- }),
+ setBatchTagProgress({ done: 0, total });
+
+ let suggestedTotal = 0;
+ let failedTotal = 0;
+ let stoppedError = null;
+
+ // Chunk the selection into several short requests so one slow/large call can't trip the
+ // production gateway timeout, and each chunk's tags are saved as soon as it returns.
+ for (let start = 0; start < targetRows.length; start += AI_TAG_CHUNK_SIZE) {
+ const chunk = targetRows.slice(start, start + AI_TAG_CHUNK_SIZE);
+
+ const { data, error } = await authenticatedRequest('/admin/pivot/ingest/suggest-tags', {
+ method: 'POST',
+ data: {
+ events: chunk.map((row) =>
+ buildTagSuggestPayload({
+ name: row.name,
+ description: row.description,
+ location: row.location,
+ organizerName: row.organizerName,
+ sourceTags: row.sourceTags,
+ }),
+ ),
+ },
+ });
+
+ if (error || !data?.success) {
+ // A transport-level failure (gateway 503/timeout) carries no structured body. Stop
+ // here so already-processed chunks stay saved and the user can click again to resume.
+ stoppedError = {
+ message: error || data?.message || 'Could not reach the tag suggestion service.',
+ type: data?.code === 'LLM_NOT_CONFIGURED' ? 'warning' : 'error',
+ };
+ break;
+ }
+
+ const suggestions = data.data?.suggestions || [];
+ failedTotal += data.data?.failedCount ?? 0;
+
+ const tagsByKey = new Map();
+ for (let i = 0; i < chunk.length; i += 1) {
+ const tags = suggestions[i]?.tags || [];
+ if (tags.length) suggestedTotal += 1;
+ tagsByKey.set(chunk[i].key, tags);
+ }
+
+ setImportBatchRows((rows) =>
+ rows.map((row) =>
+ tagsByKey.has(row.key)
+ ? {
+ ...row,
+ tags: tagsByKey.get(row.key).length ? tagsByKey.get(row.key) : row.tags || [],
+ aiTagged: true,
+ }
+ : row,
),
- },
- });
+ );
+
+ setBatchTagProgress({ done: Math.min(start + chunk.length, total), total });
+ }
+
setTagSuggestLoadingKey(null);
+ setBatchTagProgress(null);
- if (error || !data?.success) {
+ if (stoppedError) {
addNotification({
- title: 'Batch tag suggestion failed',
- message: error || data?.message || 'Could not suggest tags.',
- type: data?.code === 'LLM_NOT_CONFIGURED' ? 'warning' : 'error',
+ title: suggestedTotal
+ ? 'Tagging interrupted — partial results saved'
+ : 'Batch tag suggestion failed',
+ message: suggestedTotal
+ ? `${suggestedTotal} row(s) tagged before the error — click again to resume the rest. (${stoppedError.message})`
+ : stoppedError.message,
+ type: suggestedTotal ? 'warning' : stoppedError.type,
});
return;
}
- const suggestions = data.data?.suggestions || [];
- const failedCount = data.data?.failedCount ?? 0;
- const suggestedCount = data.data?.suggestedCount ?? 0;
- setImportBatchRows((rows) =>
- rows.map((row) => {
- const selectedIndex = selectedBatchRows.findIndex((entry) => entry.key === row.key);
- if (selectedIndex === -1) return row;
- return { ...row, tags: suggestions[selectedIndex]?.tags || row.tags || [] };
- }),
- );
-
- if (suggestedCount === 0) {
+ if (suggestedTotal === 0) {
addNotification({
title: 'No tags suggested',
- message: data.data?.failures?.[0]?.message || 'Claude did not return valid catalog tags.',
+ message: 'Claude did not return valid catalog tags for the selected rows.',
type: 'warning',
});
return;
}
addNotification({
- title: failedCount ? 'Batch partially tagged' : 'Batch tags suggested',
- message: failedCount
- ? `${suggestedCount} row(s) tagged, ${failedCount} failed.`
- : `${suggestedCount} row(s) tagged via Claude.`,
- type: failedCount ? 'warning' : 'success',
+ title: failedTotal ? 'Batch partially tagged' : 'Batch tags suggested',
+ message: failedTotal
+ ? `${suggestedTotal} row(s) tagged, ${failedTotal} failed.`
+ : `${suggestedTotal} row(s) tagged via Claude.`,
+ type: failedTotal ? 'warning' : 'success',
});
}, [addNotification, buildTagSuggestPayload, selectedBatchRows]);
@@ -1642,6 +1756,7 @@ function PivotLabPage() {
data: {
tenantKey: selectedTenantKey,
batchWeek,
+ forceBatchWeek,
events: publishableBatchRows.map((row) => ({
url: row.sourceUrl.trim() || undefined,
overrides: buildBatchPublishOverrides(row),
@@ -1651,25 +1766,171 @@ function PivotLabPage() {
setImportPublishLoading(false);
if (error || !data?.success) {
- setImportError(error || data?.message || 'Could not publish selected events.');
+ setImportError(error || data?.message || 'Could not stage selected events.');
return;
}
const publishedCount = data.data?.publishedCount ?? data.data?.published?.length ?? 0;
const failedCount = data.data?.failedCount ?? data.data?.failures?.length ?? 0;
+ const updatedCount = data.data?.updatedCount ?? 0;
+ const createdCount = Math.max(publishedCount - updatedCount, 0);
+ const updatedSuffix = updatedCount ? ` (${updatedCount} updated existing)` : '';
+ const weekCounts = data.data?.batchWeekCounts || {};
+ const weekKeys = Object.keys(weekCounts).sort();
+ const weekSuffix =
+ weekKeys.length > 1
+ ? ` Weeks: ${weekKeys.map((w) => `${w} (${weekCounts[w]})`).join(', ')}.`
+ : weekKeys.length === 1
+ ? ` Week ${weekKeys[0]}.`
+ : '';
refetchEvents();
refetchOverview();
addNotification({
- title: failedCount ? 'Batch partially published' : 'Batch published',
+ title: failedCount ? 'Batch partially staged' : 'Batch staged',
message: failedCount
- ? `${publishedCount} event(s) published, ${failedCount} failed.`
- : `${publishedCount} event(s) added to ${selectedTenantKey}.`,
+ ? `${createdCount} staged, ${updatedCount} updated, ${failedCount} failed.${weekSuffix} Release separately to go live.`
+ : `${publishedCount} event(s) staged for ${selectedTenantKey}${updatedSuffix}.${weekSuffix} Not live until Release.`,
type: failedCount ? 'warning' : 'success',
});
}, [
addNotification,
batchWeek,
+ forceBatchWeek,
+ publishableBatchRows,
+ refetchEvents,
+ refetchOverview,
+ selectedTenantKey,
+ ]);
+
+ const handleStageAndReleaseNow = useCallback(async () => {
+ if (importMode === 'batch') {
+ if (!publishableBatchRows.length || !selectedTenantKey) {
+ setImportError('Select events with title, organizer, location, start time, and at least one tag.');
+ return;
+ }
+ const typed = window.prompt(
+ 'Emergency: stage & release now puts events in the live feed immediately.\n\nType RELEASE_NOW to confirm:',
+ );
+ if (typed !== 'RELEASE_NOW') {
+ if (typed != null) {
+ setImportError('Release cancelled — type RELEASE_NOW exactly to confirm.');
+ }
+ return;
+ }
+
+ setImportPublishLoading(true);
+ setImportError('');
+ const { data, error } = await authenticatedRequest('/admin/pivot/ingest/batch', {
+ method: 'POST',
+ data: {
+ tenantKey: selectedTenantKey,
+ batchWeek,
+ forceBatchWeek,
+ releaseNow: true,
+ confirm: 'RELEASE_NOW',
+ events: publishableBatchRows.map((row) => ({
+ url: row.sourceUrl.trim() || undefined,
+ overrides: buildBatchPublishOverrides(row),
+ })),
+ },
+ });
+ setImportPublishLoading(false);
+
+ if (error || !data?.success) {
+ setImportError(error || data?.message || 'Could not release selected events.');
+ return;
+ }
+
+ const publishedCount = data.data?.publishedCount ?? data.data?.published?.length ?? 0;
+ const weekCounts = data.data?.batchWeekCounts || {};
+ const weekKeys = Object.keys(weekCounts).sort();
+ const weekLabel =
+ weekKeys.length > 1
+ ? weekKeys.map((w) => `${w} (${weekCounts[w]})`).join(', ')
+ : weekKeys[0] || batchWeek;
+ refetchEvents();
+ refetchOverview();
+ addNotification({
+ title: 'Released to deck',
+ message: `${publishedCount} event(s) live in ${selectedTenantKey} for ${weekLabel}.`,
+ type: 'success',
+ });
+ return;
+ }
+
+ if (!importPreview || !selectedTenantKey) {
+ setImportError('Preview an event and choose a city before releasing.');
+ return;
+ }
+ if (!importOrganizerName.trim()) {
+ setImportError('Organizer name is required.');
+ return;
+ }
+ if (!importSelectedTags.length) {
+ setImportError('Select at least one catalog tag.');
+ return;
+ }
+
+ const typed = window.prompt(
+ 'Emergency: stage & release now puts this event in the live feed immediately.\n\nType RELEASE_NOW to confirm:',
+ );
+ if (typed !== 'RELEASE_NOW') {
+ if (typed != null) {
+ setImportError('Release cancelled — type RELEASE_NOW exactly to confirm.');
+ }
+ return;
+ }
+
+ setImportPublishLoading(true);
+ setImportError('');
+ const { data, error } = await authenticatedRequest('/admin/pivot/ingest', {
+ method: 'POST',
+ data: {
+ tenantKey: selectedTenantKey,
+ url: importUrl.trim(),
+ batchWeek,
+ forceBatchWeek,
+ releaseNow: true,
+ confirm: 'RELEASE_NOW',
+ overrides: {
+ hostName: importOrganizerName.trim(),
+ name: importName.trim() || undefined,
+ location: importLocation.trim() || undefined,
+ start_time: importStartTime.trim() || undefined,
+ description: importDescription.trim() || undefined,
+ tags: importSelectedTags,
+ },
+ },
+ });
+ setImportPublishLoading(false);
+
+ if (error || !data?.success) {
+ setImportError(error || data?.message || 'Could not release event.');
+ return;
+ }
+
+ const assignedWeek = data.data?.batchWeek || batchWeek;
+ refetchEvents();
+ refetchOverview();
+ addNotification({
+ title: 'Released to deck',
+ message: `${data.data?.event?.name || 'Event'} is live in ${selectedTenantKey} for ${assignedWeek}.`,
+ type: 'success',
+ });
+ }, [
+ addNotification,
+ batchWeek,
+ forceBatchWeek,
+ importDescription,
+ importLocation,
+ importMode,
+ importName,
+ importOrganizerName,
+ importPreview,
+ importSelectedTags,
+ importStartTime,
+ importUrl,
publishableBatchRows,
refetchEvents,
refetchOverview,
@@ -1682,7 +1943,7 @@ function PivotLabPage() {
}
if (!importPreview || !selectedTenantKey) {
- setImportError('Preview an event and choose a city before publishing.');
+ setImportError('Preview an event and choose a city before staging.');
return;
}
if (!importOrganizerName.trim()) {
@@ -1703,6 +1964,7 @@ function PivotLabPage() {
tenantKey: selectedTenantKey,
url: importUrl.trim(),
batchWeek,
+ forceBatchWeek,
overrides: {
hostName: importOrganizerName.trim(),
name: importName.trim() || undefined,
@@ -1716,20 +1978,25 @@ function PivotLabPage() {
setImportPublishLoading(false);
if (error || !data?.success) {
- setImportError(error || data?.message || 'Could not publish event.');
+ setImportError(error || data?.message || 'Could not stage event.');
return;
}
refetchEvents();
refetchOverview();
+ const wasUpdated = data.data?.updated;
+ const assignedWeek = data.data?.batchWeek || batchWeek;
addNotification({
- title: 'Published',
- message: `${data.data?.event?.name || 'Event'} added to ${selectedTenantKey}.`,
+ title: wasUpdated ? 'Updated' : 'Staged',
+ message: `${data.data?.event?.name || 'Event'} ${
+ wasUpdated ? 'updated in' : 'staged for'
+ } ${selectedTenantKey} (${assignedWeek}). Not live until Release.`,
type: 'success',
});
}, [
addNotification,
batchWeek,
+ forceBatchWeek,
handlePublishBatchImport,
importDescription,
importLocation,
@@ -1859,6 +2126,17 @@ function PivotLabPage() {