@elevasis/ui 2.71.0 → 2.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +3 -3
- package/dist/app/index.d.ts +116 -22
- package/dist/app/index.js +8 -8
- package/dist/auth/index.js +4 -4
- package/dist/charts/index.js +4 -4
- package/dist/{chunk-DCMFNSJ6.js → chunk-3TS5U5BL.js} +1 -1
- package/dist/{chunk-VRRIHA3S.js → chunk-4CW2C5BD.js} +1 -1
- package/dist/{chunk-26BTE3Z4.js → chunk-5B2ZFO44.js} +1 -1
- package/dist/{chunk-BM2WKRAQ.js → chunk-AJOVWB47.js} +3 -3
- package/dist/{chunk-DH2MO5DX.js → chunk-CJ2232PQ.js} +1 -1
- package/dist/{chunk-TVFSVTRG.js → chunk-ETN3TTRT.js} +7 -7
- package/dist/{chunk-NM4A63KJ.js → chunk-IBCEAVB2.js} +56 -20
- package/dist/{chunk-3BLYJT5L.js → chunk-IGBPWD6S.js} +3 -3
- package/dist/{chunk-TB2PM2OU.js → chunk-JYBJMVNY.js} +1 -1
- package/dist/{chunk-TCQZ4AJI.js → chunk-LEPYXLJ2.js} +1 -1
- package/dist/{chunk-H3SDGC5F.js → chunk-LZT2H2MA.js} +4 -4
- package/dist/{chunk-VDX6DL7Z.js → chunk-NHNTX6ZL.js} +1 -1
- package/dist/{chunk-VHPHWTX7.js → chunk-NI37YNRP.js} +4 -4
- package/dist/{chunk-UZ7MSORW.js → chunk-OCVSPAKM.js} +540 -29
- package/dist/{chunk-LL5H7J4N.js → chunk-PQ4B62HH.js} +2 -2
- package/dist/{chunk-CPBT6Z2G.js → chunk-SWKAH2YM.js} +52 -20
- package/dist/{chunk-C75XWBYZ.js → chunk-U2H3TIHD.js} +1 -1
- package/dist/{chunk-AIOV5NM7.js → chunk-Z4ICFQXG.js} +1 -1
- package/dist/{chunk-JTSNU6KA.js → chunk-ZDKPHFTH.js} +3 -3
- package/dist/components/index.d.ts +307 -24
- package/dist/components/index.js +316 -26
- package/dist/components/navigation/index.js +5 -5
- package/dist/features/auth/index.d.ts +76 -0
- package/dist/features/auth/index.js +6 -6
- package/dist/features/clients/index.js +5 -5
- package/dist/features/content/index.d.ts +586 -66
- package/dist/features/content/index.js +1213 -312
- package/dist/features/crm/index.d.ts +117 -22
- package/dist/features/crm/index.js +6 -6
- package/dist/features/dashboard/index.d.ts +20 -11
- package/dist/features/dashboard/index.js +6 -6
- package/dist/features/delivery/index.d.ts +76 -0
- package/dist/features/delivery/index.js +5 -5
- package/dist/features/knowledge/index.js +4 -4
- package/dist/features/lead-gen/index.d.ts +41 -22
- package/dist/features/lead-gen/index.js +6 -6
- package/dist/features/monitoring/index.js +7 -7
- package/dist/features/monitoring/requests/index.js +5 -5
- package/dist/features/operations/index.d.ts +40 -22
- package/dist/features/operations/index.js +9 -9
- package/dist/features/settings/index.d.ts +76 -0
- package/dist/features/settings/index.js +6 -6
- package/dist/hooks/access/index.js +4 -4
- package/dist/hooks/delivery/index.d.ts +76 -0
- package/dist/hooks/delivery/index.js +2 -2
- package/dist/hooks/index.d.ts +1259 -32
- package/dist/hooks/index.js +4 -4
- package/dist/hooks/operations/command-view/utils/transformCommandViewData.d.ts +20 -11
- package/dist/hooks/published.d.ts +1259 -32
- package/dist/hooks/published.js +4 -4
- package/dist/initialization/index.d.ts +76 -0
- package/dist/knowledge/index.d.ts +40 -22
- package/dist/knowledge/index.js +18 -18
- package/dist/{knowledge-search-index-LP2WVSQX.js → knowledge-search-index-RZLIVWPJ.js} +5 -5
- package/dist/layout/index.js +7 -7
- package/dist/profile/index.d.ts +76 -0
- package/dist/provider/index.d.ts +116 -22
- package/dist/provider/index.js +4 -4
- package/dist/provider/published.d.ts +116 -22
- package/dist/provider/published.js +4 -4
- package/dist/supabase/index.d.ts +149 -0
- package/dist/test-utils/index.js +3 -3
- package/dist/types/index.d.ts +126 -14
- package/dist/utils/index.d.ts +20 -11
- package/dist/utils/index.js +1 -1
- package/package.json +5 -5
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { PageContainer } from '../../chunk-BZZCNLT6.js';
|
|
2
2
|
import '../../chunk-TUMSNGTX.js';
|
|
3
3
|
import { FilterBar } from '../../chunk-PDHTXPSF.js';
|
|
4
|
-
import { CustomModal, useContentItems, useContentDistributions, useContentConfig, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState,
|
|
5
|
-
export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-
|
|
4
|
+
import { CustomModal, useContentItemSourceAssetDetails, useContentSourceAssets, useAddContentItemSourceAsset, useRemoveContentItemSourceAsset, useReorderContentItemSourceAssets, useUpdateContentItemSourceAsset, useCloudStorageTemporaryLinks, useContentItemAttempts, useExecuteAsync, useUpdateContentItem, useExecution, contentKeys, useContentItems, useContentDistributions, useContentConfig, isAwaitingReview, SubshellContentContainer, PageTitleCaption, CenteredErrorState, EmptyState, StatCard, getContentItemIdentity, usePaginationState, resolveCurrentContentStep, useContentItem, useReviewContentItem, resolveReviewTarget, useContentDistribution, useStorageSignedUrls, storageObjectRefKey, useContentPipelineSummary, deriveContentBoard, resolveContentWorkspaceRoute, useExecutions } from '../../chunk-OCVSPAKM.js';
|
|
5
|
+
export { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle, DEFAULT_CONTENT_VOCABULARY, MyReviewQueuePanel, contentManifest, deriveContentBoard, getContentItemIdentity, resolveContentStepResource, resolveContentVocabulary, useContentConfig } from '../../chunk-OCVSPAKM.js';
|
|
6
6
|
import '../../chunk-R6NMRXNO.js';
|
|
7
7
|
import '../../chunk-RBGVNPA6.js';
|
|
8
8
|
import '../../chunk-MKH2KOAO.js';
|
|
9
|
-
import '../../chunk-
|
|
9
|
+
import '../../chunk-NHNTX6ZL.js';
|
|
10
10
|
import '../../chunk-L7BZZ4SI.js';
|
|
11
|
-
import { showSuccessNotification } from '../../chunk-
|
|
11
|
+
import { showSuccessNotification } from '../../chunk-U2H3TIHD.js';
|
|
12
12
|
import '../../chunk-MISHPEJX.js';
|
|
13
13
|
import { CardHeader } from '../../chunk-62GVNH5U.js';
|
|
14
14
|
import '../../chunk-P45GQ3ZW.js';
|
|
@@ -20,7 +20,7 @@ import '../../chunk-UD657ICS.js';
|
|
|
20
20
|
import '../../chunk-DD3CCMCZ.js';
|
|
21
21
|
import '../../chunk-RNP5R5I3.js';
|
|
22
22
|
import '../../chunk-RT4KRGZT.js';
|
|
23
|
-
import { formatTimeAgo, formatRelativeTime } from '../../chunk-
|
|
23
|
+
import { formatTimeAgo, formatRelativeTime } from '../../chunk-IBCEAVB2.js';
|
|
24
24
|
import '../../chunk-Y6I3IC45.js';
|
|
25
25
|
import '../../chunk-6SSQGWK7.js';
|
|
26
26
|
import '../../chunk-BI5VDO2K.js';
|
|
@@ -29,12 +29,13 @@ import '../../chunk-TVTSASST.js';
|
|
|
29
29
|
import '../../chunk-BRJ3QZ4E.js';
|
|
30
30
|
import '../../chunk-Q7DJKLEN.js';
|
|
31
31
|
import '../../chunk-I2KLQ2HA.js';
|
|
32
|
-
import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper,
|
|
33
|
-
import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay,
|
|
32
|
+
import { Stack, Group, Text, Badge, Button, Textarea, Box, Loader, Accordion, Tooltip, Card, Alert, List, Paper, SegmentedControl, Slider, Title, Table, TextInput, Select, Modal, ActionIcon, Center, SimpleGrid, Pagination, Tabs, Anchor, Divider, Progress, Image as Image$1, UnstyledButton, ScrollArea } from '@mantine/core';
|
|
33
|
+
import { IconAlertTriangle, IconShieldCheck, IconShieldX, IconPhoto, IconGripVertical, IconCrop, IconArrowUp, IconArrowDown, IconUnlink, IconLink, IconPhotoOff, IconChevronLeft, IconChevronRight, IconCheck, IconSparkles, IconEye, IconArticle, IconArrowRight, IconClock, IconSend, IconSearch, IconFileText, IconExternalLink, IconListDetails, IconAlertCircle, IconLayoutGrid, IconShare, IconPlayerPlay, IconCircleCheck, IconCircleDashed, IconMaximize, IconCopy, IconDownload, IconArrowLeft, IconArrowsExchange, IconX, IconChecklist, IconRoute, IconInfoCircle, IconClockHour4 } from '@tabler/icons-react';
|
|
34
34
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
35
|
-
import { useState, useMemo, Fragment as Fragment$1
|
|
35
|
+
import { useState, useRef, useCallback, useMemo, useEffect, Fragment as Fragment$1 } from 'react';
|
|
36
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
37
|
+
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
|
|
36
38
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
37
|
-
import { useDisclosure } from '@mantine/hooks';
|
|
38
39
|
|
|
39
40
|
function renderFieldValue(value, kind) {
|
|
40
41
|
if (value === void 0 || value === null || value === "") {
|
|
@@ -425,7 +426,8 @@ function ContentReviewCard({
|
|
|
425
426
|
processingSteps = [],
|
|
426
427
|
actions,
|
|
427
428
|
onAction,
|
|
428
|
-
actionsPending = false
|
|
429
|
+
actionsPending = false,
|
|
430
|
+
readOnly = false
|
|
429
431
|
}) {
|
|
430
432
|
return /* @__PURE__ */ jsx(Card, { withBorder: true, radius: "md", padding: "xl", "data-testid": "content-review-card", children: /* @__PURE__ */ jsxs(Stack, { gap: "lg", children: [
|
|
431
433
|
title || processingSteps.length > 0 ? /* @__PURE__ */ jsxs(Group, { justify: "space-between", align: "flex-start", wrap: "nowrap", children: [
|
|
@@ -466,10 +468,10 @@ function ContentReviewCard({
|
|
|
466
468
|
onReset,
|
|
467
469
|
payload,
|
|
468
470
|
payloadFields,
|
|
469
|
-
disabled: actionsPending
|
|
471
|
+
disabled: actionsPending || readOnly
|
|
470
472
|
}
|
|
471
473
|
),
|
|
472
|
-
/* @__PURE__ */ jsx(
|
|
474
|
+
readOnly ? null : /* @__PURE__ */ jsx(
|
|
473
475
|
AlternatesPanel,
|
|
474
476
|
{
|
|
475
477
|
body,
|
|
@@ -535,6 +537,1007 @@ function ContentReviewCard({
|
|
|
535
537
|
/* @__PURE__ */ jsx(ReviewActionBar, { actions, onAction, pending: actionsPending })
|
|
536
538
|
] }) });
|
|
537
539
|
}
|
|
540
|
+
function moveItem(items, fromIndex, toIndex) {
|
|
541
|
+
const next = items.slice();
|
|
542
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
543
|
+
next.splice(toIndex, 0, moved);
|
|
544
|
+
return next;
|
|
545
|
+
}
|
|
546
|
+
function SelectedAssetStrip({ assets, onReorder }) {
|
|
547
|
+
const [dragOverIndex, setDragOverIndex] = useState(null);
|
|
548
|
+
const draggingIndexRef = useRef(null);
|
|
549
|
+
const handleDragStart = useCallback((e, index) => {
|
|
550
|
+
draggingIndexRef.current = index;
|
|
551
|
+
e.dataTransfer.effectAllowed = "move";
|
|
552
|
+
e.dataTransfer.setData("text/plain", String(index));
|
|
553
|
+
}, []);
|
|
554
|
+
const handleDragOver = useCallback((e, index) => {
|
|
555
|
+
e.preventDefault();
|
|
556
|
+
e.dataTransfer.dropEffect = "move";
|
|
557
|
+
setDragOverIndex(index);
|
|
558
|
+
}, []);
|
|
559
|
+
const handleDragLeave = useCallback(() => {
|
|
560
|
+
setDragOverIndex(null);
|
|
561
|
+
}, []);
|
|
562
|
+
const handleDrop = useCallback(
|
|
563
|
+
(e, targetIndex) => {
|
|
564
|
+
e.preventDefault();
|
|
565
|
+
setDragOverIndex(null);
|
|
566
|
+
const sourceIndex = draggingIndexRef.current;
|
|
567
|
+
draggingIndexRef.current = null;
|
|
568
|
+
if (sourceIndex === null || sourceIndex === targetIndex) return;
|
|
569
|
+
onReorder(moveItem(assets, sourceIndex, targetIndex));
|
|
570
|
+
},
|
|
571
|
+
[assets, onReorder]
|
|
572
|
+
);
|
|
573
|
+
const handleDragEnd = useCallback(() => {
|
|
574
|
+
draggingIndexRef.current = null;
|
|
575
|
+
setDragOverIndex(null);
|
|
576
|
+
}, []);
|
|
577
|
+
if (assets.length === 0) {
|
|
578
|
+
return /* @__PURE__ */ jsx(
|
|
579
|
+
Paper,
|
|
580
|
+
{
|
|
581
|
+
withBorder: true,
|
|
582
|
+
p: "md",
|
|
583
|
+
"data-testid": "selected-asset-strip-empty",
|
|
584
|
+
style: { borderStyle: "dashed", borderColor: "var(--color-border)" },
|
|
585
|
+
children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", ta: "center", children: "No assets selected yet." })
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return /* @__PURE__ */ jsx(Group, { gap: "sm", wrap: "wrap", "data-testid": "selected-asset-strip", role: "list", children: assets.map((asset, index) => {
|
|
590
|
+
const isCover = index === 0;
|
|
591
|
+
const isDragTarget = dragOverIndex === index;
|
|
592
|
+
return /* @__PURE__ */ jsx(
|
|
593
|
+
Paper,
|
|
594
|
+
{
|
|
595
|
+
withBorder: true,
|
|
596
|
+
role: "listitem",
|
|
597
|
+
"data-testid": `selected-asset-strip-item-${asset.id}`,
|
|
598
|
+
draggable: assets.length > 1,
|
|
599
|
+
onDragStart: (e) => handleDragStart(e, index),
|
|
600
|
+
onDragOver: (e) => handleDragOver(e, index),
|
|
601
|
+
onDragLeave: handleDragLeave,
|
|
602
|
+
onDrop: (e) => handleDrop(e, index),
|
|
603
|
+
onDragEnd: handleDragEnd,
|
|
604
|
+
style: {
|
|
605
|
+
width: 120,
|
|
606
|
+
overflow: "hidden",
|
|
607
|
+
cursor: assets.length > 1 ? "grab" : "default",
|
|
608
|
+
userSelect: "none",
|
|
609
|
+
borderColor: isDragTarget ? "var(--color-primary)" : "var(--color-border)",
|
|
610
|
+
transition: `border-color var(--duration-fast) var(--easing)`
|
|
611
|
+
},
|
|
612
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: 4, p: 6, children: [
|
|
613
|
+
/* @__PURE__ */ jsxs(
|
|
614
|
+
Box,
|
|
615
|
+
{
|
|
616
|
+
style: {
|
|
617
|
+
position: "relative",
|
|
618
|
+
width: "100%",
|
|
619
|
+
aspectRatio: "1 / 1",
|
|
620
|
+
borderRadius: 4,
|
|
621
|
+
overflow: "hidden",
|
|
622
|
+
backgroundColor: "var(--color-surface)"
|
|
623
|
+
},
|
|
624
|
+
children: [
|
|
625
|
+
asset.thumbnailUrl ? /* @__PURE__ */ jsx(
|
|
626
|
+
"img",
|
|
627
|
+
{
|
|
628
|
+
src: asset.thumbnailUrl,
|
|
629
|
+
alt: asset.name,
|
|
630
|
+
style: { width: "100%", height: "100%", objectFit: "cover", display: "block" }
|
|
631
|
+
}
|
|
632
|
+
) : /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsx(IconPhoto, { size: 24, color: "var(--color-textDimmed)" }) }),
|
|
633
|
+
isCover && /* @__PURE__ */ jsx(
|
|
634
|
+
Badge,
|
|
635
|
+
{
|
|
636
|
+
size: "xs",
|
|
637
|
+
variant: "filled",
|
|
638
|
+
"data-testid": `selected-asset-strip-cover-${asset.id}`,
|
|
639
|
+
style: { position: "absolute", top: 4, left: 4 },
|
|
640
|
+
children: "Cover"
|
|
641
|
+
}
|
|
642
|
+
),
|
|
643
|
+
assets.length > 1 && /* @__PURE__ */ jsx(Box, { style: { position: "absolute", top: 4, right: 4, color: "var(--color-textDimmed)" }, children: /* @__PURE__ */ jsx(IconGripVertical, { size: 16 }) })
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
),
|
|
647
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", lineClamp: 1, title: asset.name, children: asset.name })
|
|
648
|
+
] })
|
|
649
|
+
},
|
|
650
|
+
asset.id
|
|
651
|
+
);
|
|
652
|
+
}) });
|
|
653
|
+
}
|
|
654
|
+
var CROP_ASPECT_OPTIONS = [
|
|
655
|
+
{ value: "original", label: "Original", ratio: null },
|
|
656
|
+
{ value: "portrait", label: "4:5", ratio: 4 / 5 },
|
|
657
|
+
{ value: "square", label: "1:1", ratio: 1 },
|
|
658
|
+
{ value: "landscape", label: "1.91:1", ratio: 1.91 }
|
|
659
|
+
];
|
|
660
|
+
var MIN_CROP_SCALE = 0.2;
|
|
661
|
+
function fitBox(pixelRatio, naturalWidth, naturalHeight) {
|
|
662
|
+
if (pixelRatio === null || naturalWidth === 0 || naturalHeight === 0) return { width: 1, height: 1 };
|
|
663
|
+
const normalizedRatio = pixelRatio * naturalHeight / naturalWidth;
|
|
664
|
+
return normalizedRatio >= 1 ? { width: 1, height: 1 / normalizedRatio } : { width: normalizedRatio, height: 1 };
|
|
665
|
+
}
|
|
666
|
+
function clamp(value, min, max) {
|
|
667
|
+
return Math.min(Math.max(value, min), max);
|
|
668
|
+
}
|
|
669
|
+
function round(value) {
|
|
670
|
+
return Math.round(value * 1e4) / 1e4;
|
|
671
|
+
}
|
|
672
|
+
function AssetCropEditor({
|
|
673
|
+
imageUrl,
|
|
674
|
+
value,
|
|
675
|
+
onChange,
|
|
676
|
+
defaultAspect = "original",
|
|
677
|
+
aspect,
|
|
678
|
+
disabled = false,
|
|
679
|
+
label
|
|
680
|
+
}) {
|
|
681
|
+
const [internalAspect, setInternalAspect] = useState(defaultAspect);
|
|
682
|
+
const activeAspect = aspect ?? internalAspect;
|
|
683
|
+
const [natural, setNatural] = useState(null);
|
|
684
|
+
const frameRef = useRef(null);
|
|
685
|
+
const dragOffsetRef = useRef(null);
|
|
686
|
+
const ratio = useMemo(
|
|
687
|
+
() => CROP_ASPECT_OPTIONS.find((option) => option.value === activeAspect)?.ratio ?? null,
|
|
688
|
+
[activeAspect]
|
|
689
|
+
);
|
|
690
|
+
const maxBox = useMemo(
|
|
691
|
+
() => fitBox(ratio, natural?.width ?? 0, natural?.height ?? 0),
|
|
692
|
+
[ratio, natural?.width, natural?.height]
|
|
693
|
+
);
|
|
694
|
+
const crop = value ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
695
|
+
const scale = maxBox.width === 0 ? 1 : clamp(crop.width / maxBox.width, MIN_CROP_SCALE, 1);
|
|
696
|
+
const emit = useCallback(
|
|
697
|
+
(next) => {
|
|
698
|
+
const width = clamp(next.width, 0.01, 1);
|
|
699
|
+
const height = clamp(next.height, 0.01, 1);
|
|
700
|
+
onChange({
|
|
701
|
+
x: round(clamp(next.x, 0, 1 - width)),
|
|
702
|
+
y: round(clamp(next.y, 0, 1 - height)),
|
|
703
|
+
width: round(width),
|
|
704
|
+
height: round(height)
|
|
705
|
+
});
|
|
706
|
+
},
|
|
707
|
+
[onChange]
|
|
708
|
+
);
|
|
709
|
+
const refit = useCallback(
|
|
710
|
+
(nextScale) => {
|
|
711
|
+
const width = maxBox.width * nextScale;
|
|
712
|
+
const height = maxBox.height * nextScale;
|
|
713
|
+
emit({ x: (1 - width) / 2, y: (1 - height) / 2, width, height });
|
|
714
|
+
},
|
|
715
|
+
[emit, maxBox.height, maxBox.width]
|
|
716
|
+
);
|
|
717
|
+
const fittedRatioRef = useRef(void 0);
|
|
718
|
+
useEffect(() => {
|
|
719
|
+
if (natural === null || disabled) return;
|
|
720
|
+
const isFirstMeasurement = fittedRatioRef.current === void 0;
|
|
721
|
+
fittedRatioRef.current = ratio;
|
|
722
|
+
if (isFirstMeasurement && value !== null) return;
|
|
723
|
+
if (ratio === null && value === null) return;
|
|
724
|
+
refit(scale);
|
|
725
|
+
}, [ratio, natural?.width, natural?.height]);
|
|
726
|
+
const handlePointerDown = (event) => {
|
|
727
|
+
if (disabled || frameRef.current === null) return;
|
|
728
|
+
const frame = frameRef.current.getBoundingClientRect();
|
|
729
|
+
dragOffsetRef.current = {
|
|
730
|
+
x: (event.clientX - frame.left) / frame.width - crop.x,
|
|
731
|
+
y: (event.clientY - frame.top) / frame.height - crop.y
|
|
732
|
+
};
|
|
733
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
734
|
+
};
|
|
735
|
+
const handlePointerMove = (event) => {
|
|
736
|
+
const offset = dragOffsetRef.current;
|
|
737
|
+
if (offset === null || frameRef.current === null) return;
|
|
738
|
+
const frame = frameRef.current.getBoundingClientRect();
|
|
739
|
+
emit({
|
|
740
|
+
x: (event.clientX - frame.left) / frame.width - offset.x,
|
|
741
|
+
y: (event.clientY - frame.top) / frame.height - offset.y,
|
|
742
|
+
width: crop.width,
|
|
743
|
+
height: crop.height
|
|
744
|
+
});
|
|
745
|
+
};
|
|
746
|
+
const endDrag = (event) => {
|
|
747
|
+
dragOffsetRef.current = null;
|
|
748
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
749
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const handleKeyDown = (event) => {
|
|
753
|
+
if (disabled) return;
|
|
754
|
+
const step = 0.01;
|
|
755
|
+
const nudge = {
|
|
756
|
+
ArrowLeft: [-step, 0],
|
|
757
|
+
ArrowRight: [step, 0],
|
|
758
|
+
ArrowUp: [0, -step],
|
|
759
|
+
ArrowDown: [0, step]
|
|
760
|
+
};
|
|
761
|
+
const delta = nudge[event.key];
|
|
762
|
+
if (delta === void 0) return;
|
|
763
|
+
event.preventDefault();
|
|
764
|
+
emit({ x: crop.x + delta[0], y: crop.y + delta[1], width: crop.width, height: crop.height });
|
|
765
|
+
};
|
|
766
|
+
const croppedPixels = natural === null ? null : `${Math.round(crop.width * natural.width)}x${Math.round(crop.height * natural.height)}`;
|
|
767
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "xs", "data-testid": "asset-crop-editor", children: [
|
|
768
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
|
|
769
|
+
label !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, children: label }),
|
|
770
|
+
croppedPixels !== null && /* @__PURE__ */ jsx(Badge, { variant: "light", size: "sm", children: croppedPixels })
|
|
771
|
+
] }),
|
|
772
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden", position: "relative" }, children: /* @__PURE__ */ jsxs("div", { ref: frameRef, style: { position: "relative", lineHeight: 0 }, children: [
|
|
773
|
+
imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "xl", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
|
|
774
|
+
"img",
|
|
775
|
+
{
|
|
776
|
+
src: imageUrl,
|
|
777
|
+
alt: "",
|
|
778
|
+
style: { width: "100%", display: "block" },
|
|
779
|
+
onLoad: (event) => setNatural({
|
|
780
|
+
width: event.currentTarget.naturalWidth,
|
|
781
|
+
height: event.currentTarget.naturalHeight
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
),
|
|
785
|
+
imageUrl !== null && /* @__PURE__ */ jsx(
|
|
786
|
+
"div",
|
|
787
|
+
{
|
|
788
|
+
role: "slider",
|
|
789
|
+
tabIndex: disabled ? -1 : 0,
|
|
790
|
+
"aria-label": `Crop area${label === void 0 ? "" : ` for ${label}`}`,
|
|
791
|
+
"aria-valuetext": `x ${Math.round(crop.x * 100)}%, y ${Math.round(crop.y * 100)}%, ${Math.round(
|
|
792
|
+
crop.width * 100
|
|
793
|
+
)}% wide`,
|
|
794
|
+
"data-testid": "asset-crop-box",
|
|
795
|
+
onPointerDown: handlePointerDown,
|
|
796
|
+
onPointerMove: handlePointerMove,
|
|
797
|
+
onPointerUp: endDrag,
|
|
798
|
+
onPointerCancel: endDrag,
|
|
799
|
+
onKeyDown: handleKeyDown,
|
|
800
|
+
style: {
|
|
801
|
+
position: "absolute",
|
|
802
|
+
left: `${crop.x * 100}%`,
|
|
803
|
+
top: `${crop.y * 100}%`,
|
|
804
|
+
width: `${crop.width * 100}%`,
|
|
805
|
+
height: `${crop.height * 100}%`,
|
|
806
|
+
border: "2px solid var(--mantine-color-blue-5)",
|
|
807
|
+
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.45)",
|
|
808
|
+
cursor: disabled ? "default" : "move",
|
|
809
|
+
touchAction: "none"
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
)
|
|
813
|
+
] }) }),
|
|
814
|
+
/* @__PURE__ */ jsxs(Group, { gap: "sm", align: "center", wrap: "wrap", children: [
|
|
815
|
+
aspect === void 0 && /* @__PURE__ */ jsx(
|
|
816
|
+
SegmentedControl,
|
|
817
|
+
{
|
|
818
|
+
size: "xs",
|
|
819
|
+
disabled,
|
|
820
|
+
value: activeAspect,
|
|
821
|
+
onChange: setInternalAspect,
|
|
822
|
+
data: CROP_ASPECT_OPTIONS.map((option) => ({ value: option.value, label: option.label }))
|
|
823
|
+
}
|
|
824
|
+
),
|
|
825
|
+
/* @__PURE__ */ jsx(
|
|
826
|
+
Slider,
|
|
827
|
+
{
|
|
828
|
+
"aria-label": "Crop size",
|
|
829
|
+
style: { flex: 1, minWidth: 140 },
|
|
830
|
+
disabled: disabled || imageUrl === null,
|
|
831
|
+
min: MIN_CROP_SCALE,
|
|
832
|
+
max: 1,
|
|
833
|
+
step: 0.01,
|
|
834
|
+
value: scale,
|
|
835
|
+
label: (current) => `${Math.round(current * 100)}%`,
|
|
836
|
+
onChange: refit
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
] })
|
|
840
|
+
] });
|
|
841
|
+
}
|
|
842
|
+
var ASSET_PICKER_FETCH_LIMIT = 100;
|
|
843
|
+
function describeAsset(membership, asset) {
|
|
844
|
+
return asset?.title ?? membership.sourceAssetId;
|
|
845
|
+
}
|
|
846
|
+
function readStorageReference(asset) {
|
|
847
|
+
const metadata = asset?.metadata;
|
|
848
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
849
|
+
const { credentialName, cloudStorageId } = metadata;
|
|
850
|
+
if (typeof credentialName !== "string" || typeof cloudStorageId !== "string") return null;
|
|
851
|
+
return { credentialName, cloudStorageId };
|
|
852
|
+
}
|
|
853
|
+
function SourceAssetsPanel({ itemId }) {
|
|
854
|
+
const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
|
|
855
|
+
const assetsQuery = useContentSourceAssets({ limit: ASSET_PICKER_FETCH_LIMIT });
|
|
856
|
+
const addMembership = useAddContentItemSourceAsset();
|
|
857
|
+
const removeMembership = useRemoveContentItemSourceAsset();
|
|
858
|
+
const reorderMemberships = useReorderContentItemSourceAssets();
|
|
859
|
+
const updateMembership = useUpdateContentItemSourceAsset();
|
|
860
|
+
const [assetToLink, setAssetToLink] = useState(null);
|
|
861
|
+
const [altDrafts, setAltDrafts] = useState({});
|
|
862
|
+
const [croppingAssetId, setCroppingAssetId] = useState(null);
|
|
863
|
+
const [cropDraft, setCropDraft] = useState(null);
|
|
864
|
+
const croppingDetail = memberships.find((detail) => detail.membership.sourceAssetId === croppingAssetId);
|
|
865
|
+
const croppingReference = readStorageReference(croppingDetail?.asset);
|
|
866
|
+
const { links } = useCloudStorageTemporaryLinks(
|
|
867
|
+
croppingReference?.credentialName ?? "",
|
|
868
|
+
croppingReference === null ? [] : [croppingReference.cloudStorageId]
|
|
869
|
+
);
|
|
870
|
+
const linkedIds = useMemo(() => new Set(memberships.map((detail) => detail.membership.sourceAssetId)), [memberships]);
|
|
871
|
+
const linkableOptions = useMemo(
|
|
872
|
+
() => (assetsQuery.data?.data ?? []).filter((asset) => !linkedIds.has(asset.id)).map((asset) => ({ value: asset.id, label: `${asset.title} (${asset.kind})` })),
|
|
873
|
+
[assetsQuery.data, linkedIds]
|
|
874
|
+
);
|
|
875
|
+
const orderedIds = memberships.map((detail) => detail.membership.sourceAssetId);
|
|
876
|
+
const handleLink = () => {
|
|
877
|
+
if (!assetToLink) return;
|
|
878
|
+
const highestPosition = memberships.reduce((highest, detail) => Math.max(highest, detail.membership.position), -1);
|
|
879
|
+
addMembership.mutate(
|
|
880
|
+
{ itemId, membership: { sourceAssetId: assetToLink, position: highestPosition + 1 } },
|
|
881
|
+
{ onSuccess: () => setAssetToLink(null) }
|
|
882
|
+
);
|
|
883
|
+
};
|
|
884
|
+
const stripAssets = useMemo(
|
|
885
|
+
() => memberships.map((detail) => ({
|
|
886
|
+
id: detail.membership.sourceAssetId,
|
|
887
|
+
name: describeAsset(detail.membership, detail.asset),
|
|
888
|
+
kind: "file"
|
|
889
|
+
})),
|
|
890
|
+
[memberships]
|
|
891
|
+
);
|
|
892
|
+
const handleMove = (fromIndex, toIndex) => {
|
|
893
|
+
if (toIndex < 0 || toIndex >= orderedIds.length) return;
|
|
894
|
+
const next = orderedIds.slice();
|
|
895
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
896
|
+
next.splice(toIndex, 0, moved);
|
|
897
|
+
reorderMemberships.mutate({ itemId, orderedAssetIds: next });
|
|
898
|
+
};
|
|
899
|
+
const handleSaveAltText = (sourceAssetId) => {
|
|
900
|
+
const draft = altDrafts[sourceAssetId];
|
|
901
|
+
if (draft === void 0) return;
|
|
902
|
+
const trimmed = draft.trim();
|
|
903
|
+
updateMembership.mutate(
|
|
904
|
+
// `altText` is `.min(1)` when present, so clearing the field is `null` rather
|
|
905
|
+
// than an empty string -- an empty string is a validation error, not a clear.
|
|
906
|
+
{ itemId, sourceAssetId, updates: { altText: trimmed === "" ? null : trimmed } },
|
|
907
|
+
{
|
|
908
|
+
onSuccess: () => setAltDrafts((drafts) => {
|
|
909
|
+
const { [sourceAssetId]: _cleared, ...rest } = drafts;
|
|
910
|
+
return rest;
|
|
911
|
+
})
|
|
912
|
+
}
|
|
913
|
+
);
|
|
914
|
+
};
|
|
915
|
+
const isMutating = addMembership.isPending || removeMembership.isPending || reorderMemberships.isPending || updateMembership.isPending;
|
|
916
|
+
return /* @__PURE__ */ jsxs(Paper, { withBorder: true, p: "md", "data-testid": "source-assets-panel", children: [
|
|
917
|
+
/* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
918
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
919
|
+
/* @__PURE__ */ jsx(Title, { order: 4, children: "Source Assets" }),
|
|
920
|
+
isMembershipLoaded && /* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
921
|
+
memberships.length,
|
|
922
|
+
" linked"
|
|
923
|
+
] })
|
|
924
|
+
] }),
|
|
925
|
+
!isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", py: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No source assets linked to this item." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
926
|
+
/* @__PURE__ */ jsx(
|
|
927
|
+
SelectedAssetStrip,
|
|
928
|
+
{
|
|
929
|
+
assets: stripAssets,
|
|
930
|
+
onReorder: (reordered) => reorderMemberships.mutate({ itemId, orderedAssetIds: reordered.map((asset) => asset.id) })
|
|
931
|
+
}
|
|
932
|
+
),
|
|
933
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
934
|
+
/* @__PURE__ */ jsx(Table.Thead, { children: /* @__PURE__ */ jsxs(Table.Tr, { children: [
|
|
935
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Position" }),
|
|
936
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Asset" }),
|
|
937
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Kind" }),
|
|
938
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Alt text" }),
|
|
939
|
+
/* @__PURE__ */ jsx(Table.Th, { children: "Crop" }),
|
|
940
|
+
/* @__PURE__ */ jsx(Table.Th, {})
|
|
941
|
+
] }) }),
|
|
942
|
+
/* @__PURE__ */ jsx(Table.Tbody, { children: memberships.map((detail, index) => {
|
|
943
|
+
const { membership, asset } = detail;
|
|
944
|
+
const draft = altDrafts[membership.sourceAssetId];
|
|
945
|
+
const storedAltText = membership.altText ?? "";
|
|
946
|
+
return /* @__PURE__ */ jsxs(
|
|
947
|
+
Table.Tr,
|
|
948
|
+
{
|
|
949
|
+
"data-testid": `source-asset-row-${membership.sourceAssetId}`,
|
|
950
|
+
children: [
|
|
951
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
952
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: membership.position }),
|
|
953
|
+
index === 0 && /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "filled", children: "Cover" })
|
|
954
|
+
] }) }),
|
|
955
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", children: describeAsset(membership, asset) }) }),
|
|
956
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: asset?.kind ?? "\u2014" }) }),
|
|
957
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
958
|
+
/* @__PURE__ */ jsx(
|
|
959
|
+
TextInput,
|
|
960
|
+
{
|
|
961
|
+
size: "xs",
|
|
962
|
+
placeholder: "Describe this slide",
|
|
963
|
+
value: draft ?? storedAltText,
|
|
964
|
+
onChange: (event) => setAltDrafts((drafts) => ({
|
|
965
|
+
...drafts,
|
|
966
|
+
[membership.sourceAssetId]: event.currentTarget.value
|
|
967
|
+
}))
|
|
968
|
+
}
|
|
969
|
+
),
|
|
970
|
+
draft !== void 0 && draft !== storedAltText && /* @__PURE__ */ jsx(
|
|
971
|
+
Button,
|
|
972
|
+
{
|
|
973
|
+
size: "compact-xs",
|
|
974
|
+
variant: "light",
|
|
975
|
+
disabled: isMutating,
|
|
976
|
+
onClick: () => handleSaveAltText(membership.sourceAssetId),
|
|
977
|
+
children: "Save"
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
] }) }),
|
|
981
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
982
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: membership.crop ? "set" : "\u2014" }),
|
|
983
|
+
/* @__PURE__ */ jsx(
|
|
984
|
+
Button,
|
|
985
|
+
{
|
|
986
|
+
size: "compact-xs",
|
|
987
|
+
variant: "subtle",
|
|
988
|
+
leftSection: /* @__PURE__ */ jsx(IconCrop, { size: 14 }),
|
|
989
|
+
disabled: isMutating || readStorageReference(asset) === null,
|
|
990
|
+
onClick: () => {
|
|
991
|
+
setCropDraft(membership.crop);
|
|
992
|
+
setCroppingAssetId(membership.sourceAssetId);
|
|
993
|
+
},
|
|
994
|
+
children: "Crop"
|
|
995
|
+
}
|
|
996
|
+
)
|
|
997
|
+
] }) }),
|
|
998
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsxs(Group, { gap: 4, justify: "flex-end", wrap: "nowrap", children: [
|
|
999
|
+
/* @__PURE__ */ jsx(
|
|
1000
|
+
Button,
|
|
1001
|
+
{
|
|
1002
|
+
size: "compact-xs",
|
|
1003
|
+
variant: "subtle",
|
|
1004
|
+
"aria-label": "Move up",
|
|
1005
|
+
disabled: index === 0 || isMutating,
|
|
1006
|
+
onClick: () => handleMove(index, index - 1),
|
|
1007
|
+
children: /* @__PURE__ */ jsx(IconArrowUp, { size: 14 })
|
|
1008
|
+
}
|
|
1009
|
+
),
|
|
1010
|
+
/* @__PURE__ */ jsx(
|
|
1011
|
+
Button,
|
|
1012
|
+
{
|
|
1013
|
+
size: "compact-xs",
|
|
1014
|
+
variant: "subtle",
|
|
1015
|
+
"aria-label": "Move down",
|
|
1016
|
+
disabled: index === memberships.length - 1 || isMutating,
|
|
1017
|
+
onClick: () => handleMove(index, index + 1),
|
|
1018
|
+
children: /* @__PURE__ */ jsx(IconArrowDown, { size: 14 })
|
|
1019
|
+
}
|
|
1020
|
+
),
|
|
1021
|
+
/* @__PURE__ */ jsx(
|
|
1022
|
+
Button,
|
|
1023
|
+
{
|
|
1024
|
+
size: "compact-xs",
|
|
1025
|
+
variant: "subtle",
|
|
1026
|
+
color: "red",
|
|
1027
|
+
leftSection: /* @__PURE__ */ jsx(IconUnlink, { size: 14 }),
|
|
1028
|
+
disabled: isMutating,
|
|
1029
|
+
onClick: () => removeMembership.mutate({ itemId, sourceAssetId: membership.sourceAssetId }),
|
|
1030
|
+
children: "Unlink"
|
|
1031
|
+
}
|
|
1032
|
+
)
|
|
1033
|
+
] }) })
|
|
1034
|
+
]
|
|
1035
|
+
},
|
|
1036
|
+
membership.sourceAssetId
|
|
1037
|
+
);
|
|
1038
|
+
}) })
|
|
1039
|
+
] })
|
|
1040
|
+
] }),
|
|
1041
|
+
/* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-end", children: [
|
|
1042
|
+
/* @__PURE__ */ jsx(
|
|
1043
|
+
Select,
|
|
1044
|
+
{
|
|
1045
|
+
size: "xs",
|
|
1046
|
+
searchable: true,
|
|
1047
|
+
clearable: true,
|
|
1048
|
+
label: "Link an existing source asset",
|
|
1049
|
+
placeholder: linkableOptions.length === 0 ? "Nothing left to link" : "Choose an asset",
|
|
1050
|
+
data: linkableOptions,
|
|
1051
|
+
value: assetToLink,
|
|
1052
|
+
onChange: setAssetToLink,
|
|
1053
|
+
disabled: linkableOptions.length === 0 || isMutating,
|
|
1054
|
+
style: { minWidth: 320 }
|
|
1055
|
+
}
|
|
1056
|
+
),
|
|
1057
|
+
/* @__PURE__ */ jsx(
|
|
1058
|
+
Button,
|
|
1059
|
+
{
|
|
1060
|
+
size: "compact-sm",
|
|
1061
|
+
variant: "light",
|
|
1062
|
+
leftSection: /* @__PURE__ */ jsx(IconLink, { size: 14 }),
|
|
1063
|
+
disabled: !assetToLink || isMutating,
|
|
1064
|
+
onClick: handleLink,
|
|
1065
|
+
children: "Link"
|
|
1066
|
+
}
|
|
1067
|
+
)
|
|
1068
|
+
] }),
|
|
1069
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Position 0 is the cover. Unlinking leaves the remaining positions untouched, so a gap is normal \u2014 use move up / move down to renumber the whole set. Unlinking never deletes the source asset itself." })
|
|
1070
|
+
] }),
|
|
1071
|
+
/* @__PURE__ */ jsx(
|
|
1072
|
+
Modal,
|
|
1073
|
+
{
|
|
1074
|
+
opened: croppingAssetId !== null,
|
|
1075
|
+
onClose: () => setCroppingAssetId(null),
|
|
1076
|
+
title: croppingDetail === void 0 ? "Crop" : `Crop ${describeAsset(croppingDetail.membership, croppingDetail.asset)}`,
|
|
1077
|
+
size: "lg",
|
|
1078
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1079
|
+
/* @__PURE__ */ jsx(
|
|
1080
|
+
AssetCropEditor,
|
|
1081
|
+
{
|
|
1082
|
+
imageUrl: croppingReference === null ? null : links[croppingReference.cloudStorageId] ?? null,
|
|
1083
|
+
value: cropDraft,
|
|
1084
|
+
onChange: setCropDraft,
|
|
1085
|
+
disabled: isMutating
|
|
1086
|
+
}
|
|
1087
|
+
),
|
|
1088
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1089
|
+
/* @__PURE__ */ jsx(
|
|
1090
|
+
Button,
|
|
1091
|
+
{
|
|
1092
|
+
variant: "subtle",
|
|
1093
|
+
color: "red",
|
|
1094
|
+
disabled: isMutating || cropDraft === null,
|
|
1095
|
+
onClick: () => setCropDraft(null),
|
|
1096
|
+
children: "Clear crop"
|
|
1097
|
+
}
|
|
1098
|
+
),
|
|
1099
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1100
|
+
/* @__PURE__ */ jsx(Button, { variant: "default", onClick: () => setCroppingAssetId(null), children: "Cancel" }),
|
|
1101
|
+
/* @__PURE__ */ jsx(
|
|
1102
|
+
Button,
|
|
1103
|
+
{
|
|
1104
|
+
disabled: isMutating || croppingAssetId === null,
|
|
1105
|
+
onClick: () => {
|
|
1106
|
+
if (croppingAssetId === null) return;
|
|
1107
|
+
updateMembership.mutate(
|
|
1108
|
+
{ itemId, sourceAssetId: croppingAssetId, updates: { crop: cropDraft } },
|
|
1109
|
+
{ onSuccess: () => setCroppingAssetId(null) }
|
|
1110
|
+
);
|
|
1111
|
+
},
|
|
1112
|
+
children: "Save crop"
|
|
1113
|
+
}
|
|
1114
|
+
)
|
|
1115
|
+
] })
|
|
1116
|
+
] })
|
|
1117
|
+
] })
|
|
1118
|
+
}
|
|
1119
|
+
)
|
|
1120
|
+
] });
|
|
1121
|
+
}
|
|
1122
|
+
var PREVIEW_MAX_WIDTH = 380;
|
|
1123
|
+
function PostPreview({ slides, caption, hashtags = [], aspectRatio = 4 / 5, title }) {
|
|
1124
|
+
const [index, setIndex] = useState(0);
|
|
1125
|
+
useEffect(() => {
|
|
1126
|
+
if (index > slides.length - 1) setIndex(Math.max(0, slides.length - 1));
|
|
1127
|
+
}, [slides.length, index]);
|
|
1128
|
+
const slide = slides[index];
|
|
1129
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: "xs", style: { maxWidth: PREVIEW_MAX_WIDTH }, "data-testid": "post-preview", children: [
|
|
1130
|
+
title !== void 0 && /* @__PURE__ */ jsx(Text, { fw: 500, lineClamp: 1, children: title }),
|
|
1131
|
+
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: 0, style: { overflow: "hidden" }, children: /* @__PURE__ */ jsxs(
|
|
1132
|
+
Box,
|
|
1133
|
+
{
|
|
1134
|
+
style: {
|
|
1135
|
+
position: "relative",
|
|
1136
|
+
width: "100%",
|
|
1137
|
+
aspectRatio: `${aspectRatio}`,
|
|
1138
|
+
overflow: "hidden",
|
|
1139
|
+
background: "var(--mantine-color-gray-1)"
|
|
1140
|
+
},
|
|
1141
|
+
"data-testid": "post-preview-frame",
|
|
1142
|
+
children: [
|
|
1143
|
+
slide === void 0 || slide.imageUrl === null ? /* @__PURE__ */ jsx(Group, { justify: "center", align: "center", h: "100%", children: /* @__PURE__ */ jsx(IconPhotoOff, { size: 20, color: "var(--mantine-color-dimmed)" }) }) : /* @__PURE__ */ jsx(
|
|
1144
|
+
"img",
|
|
1145
|
+
{
|
|
1146
|
+
src: slide.imageUrl,
|
|
1147
|
+
alt: slide.altText ?? "",
|
|
1148
|
+
"data-testid": "post-preview-image",
|
|
1149
|
+
style: cropStyle(slide.crop ?? null)
|
|
1150
|
+
}
|
|
1151
|
+
),
|
|
1152
|
+
index > 0 && /* @__PURE__ */ jsx(
|
|
1153
|
+
ActionIcon,
|
|
1154
|
+
{
|
|
1155
|
+
variant: "default",
|
|
1156
|
+
radius: "xl",
|
|
1157
|
+
size: "lg",
|
|
1158
|
+
"aria-label": "Previous slide",
|
|
1159
|
+
onClick: () => setIndex((current) => Math.max(0, current - 1)),
|
|
1160
|
+
style: { position: "absolute", left: 8, top: "50%", transform: "translateY(-50%)" },
|
|
1161
|
+
children: /* @__PURE__ */ jsx(IconChevronLeft, { size: 18 })
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
index < slides.length - 1 && /* @__PURE__ */ jsx(
|
|
1165
|
+
ActionIcon,
|
|
1166
|
+
{
|
|
1167
|
+
variant: "default",
|
|
1168
|
+
radius: "xl",
|
|
1169
|
+
size: "lg",
|
|
1170
|
+
"aria-label": "Next slide",
|
|
1171
|
+
onClick: () => setIndex((current) => Math.min(slides.length - 1, current + 1)),
|
|
1172
|
+
style: { position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)" },
|
|
1173
|
+
children: /* @__PURE__ */ jsx(IconChevronRight, { size: 18 })
|
|
1174
|
+
}
|
|
1175
|
+
)
|
|
1176
|
+
]
|
|
1177
|
+
}
|
|
1178
|
+
) }),
|
|
1179
|
+
slides.length > 1 && /* @__PURE__ */ jsx(Group, { justify: "center", gap: 6, "data-testid": "post-preview-dots", children: slides.map((each, dotIndex) => /* @__PURE__ */ jsx(
|
|
1180
|
+
Box,
|
|
1181
|
+
{
|
|
1182
|
+
style: {
|
|
1183
|
+
width: 6,
|
|
1184
|
+
height: 6,
|
|
1185
|
+
borderRadius: "50%",
|
|
1186
|
+
background: dotIndex === index ? "var(--mantine-color-blue-5)" : "var(--mantine-color-gray-4)"
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
each.id
|
|
1190
|
+
)) }),
|
|
1191
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, "data-testid": "post-preview-caption", children: caption === null || caption === void 0 || caption.length === 0 ? /* @__PURE__ */ jsx(Text, { component: "span", size: "sm", c: "dimmed", children: "No caption yet" }) : caption }),
|
|
1192
|
+
hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", "data-testid": "post-preview-hashtags", children: hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") })
|
|
1193
|
+
] });
|
|
1194
|
+
}
|
|
1195
|
+
function cropStyle(crop) {
|
|
1196
|
+
if (crop === null) {
|
|
1197
|
+
return { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover" };
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
position: "absolute",
|
|
1201
|
+
left: `${-crop.x / crop.width * 100}%`,
|
|
1202
|
+
top: `${-crop.y / crop.height * 100}%`,
|
|
1203
|
+
width: `${1 / crop.width * 100}%`,
|
|
1204
|
+
height: `${1 / crop.height * 100}%`,
|
|
1205
|
+
objectFit: "cover"
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// src/features/content/renderCrop.ts
|
|
1210
|
+
var CONTENT_DERIVATIVES_BUCKET = "content-derivatives";
|
|
1211
|
+
var DERIVATIVE_MAX_WIDTH = 1080;
|
|
1212
|
+
var DERIVATIVE_JPEG_QUALITY = 0.9;
|
|
1213
|
+
function derivativeObjectKey(contentItemId, sourceAssetId) {
|
|
1214
|
+
return `${contentItemId}/${sourceAssetId}.jpg`;
|
|
1215
|
+
}
|
|
1216
|
+
function isDerivativeStale(crop, derivativeCrop, derivativePath) {
|
|
1217
|
+
if (crop === null || derivativePath === null) return false;
|
|
1218
|
+
if (derivativeCrop === null) return true;
|
|
1219
|
+
return crop.x !== derivativeCrop.x || crop.y !== derivativeCrop.y || crop.width !== derivativeCrop.width || crop.height !== derivativeCrop.height;
|
|
1220
|
+
}
|
|
1221
|
+
function loadImage(url) {
|
|
1222
|
+
return new Promise((resolve, reject) => {
|
|
1223
|
+
const image = new Image();
|
|
1224
|
+
image.crossOrigin = "anonymous";
|
|
1225
|
+
image.onload = () => resolve(image);
|
|
1226
|
+
image.onerror = () => reject(new Error(`Could not load the image to crop: ${url}`));
|
|
1227
|
+
image.src = url;
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
function canvasToJpegBlob(canvas, quality) {
|
|
1231
|
+
return new Promise((resolve, reject) => {
|
|
1232
|
+
canvas.toBlob(
|
|
1233
|
+
(blob) => {
|
|
1234
|
+
if (blob === null) {
|
|
1235
|
+
reject(new Error("The browser produced no image data for the crop"));
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
resolve(blob);
|
|
1239
|
+
},
|
|
1240
|
+
"image/jpeg",
|
|
1241
|
+
quality
|
|
1242
|
+
);
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
async function renderCropToJpeg(imageUrl, crop, options = {}) {
|
|
1246
|
+
const maxWidth = options.maxWidth ?? DERIVATIVE_MAX_WIDTH;
|
|
1247
|
+
const quality = options.quality ?? DERIVATIVE_JPEG_QUALITY;
|
|
1248
|
+
const image = await loadImage(imageUrl);
|
|
1249
|
+
const box = crop ?? { x: 0, y: 0, width: 1, height: 1 };
|
|
1250
|
+
const sourceX = box.x * image.naturalWidth;
|
|
1251
|
+
const sourceY = box.y * image.naturalHeight;
|
|
1252
|
+
const sourceWidth = box.width * image.naturalWidth;
|
|
1253
|
+
const sourceHeight = box.height * image.naturalHeight;
|
|
1254
|
+
if (sourceWidth < 1 || sourceHeight < 1) {
|
|
1255
|
+
throw new Error("The crop resolves to less than one pixel of the source");
|
|
1256
|
+
}
|
|
1257
|
+
const width = Math.max(1, Math.round(Math.min(maxWidth, sourceWidth)));
|
|
1258
|
+
const height = Math.max(1, Math.round(width * sourceHeight / sourceWidth));
|
|
1259
|
+
const canvas = document.createElement("canvas");
|
|
1260
|
+
canvas.width = width;
|
|
1261
|
+
canvas.height = height;
|
|
1262
|
+
const context = canvas.getContext("2d");
|
|
1263
|
+
if (context === null) throw new Error("The browser would not provide a 2D canvas context");
|
|
1264
|
+
context.fillStyle = "#ffffff";
|
|
1265
|
+
context.fillRect(0, 0, width, height);
|
|
1266
|
+
context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, width, height);
|
|
1267
|
+
return canvasToJpegBlob(canvas, quality);
|
|
1268
|
+
}
|
|
1269
|
+
var CAPTION_STEP_KEY = "caption-generation";
|
|
1270
|
+
var CAPTION_WORKFLOW_ID = "cnt-caption-generation-workflow";
|
|
1271
|
+
var AUTOSAVE_DELAY_MS = 800;
|
|
1272
|
+
function readCaptionAttempt(attempt) {
|
|
1273
|
+
if (attempt.stepKey !== CAPTION_STEP_KEY) return null;
|
|
1274
|
+
const payload = attempt.payload;
|
|
1275
|
+
const caption = payload?.caption;
|
|
1276
|
+
if (typeof caption !== "string" || caption.length === 0) return null;
|
|
1277
|
+
return {
|
|
1278
|
+
attempt,
|
|
1279
|
+
caption,
|
|
1280
|
+
hashtags: Array.isArray(payload?.hashtags) ? payload.hashtags.filter((tag) => typeof tag === "string") : [],
|
|
1281
|
+
instructions: typeof payload?.instructions === "string" ? payload.instructions : ""
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function CaptionPanel({ itemId, body, onDraftChange }) {
|
|
1285
|
+
const queryClient = useQueryClient();
|
|
1286
|
+
const savedBody = body ?? "";
|
|
1287
|
+
const [instructions, setInstructions] = useState("");
|
|
1288
|
+
const [executionId, setExecutionId] = useState(null);
|
|
1289
|
+
const [failure, setFailure] = useState(null);
|
|
1290
|
+
const [draft, setDraft] = useState(savedBody);
|
|
1291
|
+
const attemptsQuery = useContentItemAttempts(itemId);
|
|
1292
|
+
const executeAsync = useExecuteAsync();
|
|
1293
|
+
const updateItem = useUpdateContentItem();
|
|
1294
|
+
const execution = useExecution(CAPTION_WORKFLOW_ID, executionId ?? "");
|
|
1295
|
+
const captions = useMemo(() => {
|
|
1296
|
+
const rows = (attemptsQuery.data?.data ?? []).map(readCaptionAttempt).filter((row) => row !== null);
|
|
1297
|
+
rows.sort((left, right) => right.attempt.attemptNumber - left.attempt.attemptNumber);
|
|
1298
|
+
return rows.map((row, index) => ({ ...row, displayNumber: rows.length - index }));
|
|
1299
|
+
}, [attemptsQuery.data]);
|
|
1300
|
+
const status = execution.data?.status;
|
|
1301
|
+
const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
|
|
1302
|
+
const lastSyncedRef = useRef(savedBody);
|
|
1303
|
+
const [debouncedDraft] = useDebouncedValue(draft, AUTOSAVE_DELAY_MS);
|
|
1304
|
+
useEffect(() => {
|
|
1305
|
+
if (savedBody === lastSyncedRef.current) return;
|
|
1306
|
+
lastSyncedRef.current = savedBody;
|
|
1307
|
+
setDraft(savedBody);
|
|
1308
|
+
}, [savedBody]);
|
|
1309
|
+
useEffect(() => {
|
|
1310
|
+
if (debouncedDraft === lastSyncedRef.current) return;
|
|
1311
|
+
lastSyncedRef.current = debouncedDraft;
|
|
1312
|
+
updateItem.mutate({ itemId, updates: { body: debouncedDraft } });
|
|
1313
|
+
}, [debouncedDraft, itemId, updateItem]);
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
onDraftChange?.(draft);
|
|
1316
|
+
}, [draft, onDraftChange]);
|
|
1317
|
+
const isDirty = draft !== lastSyncedRef.current;
|
|
1318
|
+
useEffect(() => {
|
|
1319
|
+
if (executionId === null || status === void 0 || status === "running") return;
|
|
1320
|
+
if (status === "failed") {
|
|
1321
|
+
setFailure(execution.data?.error ?? "The caption workflow failed. Check the execution log for details.");
|
|
1322
|
+
}
|
|
1323
|
+
void queryClient.invalidateQueries({ queryKey: contentKeys.items() });
|
|
1324
|
+
setExecutionId(null);
|
|
1325
|
+
}, [status, executionId, execution.data?.error, queryClient]);
|
|
1326
|
+
const handleGenerate = async () => {
|
|
1327
|
+
setFailure(null);
|
|
1328
|
+
try {
|
|
1329
|
+
const result = await executeAsync.mutateAsync({
|
|
1330
|
+
resourceId: CAPTION_WORKFLOW_ID,
|
|
1331
|
+
resourceType: "workflow",
|
|
1332
|
+
input: {
|
|
1333
|
+
contentItemId: itemId,
|
|
1334
|
+
...instructions.trim().length > 0 ? { instructions: instructions.trim() } : {}
|
|
1335
|
+
}
|
|
1336
|
+
});
|
|
1337
|
+
setExecutionId(result.executionId);
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
setFailure(error instanceof Error ? error.message : "Could not start the caption workflow.");
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
const handleUse = (candidate) => {
|
|
1343
|
+
setDraft(
|
|
1344
|
+
candidate.hashtags.length === 0 ? candidate.caption : `${candidate.caption}
|
|
1345
|
+
|
|
1346
|
+
${candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ")}`
|
|
1347
|
+
);
|
|
1348
|
+
};
|
|
1349
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "caption-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1350
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1351
|
+
/* @__PURE__ */ jsx(Title, { order: 5, children: "Caption" }),
|
|
1352
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
1353
|
+
captions.length,
|
|
1354
|
+
" generated"
|
|
1355
|
+
] })
|
|
1356
|
+
] }),
|
|
1357
|
+
failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "caption-panel-error", children: failure }),
|
|
1358
|
+
/* @__PURE__ */ jsx(
|
|
1359
|
+
Textarea,
|
|
1360
|
+
{
|
|
1361
|
+
label: "The caption",
|
|
1362
|
+
description: "Edit it freely. Changes save on their own.",
|
|
1363
|
+
placeholder: "Write it yourself, or generate one below and edit from there.",
|
|
1364
|
+
autosize: true,
|
|
1365
|
+
minRows: 4,
|
|
1366
|
+
maxRows: 16,
|
|
1367
|
+
value: draft,
|
|
1368
|
+
onChange: (event) => setDraft(event.currentTarget.value),
|
|
1369
|
+
"data-testid": "caption-panel-body"
|
|
1370
|
+
}
|
|
1371
|
+
),
|
|
1372
|
+
/* @__PURE__ */ jsx(Group, { justify: "flex-end", gap: "xs", mt: -6, children: updateItem.isPending ? /* @__PURE__ */ jsxs(Group, { gap: 6, children: [
|
|
1373
|
+
/* @__PURE__ */ jsx(Loader, { size: 12 }),
|
|
1374
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Saving" })
|
|
1375
|
+
] }) : isDirty ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Unsaved" }) : /* @__PURE__ */ jsxs(Group, { gap: 4, children: [
|
|
1376
|
+
/* @__PURE__ */ jsx(IconCheck, { size: 12, color: "var(--mantine-color-dimmed)" }),
|
|
1377
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", "data-testid": "caption-panel-save-state", children: "Saved" })
|
|
1378
|
+
] }) }),
|
|
1379
|
+
/* @__PURE__ */ jsx(
|
|
1380
|
+
Textarea,
|
|
1381
|
+
{
|
|
1382
|
+
label: "Direction (optional)",
|
|
1383
|
+
description: "The angle, the occasion, who is in frame -- anything the photos cannot show on their own.",
|
|
1384
|
+
placeholder: "e.g. this is the second shop opening, keep it short and warm",
|
|
1385
|
+
autosize: true,
|
|
1386
|
+
minRows: 2,
|
|
1387
|
+
maxRows: 5,
|
|
1388
|
+
value: instructions,
|
|
1389
|
+
disabled: isRunning,
|
|
1390
|
+
onChange: (event) => setInstructions(event.currentTarget.value)
|
|
1391
|
+
}
|
|
1392
|
+
),
|
|
1393
|
+
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1394
|
+
/* @__PURE__ */ jsx(
|
|
1395
|
+
Button,
|
|
1396
|
+
{
|
|
1397
|
+
leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconSparkles, { size: 16 }),
|
|
1398
|
+
disabled: isRunning,
|
|
1399
|
+
onClick: () => void handleGenerate(),
|
|
1400
|
+
"data-testid": "caption-panel-generate",
|
|
1401
|
+
children: captions.length === 0 ? "Generate caption" : "Regenerate"
|
|
1402
|
+
}
|
|
1403
|
+
),
|
|
1404
|
+
isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading the slides and writing a caption. This does not overwrite the previous one." })
|
|
1405
|
+
] }),
|
|
1406
|
+
attemptsQuery.isLoading ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : captions.map((candidate) => {
|
|
1407
|
+
const isLive = draft.startsWith(candidate.caption);
|
|
1408
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "caption-panel-attempt", children: /* @__PURE__ */ jsxs(Stack, { gap: 6, children: [
|
|
1409
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", gap: "xs", children: [
|
|
1410
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1411
|
+
/* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1412
|
+
"Attempt ",
|
|
1413
|
+
candidate.displayNumber
|
|
1414
|
+
] }),
|
|
1415
|
+
isLive && /* @__PURE__ */ jsx(Badge, { size: "xs", color: "green", variant: "light", leftSection: /* @__PURE__ */ jsx(IconCheck, { size: 10 }), children: "In use" })
|
|
1416
|
+
] }),
|
|
1417
|
+
/* @__PURE__ */ jsx(
|
|
1418
|
+
Button,
|
|
1419
|
+
{
|
|
1420
|
+
size: "compact-xs",
|
|
1421
|
+
variant: "light",
|
|
1422
|
+
disabled: isLive || updateItem.isPending,
|
|
1423
|
+
onClick: () => handleUse(candidate),
|
|
1424
|
+
children: "Use this caption"
|
|
1425
|
+
}
|
|
1426
|
+
)
|
|
1427
|
+
] }),
|
|
1428
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, children: candidate.caption }),
|
|
1429
|
+
candidate.hashtags.length > 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "blue", children: candidate.hashtags.map((tag) => `#${tag.replace(/^#/, "")}`).join(" ") }),
|
|
1430
|
+
candidate.instructions.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1431
|
+
"Asked for: ",
|
|
1432
|
+
candidate.instructions
|
|
1433
|
+
] })
|
|
1434
|
+
] }) }, candidate.attempt.id);
|
|
1435
|
+
}),
|
|
1436
|
+
!attemptsQuery.isLoading && captions.length === 0 && /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "Nothing generated yet. Analysed slides make for a caption that describes what is actually in them -- or just write the caption above yourself." })
|
|
1437
|
+
] }) });
|
|
1438
|
+
}
|
|
1439
|
+
var IMAGE_ANALYSIS_WORKFLOW_ID = "cnt-image-analysis-workflow";
|
|
1440
|
+
function readStoredAnalysis(asset) {
|
|
1441
|
+
const metadata = asset?.metadata;
|
|
1442
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
1443
|
+
const analysis = metadata.analysis;
|
|
1444
|
+
if (typeof analysis !== "object" || analysis === null) return null;
|
|
1445
|
+
const raw = analysis;
|
|
1446
|
+
const asString = (key) => typeof raw[key] === "string" ? raw[key] : void 0;
|
|
1447
|
+
return {
|
|
1448
|
+
description: asString("description"),
|
|
1449
|
+
subjects: Array.isArray(raw.subjects) ? raw.subjects.filter((one) => typeof one === "string") : void 0,
|
|
1450
|
+
setting: asString("setting"),
|
|
1451
|
+
mood: asString("mood"),
|
|
1452
|
+
textInImage: asString("textInImage"),
|
|
1453
|
+
altText: asString("altText"),
|
|
1454
|
+
model: asString("model"),
|
|
1455
|
+
generatedAt: asString("generatedAt")
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function SlideAnalysisPanel({ itemId }) {
|
|
1459
|
+
const queryClient = useQueryClient();
|
|
1460
|
+
const [executionId, setExecutionId] = useState(null);
|
|
1461
|
+
const [failure, setFailure] = useState(null);
|
|
1462
|
+
const { memberships, isMembershipLoaded } = useContentItemSourceAssetDetails(itemId);
|
|
1463
|
+
const executeAsync = useExecuteAsync();
|
|
1464
|
+
const execution = useExecution(IMAGE_ANALYSIS_WORKFLOW_ID, executionId ?? "");
|
|
1465
|
+
const status = execution.data?.status;
|
|
1466
|
+
const isRunning = executeAsync.isPending || executionId !== null && (status === void 0 || status === "running");
|
|
1467
|
+
const analyzedCount = useMemo(
|
|
1468
|
+
() => memberships.filter((detail) => readStoredAnalysis(detail.asset) !== null).length,
|
|
1469
|
+
[memberships]
|
|
1470
|
+
);
|
|
1471
|
+
useEffect(() => {
|
|
1472
|
+
if (executionId === null || status === void 0 || status === "running") return;
|
|
1473
|
+
if (status === "failed") {
|
|
1474
|
+
setFailure(execution.data?.error ?? "The analysis workflow failed. Check the execution log for details.");
|
|
1475
|
+
}
|
|
1476
|
+
void queryClient.invalidateQueries({ queryKey: contentKeys.sourceAssets() });
|
|
1477
|
+
setExecutionId(null);
|
|
1478
|
+
}, [status, executionId, execution.data?.error, queryClient]);
|
|
1479
|
+
const handleAnalyze = async () => {
|
|
1480
|
+
setFailure(null);
|
|
1481
|
+
try {
|
|
1482
|
+
const result = await executeAsync.mutateAsync({
|
|
1483
|
+
resourceId: IMAGE_ANALYSIS_WORKFLOW_ID,
|
|
1484
|
+
resourceType: "workflow",
|
|
1485
|
+
input: { contentItemId: itemId }
|
|
1486
|
+
});
|
|
1487
|
+
setExecutionId(result.executionId);
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
setFailure(error instanceof Error ? error.message : "Could not start the analysis workflow.");
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", "data-testid": "slide-analysis-panel", children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
1493
|
+
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
1494
|
+
/* @__PURE__ */ jsx(Title, { order: 5, children: "Slide analysis" }),
|
|
1495
|
+
/* @__PURE__ */ jsxs(Badge, { variant: "light", size: "sm", children: [
|
|
1496
|
+
analyzedCount,
|
|
1497
|
+
" of ",
|
|
1498
|
+
memberships.length,
|
|
1499
|
+
" analysed"
|
|
1500
|
+
] })
|
|
1501
|
+
] }),
|
|
1502
|
+
failure !== null && /* @__PURE__ */ jsx(Alert, { color: "red", icon: /* @__PURE__ */ jsx(IconAlertTriangle, { size: 16 }), "data-testid": "slide-analysis-error", children: failure }),
|
|
1503
|
+
/* @__PURE__ */ jsxs(Group, { children: [
|
|
1504
|
+
/* @__PURE__ */ jsx(
|
|
1505
|
+
Button,
|
|
1506
|
+
{
|
|
1507
|
+
leftSection: isRunning ? /* @__PURE__ */ jsx(Loader, { size: 14 }) : /* @__PURE__ */ jsx(IconEye, { size: 16 }),
|
|
1508
|
+
disabled: isRunning || memberships.length === 0,
|
|
1509
|
+
onClick: () => void handleAnalyze(),
|
|
1510
|
+
"data-testid": "slide-analysis-run",
|
|
1511
|
+
children: analyzedCount === 0 ? "Analyse slides" : "Re-analyse slides"
|
|
1512
|
+
}
|
|
1513
|
+
),
|
|
1514
|
+
isRunning && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Reading each slide with a vision model." })
|
|
1515
|
+
] }),
|
|
1516
|
+
!isMembershipLoaded ? /* @__PURE__ */ jsx(Group, { justify: "center", p: "md", children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : memberships.map((detail) => {
|
|
1517
|
+
const analysis = readStoredAnalysis(detail.asset);
|
|
1518
|
+
const label = detail.asset?.title ?? detail.membership.sourceAssetId;
|
|
1519
|
+
return /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "sm", "data-testid": "slide-analysis-row", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
1520
|
+
/* @__PURE__ */ jsxs(Group, { gap: "xs", children: [
|
|
1521
|
+
/* @__PURE__ */ jsxs(Badge, { size: "xs", variant: "light", children: [
|
|
1522
|
+
"Slide ",
|
|
1523
|
+
detail.membership.position + 1
|
|
1524
|
+
] }),
|
|
1525
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 1, children: label })
|
|
1526
|
+
] }),
|
|
1527
|
+
analysis === null ? /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Not analysed yet." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1528
|
+
analysis.description !== void 0 && /* @__PURE__ */ jsx(Text, { size: "sm", children: analysis.description }),
|
|
1529
|
+
analysis.subjects !== void 0 && analysis.subjects.length > 0 && /* @__PURE__ */ jsx(Group, { gap: 4, children: analysis.subjects.map((subject) => /* @__PURE__ */ jsx(Badge, { size: "xs", variant: "outline", children: subject }, subject)) }),
|
|
1530
|
+
(analysis.setting !== void 0 || analysis.mood !== void 0) && /* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: [analysis.setting, analysis.mood].filter(Boolean).join(" - ") }),
|
|
1531
|
+
analysis.textInImage !== void 0 && analysis.textInImage.length > 0 && /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
1532
|
+
"Text in image: ",
|
|
1533
|
+
analysis.textInImage
|
|
1534
|
+
] })
|
|
1535
|
+
] })
|
|
1536
|
+
] }) }, detail.membership.id);
|
|
1537
|
+
}),
|
|
1538
|
+
/* @__PURE__ */ jsx(Text, { size: "xs", c: "dimmed", children: "Analysis is stored on the photo itself, so every post that reuses it reads the same description and a re-crop does not invalidate it." })
|
|
1539
|
+
] }) });
|
|
1540
|
+
}
|
|
538
1541
|
|
|
539
1542
|
// src/lib/content/stage-colors.ts
|
|
540
1543
|
var FALLBACK_CONTENT_STAGE_COLORS = {
|
|
@@ -554,6 +1557,10 @@ function getContentStageColor(entry) {
|
|
|
554
1557
|
}
|
|
555
1558
|
var OVERVIEW_ITEMS_LIMIT = 100;
|
|
556
1559
|
var RECENT_ITEMS_LIMIT = 8;
|
|
1560
|
+
function resolveItemCurrentStep(item, pipelines) {
|
|
1561
|
+
const pipeline = pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
1562
|
+
return resolveCurrentContentStep(item, pipeline?.steps ?? []);
|
|
1563
|
+
}
|
|
557
1564
|
function ContentOverviewPage({ headerActions, footer } = {}) {
|
|
558
1565
|
const navigate = useNavigate();
|
|
559
1566
|
const itemsQuery = useContentItems({ limit: OVERVIEW_ITEMS_LIMIT });
|
|
@@ -568,7 +1575,9 @@ function ContentOverviewPage({ headerActions, footer } = {}) {
|
|
|
568
1575
|
}
|
|
569
1576
|
return counts;
|
|
570
1577
|
}, [items]);
|
|
571
|
-
const pendingReviewCount = items.filter(
|
|
1578
|
+
const pendingReviewCount = items.filter(
|
|
1579
|
+
(item) => isAwaitingReview(item, resolveItemCurrentStep(item, contentConfig.pipelines))
|
|
1580
|
+
).length;
|
|
572
1581
|
const totalItems = itemsQuery.data?.total ?? items.length;
|
|
573
1582
|
const totalDistributions = distributionsQuery.data?.data.length ?? 0;
|
|
574
1583
|
const recentItems = useMemo(
|
|
@@ -708,6 +1717,15 @@ function ContentItemsPage({ headerActions } = {}) {
|
|
|
708
1717
|
const [statusFilter, setStatusFilter] = useState("");
|
|
709
1718
|
const [pillarFilter, setPillarFilter] = useState("");
|
|
710
1719
|
const pagination = usePaginationState(PAGE_SIZE, [searchQuery, statusFilter, pillarFilter, reviewFilterActive]);
|
|
1720
|
+
const openItem = (item) => {
|
|
1721
|
+
const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
1722
|
+
const target = resolveReviewTarget(item, pipeline, pipeline?.steps ?? []);
|
|
1723
|
+
if (target.kind === "workspace") {
|
|
1724
|
+
void navigate({ to: target.route });
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
void navigate({ to: "/content/items/$itemId", params: { itemId: item.id } });
|
|
1728
|
+
};
|
|
711
1729
|
const itemsQuery = useContentItems({
|
|
712
1730
|
search: searchQuery.trim() || void 0,
|
|
713
1731
|
status: statusFilter || void 0,
|
|
@@ -716,7 +1734,11 @@ function ContentItemsPage({ headerActions } = {}) {
|
|
|
716
1734
|
offset: pagination.offset
|
|
717
1735
|
});
|
|
718
1736
|
const fetchedItems = itemsQuery.data?.data ?? [];
|
|
719
|
-
const items = reviewFilterActive ? fetchedItems.filter((item) =>
|
|
1737
|
+
const items = reviewFilterActive ? fetchedItems.filter((item) => {
|
|
1738
|
+
const pipeline = contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId);
|
|
1739
|
+
const step = resolveCurrentContentStep(item, pipeline?.steps ?? []);
|
|
1740
|
+
return isAwaitingReview(item, step);
|
|
1741
|
+
}) : fetchedItems;
|
|
720
1742
|
const total = itemsQuery.data?.total ?? 0;
|
|
721
1743
|
const statusOptions = [
|
|
722
1744
|
{ value: "", label: "All statuses" },
|
|
@@ -789,31 +1811,23 @@ function ContentItemsPage({ headerActions } = {}) {
|
|
|
789
1811
|
const statusEntry = contentConfig.stageCatalog[item.status];
|
|
790
1812
|
const pillarEntry = item.pillar ? contentConfig.stageCatalog[item.pillar] : void 0;
|
|
791
1813
|
const statusColors = getContentStageColor(statusEntry);
|
|
792
|
-
return /* @__PURE__ */ jsxs(
|
|
793
|
-
Table.
|
|
794
|
-
{
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
) }),
|
|
810
|
-
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pillarEntry?.label ?? item.pillar ?? "\u2014" }) }),
|
|
811
|
-
/* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) }),
|
|
812
|
-
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatRelativeTime(item.updatedAt) }) })
|
|
813
|
-
]
|
|
814
|
-
},
|
|
815
|
-
item.id
|
|
816
|
-
);
|
|
1814
|
+
return /* @__PURE__ */ jsxs(Table.Tr, { style: { cursor: "pointer" }, onClick: () => openItem(item), children: [
|
|
1815
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, children: getContentItemIdentity(item) }) }),
|
|
1816
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(
|
|
1817
|
+
Badge,
|
|
1818
|
+
{
|
|
1819
|
+
size: "sm",
|
|
1820
|
+
variant: "light",
|
|
1821
|
+
styles: {
|
|
1822
|
+
root: { color: statusColors.accent, backgroundColor: statusColors.background }
|
|
1823
|
+
},
|
|
1824
|
+
children: statusEntry?.label ?? item.status
|
|
1825
|
+
}
|
|
1826
|
+
) }),
|
|
1827
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: pillarEntry?.label ?? item.pillar ?? "\u2014" }) }),
|
|
1828
|
+
/* @__PURE__ */ jsx(Table.Td, { children: item.reviewedAt ? /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-success)", children: "Reviewed" }) : /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", color: "var(--color-warning)", children: "Pending" }) }),
|
|
1829
|
+
/* @__PURE__ */ jsx(Table.Td, { children: /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: formatRelativeTime(item.updatedAt) }) })
|
|
1830
|
+
] }, item.id);
|
|
817
1831
|
}) })
|
|
818
1832
|
] }),
|
|
819
1833
|
total > PAGE_SIZE && /* @__PURE__ */ jsx(Group, { justify: "center", children: /* @__PURE__ */ jsx(
|
|
@@ -942,20 +1956,21 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
942
1956
|
const pipeline = item ? contentConfig.pipelines.find((candidate) => candidate.id === item.pipelineId) : void 0;
|
|
943
1957
|
const pipelineSteps = useMemo(() => pipeline?.steps ?? [], [pipeline]);
|
|
944
1958
|
const isTerminalPipeline = pipelineSteps.length <= 1;
|
|
945
|
-
const
|
|
946
|
-
() =>
|
|
947
|
-
[item
|
|
1959
|
+
const reviewTarget = useMemo(
|
|
1960
|
+
() => item ? resolveReviewTarget(item, pipeline, pipelineSteps) : { kind: "none" },
|
|
1961
|
+
[item, pipeline, pipelineSteps]
|
|
948
1962
|
);
|
|
949
|
-
const earliestOpenGate =
|
|
1963
|
+
const earliestOpenGate = reviewTarget.kind === "queued" ? reviewTarget.gate : void 0;
|
|
1964
|
+
const workspaceRoute = reviewTarget.kind === "workspace" ? reviewTarget.route : void 0;
|
|
950
1965
|
const reviewDisabledReason = useMemo(() => {
|
|
951
|
-
if (
|
|
1966
|
+
if (reviewTarget.kind !== "none") return void 0;
|
|
952
1967
|
if (!item?.pipelineId) return "This item has no pipeline, so there is no review gate to clear.";
|
|
953
1968
|
if (!pipeline) return `Unknown pipeline "${item.pipelineId}" -- there is no review gate to clear.`;
|
|
954
1969
|
return "No open review gate on this item. Every queued step has already been reviewed, or the current step is still running.";
|
|
955
|
-
}, [
|
|
1970
|
+
}, [reviewTarget, item?.pipelineId, pipeline]);
|
|
956
1971
|
const reviewActions = useMemo(
|
|
957
|
-
() => buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
|
|
958
|
-
[isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
|
|
1972
|
+
() => workspaceRoute ? [] : buildReviewActions(isTerminalPipeline, earliestOpenGate, reviewDisabledReason),
|
|
1973
|
+
[workspaceRoute, isTerminalPipeline, earliestOpenGate, reviewDisabledReason]
|
|
959
1974
|
);
|
|
960
1975
|
const pipelinePayloadFields = useMemo(() => getPipelinePayloadFields(pipelineSteps), [pipelineSteps]);
|
|
961
1976
|
const primaryTextField = useMemo(() => getPrimaryPayloadTextField(pipelinePayloadFields), [pipelinePayloadFields]);
|
|
@@ -979,8 +1994,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
979
1994
|
});
|
|
980
1995
|
const reviewQueue = useMemo(() => {
|
|
981
1996
|
if (!item?.pipelineId) return [];
|
|
982
|
-
return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => candidate
|
|
983
|
-
}, [pipelineItemsQuery.data, item?.pipelineId]);
|
|
1997
|
+
return (pipelineItemsQuery.data?.data ?? []).filter((candidate) => isAwaitingReview(candidate, resolveCurrentContentStep(candidate, pipelineSteps))).sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1998
|
+
}, [pipelineItemsQuery.data, item?.pipelineId, pipelineSteps]);
|
|
984
1999
|
const queueIndex = item ? reviewQueue.findIndex((candidate) => candidate.id === item.id) : -1;
|
|
985
2000
|
const queuePosition = queueIndex === -1 ? void 0 : queueIndex + 1;
|
|
986
2001
|
const nextQueueItemId = queueIndex === -1 ? void 0 : reviewQueue[queueIndex + 1]?.id;
|
|
@@ -1099,7 +2114,7 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1099
2114
|
] })
|
|
1100
2115
|
] }),
|
|
1101
2116
|
/* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", children: [
|
|
1102
|
-
/* @__PURE__ */ jsx(
|
|
2117
|
+
workspaceRoute ? null : /* @__PURE__ */ jsx(
|
|
1103
2118
|
Button,
|
|
1104
2119
|
{
|
|
1105
2120
|
size: "xs",
|
|
@@ -1124,6 +2139,27 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1124
2139
|
children: /* @__PURE__ */ jsx(Text, { size: "sm", children: reviewDisabledReason })
|
|
1125
2140
|
}
|
|
1126
2141
|
),
|
|
2142
|
+
workspaceRoute && /* @__PURE__ */ jsx(
|
|
2143
|
+
Alert,
|
|
2144
|
+
{
|
|
2145
|
+
icon: /* @__PURE__ */ jsx(IconExternalLink, { size: 16 }),
|
|
2146
|
+
variant: "light",
|
|
2147
|
+
title: "Reviewed in your workspace",
|
|
2148
|
+
"data-testid": "content-review-workspace-pointer",
|
|
2149
|
+
children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", align: "flex-start", children: [
|
|
2150
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: "This pipeline clears its gates on your own screen. Everything below is the piece as it stands." }),
|
|
2151
|
+
/* @__PURE__ */ jsx(
|
|
2152
|
+
Button,
|
|
2153
|
+
{
|
|
2154
|
+
size: "xs",
|
|
2155
|
+
rightSection: /* @__PURE__ */ jsx(IconExternalLink, { size: 14 }),
|
|
2156
|
+
onClick: () => void navigate({ to: workspaceRoute }),
|
|
2157
|
+
children: "Open workspace"
|
|
2158
|
+
}
|
|
2159
|
+
)
|
|
2160
|
+
] })
|
|
2161
|
+
}
|
|
2162
|
+
),
|
|
1127
2163
|
/* @__PURE__ */ jsx(
|
|
1128
2164
|
ContentReviewCard,
|
|
1129
2165
|
{
|
|
@@ -1137,7 +2173,8 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1137
2173
|
processingSteps: visibleProcessingSteps,
|
|
1138
2174
|
actions: reviewActions,
|
|
1139
2175
|
onAction: handleAction,
|
|
1140
|
-
actionsPending: isSaving
|
|
2176
|
+
actionsPending: isSaving,
|
|
2177
|
+
readOnly: workspaceRoute !== void 0
|
|
1141
2178
|
}
|
|
1142
2179
|
),
|
|
1143
2180
|
item.reviewerFeedback && /* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
@@ -1148,6 +2185,9 @@ function ContentItemReviewPage({ itemId, headerActions }) {
|
|
|
1148
2185
|
/* @__PURE__ */ jsx(Text, { size: "xs", c: "var(--color-error)", fw: 600, children: "Reject reason" }),
|
|
1149
2186
|
/* @__PURE__ */ jsx(Text, { size: "sm", children: item.rejectReason })
|
|
1150
2187
|
] }) }),
|
|
2188
|
+
/* @__PURE__ */ jsx(SourceAssetsPanel, { itemId }),
|
|
2189
|
+
/* @__PURE__ */ jsx(SlideAnalysisPanel, { itemId }),
|
|
2190
|
+
/* @__PURE__ */ jsx(CaptionPanel, { itemId, body: item.body }),
|
|
1151
2191
|
/* @__PURE__ */ jsx(Paper, { withBorder: true, p: "md", children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
1152
2192
|
/* @__PURE__ */ jsx(Title, { order: 4, children: "Attempt History" }),
|
|
1153
2193
|
attempts.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No recorded attempts." }) : /* @__PURE__ */ jsxs(Table, { children: [
|
|
@@ -1278,13 +2318,13 @@ var ACTOR_LEAD = {
|
|
|
1278
2318
|
workflow: "An automated workflow",
|
|
1279
2319
|
agent: "An AI agent"
|
|
1280
2320
|
};
|
|
1281
|
-
var
|
|
1282
|
-
|
|
1283
|
-
|
|
2321
|
+
var REVIEW_VENUE_TAIL = {
|
|
2322
|
+
workspace: "and you review it on your own screen, in the moment",
|
|
2323
|
+
queue: "then it waits for your review",
|
|
1284
2324
|
none: "and moves on without review"
|
|
1285
2325
|
};
|
|
1286
2326
|
function describeStep(step) {
|
|
1287
|
-
return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${
|
|
2327
|
+
return `${ACTOR_LEAD[step.actor]} handles "${step.label}," ${REVIEW_VENUE_TAIL[step.reviewVenue]}.`;
|
|
1288
2328
|
}
|
|
1289
2329
|
function StepRow({ step, resourceId }) {
|
|
1290
2330
|
return /* @__PURE__ */ jsxs(Group, { gap: "sm", align: "flex-start", wrap: "nowrap", children: [
|
|
@@ -1318,7 +2358,7 @@ function formatAge(ms) {
|
|
|
1318
2358
|
return `${Math.floor(hours / 24)}d`;
|
|
1319
2359
|
}
|
|
1320
2360
|
function BoardItemCard({ card, onOpen }) {
|
|
1321
|
-
return /* @__PURE__ */ jsx(Card, { withBorder: true, padding: "xs", onClick: () => onOpen(card
|
|
2361
|
+
return /* @__PURE__ */ jsx(Card, { withBorder: true, padding: "xs", onClick: () => onOpen(card), style: { cursor: "pointer" }, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
1322
2362
|
/* @__PURE__ */ jsx(Text, { size: "sm", fw: 500, lineClamp: 2, children: getContentItemIdentity(card.item) }),
|
|
1323
2363
|
/* @__PURE__ */ jsxs(Group, { gap: 6, justify: "space-between", wrap: "nowrap", children: [
|
|
1324
2364
|
/* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", children: [
|
|
@@ -1398,7 +2438,15 @@ function BoardTab({ pipeline }) {
|
|
|
1398
2438
|
const itemsQuery = useContentItems({ pipelineId: pipeline.id, limit: BOARD_FETCH_LIMIT });
|
|
1399
2439
|
const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
|
|
1400
2440
|
const board = useMemo(() => deriveContentBoard(items, pipeline, Date.now()), [items, pipeline]);
|
|
1401
|
-
const openItem = (
|
|
2441
|
+
const openItem = (card) => {
|
|
2442
|
+
const heldAtStep = pipeline.steps.find((step) => step.key === card.stepKey);
|
|
2443
|
+
const workspaceRoute = card.awaitingReview ? resolveContentWorkspaceRoute(pipeline, heldAtStep) : void 0;
|
|
2444
|
+
if (workspaceRoute) {
|
|
2445
|
+
void navigate({ to: workspaceRoute });
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
void navigate({ to: "/content/items/$itemId", params: { itemId: card.item.id } });
|
|
2449
|
+
};
|
|
1402
2450
|
if (itemsQuery.isLoading) {
|
|
1403
2451
|
return /* @__PURE__ */ jsx(Center, { p: "xl", children: /* @__PURE__ */ jsx(Loader, {}) });
|
|
1404
2452
|
}
|
|
@@ -1613,21 +2661,21 @@ function RunsTab({ pipeline }) {
|
|
|
1613
2661
|
const contentConfig = useContentConfig();
|
|
1614
2662
|
return /* @__PURE__ */ jsx(Stack, { gap: "md", children: pipeline.steps.map((step) => /* @__PURE__ */ jsx(StepRunsSection, { step, resourceId: contentConfig.resolveStepResource(step)?.resourceId }, step.key)) });
|
|
1615
2663
|
}
|
|
1616
|
-
var
|
|
1617
|
-
function
|
|
1618
|
-
return typeof value === "string" &&
|
|
2664
|
+
var PIPELINE_BOARD_TABS = ["board", "distributions", "runs"];
|
|
2665
|
+
function isPipelineBoardTab(value) {
|
|
2666
|
+
return typeof value === "string" && PIPELINE_BOARD_TABS.includes(value);
|
|
1619
2667
|
}
|
|
1620
|
-
function
|
|
2668
|
+
function ContentPipelineBoardPage({
|
|
1621
2669
|
pipelineId,
|
|
1622
2670
|
headerActions
|
|
1623
2671
|
}) {
|
|
1624
2672
|
const navigate = useNavigate();
|
|
1625
2673
|
const contentConfig = useContentConfig();
|
|
1626
2674
|
const rawSearch = useSearch({ strict: false });
|
|
1627
|
-
const activeTab =
|
|
2675
|
+
const activeTab = isPipelineBoardTab(rawSearch.tab) ? rawSearch.tab : "board";
|
|
1628
2676
|
const handleTabChange = useCallback(
|
|
1629
2677
|
(value) => {
|
|
1630
|
-
if (!
|
|
2678
|
+
if (!isPipelineBoardTab(value) || value === activeTab) return;
|
|
1631
2679
|
void navigate({
|
|
1632
2680
|
to: ".",
|
|
1633
2681
|
search: ((current) => ({ ...current, tab: value })),
|
|
@@ -1848,6 +2896,33 @@ function ContentDistributionsPage({ headerActions } = {}) {
|
|
|
1848
2896
|
] }) })
|
|
1849
2897
|
] }) }) });
|
|
1850
2898
|
}
|
|
2899
|
+
var COMMON_PLATFORM_FIELDS = [
|
|
2900
|
+
{ key: "title", label: "Title" },
|
|
2901
|
+
{ key: "description", label: "Description" },
|
|
2902
|
+
{ key: "tags", label: "Tags" }
|
|
2903
|
+
];
|
|
2904
|
+
var PLATFORM_SPECIFIC_FIELDS = {
|
|
2905
|
+
youtube: [
|
|
2906
|
+
{ key: "timestamps", label: "Timestamps" },
|
|
2907
|
+
{ key: "endScreenVideoId", label: "End Screen Video ID" },
|
|
2908
|
+
{ key: "pinnedComment", label: "Pinned Comment" }
|
|
2909
|
+
],
|
|
2910
|
+
linkedin: [
|
|
2911
|
+
{ key: "hook", label: "Hook" },
|
|
2912
|
+
{ key: "hashtags", label: "Hashtags" },
|
|
2913
|
+
{ key: "firstComment", label: "First Comment" }
|
|
2914
|
+
],
|
|
2915
|
+
instagram: [
|
|
2916
|
+
{ key: "altText", label: "Alt Text" },
|
|
2917
|
+
{ key: "hashtags", label: "Hashtags" },
|
|
2918
|
+
{ key: "storyLink", label: "Story Link" }
|
|
2919
|
+
],
|
|
2920
|
+
x: [
|
|
2921
|
+
{ key: "hookTweet", label: "Hook Tweet" },
|
|
2922
|
+
{ key: "threadTweets", label: "Thread Tweets" },
|
|
2923
|
+
{ key: "replyThread", label: "Reply Thread" }
|
|
2924
|
+
]
|
|
2925
|
+
};
|
|
1851
2926
|
function formatDateTime3(value) {
|
|
1852
2927
|
return value ? new Date(value).toLocaleString() : "\u2014";
|
|
1853
2928
|
}
|
|
@@ -1862,6 +2937,17 @@ function getChecklistProgress(checklist) {
|
|
|
1862
2937
|
const done = checklist.filter((item) => item.done).length;
|
|
1863
2938
|
return { done, total: checklist.length, pct: Math.round(done / checklist.length * 100) };
|
|
1864
2939
|
}
|
|
2940
|
+
function hasPlatformFieldValue(value) {
|
|
2941
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
2942
|
+
return typeof value === "string" && value.trim() !== "";
|
|
2943
|
+
}
|
|
2944
|
+
function PlatformContentField({ label, value }) {
|
|
2945
|
+
if (!hasPlatformFieldValue(value)) return null;
|
|
2946
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
2947
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: label }),
|
|
2948
|
+
Array.isArray(value) ? /* @__PURE__ */ jsx(Group, { gap: 4, children: value.map((entry, index) => /* @__PURE__ */ jsx(Badge, { size: "sm", variant: "light", children: String(entry) }, `${String(entry)}-${index}`)) }) : /* @__PURE__ */ jsx(Text, { size: "sm", style: { whiteSpace: "pre-wrap" }, children: String(value) })
|
|
2949
|
+
] });
|
|
2950
|
+
}
|
|
1865
2951
|
function MetricsDisplay({ metrics }) {
|
|
1866
2952
|
if (!metrics || typeof metrics !== "object" || Object.keys(metrics).length === 0) {
|
|
1867
2953
|
return /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No metrics recorded yet." });
|
|
@@ -1885,17 +2971,14 @@ function ContentDistributionDetailPage({
|
|
|
1885
2971
|
const distributionQuery = useContentDistribution(distributionId);
|
|
1886
2972
|
const dist = distributionQuery.data;
|
|
1887
2973
|
const itemQuery = useContentItem(dist?.contentItemId);
|
|
1888
|
-
const updateDistribution = useUpdateContentDistribution();
|
|
1889
|
-
const [platformContent, setPlatformContent] = useState({});
|
|
1890
|
-
const [platformUrl, setPlatformUrl] = useState("");
|
|
1891
|
-
const [status, setStatus] = useState("");
|
|
1892
2974
|
const [mediaOpened, { open: openMedia, close: closeMedia }] = useDisclosure(false);
|
|
1893
2975
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2976
|
+
const mediaEntries = useMemo(() => dist?.mediaUrls ?? [], [dist]);
|
|
2977
|
+
const mediaRefs = useMemo(
|
|
2978
|
+
() => mediaEntries.filter((entry) => entry.kind === "storage").map(({ bucket, path }) => ({ bucket, path })),
|
|
2979
|
+
[mediaEntries]
|
|
2980
|
+
);
|
|
2981
|
+
const { urls: signedMediaUrls } = useStorageSignedUrls(mediaRefs);
|
|
1899
2982
|
const backButton = /* @__PURE__ */ jsx(
|
|
1900
2983
|
Button,
|
|
1901
2984
|
{
|
|
@@ -1922,24 +3005,15 @@ function ContentDistributionDetailPage({
|
|
|
1922
3005
|
const contentTitle = contentItem?.title ?? dist.contentItemId;
|
|
1923
3006
|
const checklist = toChecklist(dist.checklist);
|
|
1924
3007
|
const checklistProgress = getChecklistProgress(checklist);
|
|
1925
|
-
const
|
|
3008
|
+
const mediaItems = mediaEntries.map(
|
|
3009
|
+
(entry) => entry.kind === "storage" ? { key: storageObjectRefKey(entry), url: signedMediaUrls[storageObjectRefKey(entry)] ?? null } : { key: entry.url, url: entry.url }
|
|
3010
|
+
);
|
|
1926
3011
|
const platformEntry = contentConfig.stageCatalog[dist.platform];
|
|
1927
3012
|
const platformColors = getContentStageColor(platformEntry);
|
|
1928
3013
|
const pillarEntry = contentItem?.pillar ? contentConfig.stageCatalog[contentItem.pillar] : void 0;
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
};
|
|
1933
|
-
const updatePlatformField = (field, value) => {
|
|
1934
|
-
setPlatformContent((prev) => ({ ...prev, [field]: value }));
|
|
1935
|
-
};
|
|
1936
|
-
const toggleChecklistItem = (key) => {
|
|
1937
|
-
const nextChecklist = checklist.map((item) => item.key === key ? { ...item, done: !item.done } : item);
|
|
1938
|
-
patch({ checklist: nextChecklist });
|
|
1939
|
-
};
|
|
1940
|
-
const deleteMedia = (url) => {
|
|
1941
|
-
patch({ mediaUrls: mediaUrls.filter((existing) => existing !== url) });
|
|
1942
|
-
};
|
|
3014
|
+
const platformContent = dist.platformContent ?? {};
|
|
3015
|
+
const platformFields = [...COMMON_PLATFORM_FIELDS, ...PLATFORM_SPECIFIC_FIELDS[dist.platform] ?? []];
|
|
3016
|
+
const hasPlatformContent = platformFields.some((field) => hasPlatformFieldValue(platformContent[field.key]));
|
|
1943
3017
|
return /* @__PURE__ */ jsxs(SubshellContentContainer, { children: [
|
|
1944
3018
|
/* @__PURE__ */ jsxs(PageContainer, { children: [
|
|
1945
3019
|
/* @__PURE__ */ jsxs(Stack, { children: [
|
|
@@ -1997,30 +3071,9 @@ function ContentDistributionDetailPage({
|
|
|
1997
3071
|
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Format" }),
|
|
1998
3072
|
/* @__PURE__ */ jsx(Text, { size: "sm", children: dist.format })
|
|
1999
3073
|
] }),
|
|
2000
|
-
/* @__PURE__ */ jsxs(Group, {
|
|
3074
|
+
/* @__PURE__ */ jsxs(Group, { children: [
|
|
2001
3075
|
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Status" }),
|
|
2002
|
-
/* @__PURE__ */ jsx(
|
|
2003
|
-
TextInput,
|
|
2004
|
-
{
|
|
2005
|
-
size: "xs",
|
|
2006
|
-
value: status,
|
|
2007
|
-
onChange: (event) => setStatus(event.currentTarget.value),
|
|
2008
|
-
style: { flex: 1 },
|
|
2009
|
-
onBlur: () => {
|
|
2010
|
-
const value = status.trim();
|
|
2011
|
-
if (value && value !== dist.status) {
|
|
2012
|
-
const updates = { status: value };
|
|
2013
|
-
if (value === "published" && !dist.publishedAt) {
|
|
2014
|
-
updates.publishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2015
|
-
}
|
|
2016
|
-
patch(updates);
|
|
2017
|
-
}
|
|
2018
|
-
},
|
|
2019
|
-
onKeyDown: (event) => {
|
|
2020
|
-
if (event.key === "Enter") event.currentTarget.blur();
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
)
|
|
3076
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", children: dist.status })
|
|
2024
3077
|
] }),
|
|
2025
3078
|
/* @__PURE__ */ jsxs(Group, { children: [
|
|
2026
3079
|
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "Publish Method" }),
|
|
@@ -2044,26 +3097,20 @@ function ContentDistributionDetailPage({
|
|
|
2044
3097
|
] }),
|
|
2045
3098
|
/* @__PURE__ */ jsxs(Group, { wrap: "nowrap", children: [
|
|
2046
3099
|
/* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", w: 140, children: "URL" }),
|
|
2047
|
-
/* @__PURE__ */ jsx(
|
|
2048
|
-
|
|
3100
|
+
dist.platformUrl ? /* @__PURE__ */ jsx(
|
|
3101
|
+
Anchor,
|
|
2049
3102
|
{
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
style: {
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
}
|
|
2060
|
-
},
|
|
2061
|
-
onKeyDown: (event) => {
|
|
2062
|
-
if (event.key === "Enter") event.currentTarget.blur();
|
|
2063
|
-
},
|
|
2064
|
-
rightSection: dist.platformUrl ? /* @__PURE__ */ jsx(Anchor, { href: dist.platformUrl, target: "_blank", rel: "noopener noreferrer", children: /* @__PURE__ */ jsx(IconExternalLink, { size: 14 }) }) : void 0
|
|
3103
|
+
href: dist.platformUrl,
|
|
3104
|
+
target: "_blank",
|
|
3105
|
+
rel: "noopener noreferrer",
|
|
3106
|
+
size: "sm",
|
|
3107
|
+
style: { overflowWrap: "anywhere" },
|
|
3108
|
+
children: /* @__PURE__ */ jsxs(Group, { gap: 4, wrap: "nowrap", children: [
|
|
3109
|
+
dist.platformUrl,
|
|
3110
|
+
/* @__PURE__ */ jsx(IconExternalLink, { size: 14 })
|
|
3111
|
+
] })
|
|
2065
3112
|
}
|
|
2066
|
-
)
|
|
3113
|
+
) : /* @__PURE__ */ jsx(Text, { size: "sm", children: "\u2014" })
|
|
2067
3114
|
] })
|
|
2068
3115
|
] })
|
|
2069
3116
|
] }) }),
|
|
@@ -2094,174 +3141,15 @@ function ContentDistributionDetailPage({
|
|
|
2094
3141
|
] })
|
|
2095
3142
|
] }),
|
|
2096
3143
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2097
|
-
/* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", verticalSpacing: "sm", children: checklist.map((item) => /* @__PURE__ */
|
|
2098
|
-
|
|
2099
|
-
{
|
|
2100
|
-
|
|
2101
|
-
checked: item.done,
|
|
2102
|
-
onChange: () => toggleChecklistItem(item.key),
|
|
2103
|
-
disabled: updateDistribution.isPending,
|
|
2104
|
-
size: "sm"
|
|
2105
|
-
},
|
|
2106
|
-
item.key
|
|
2107
|
-
)) })
|
|
3144
|
+
/* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, sm: 2 }, spacing: "sm", verticalSpacing: "sm", children: checklist.map((item) => /* @__PURE__ */ jsxs(Group, { gap: "xs", wrap: "nowrap", align: "flex-start", children: [
|
|
3145
|
+
item.done ? /* @__PURE__ */ jsx(IconCircleCheck, { size: 16, color: "var(--color-success)" }) : /* @__PURE__ */ jsx(IconCircleDashed, { size: 16, color: "var(--mantine-color-dimmed)" }),
|
|
3146
|
+
/* @__PURE__ */ jsx(Text, { size: "sm", c: item.done ? "dimmed" : void 0, children: item.label })
|
|
3147
|
+
] }, item.key)) })
|
|
2108
3148
|
] }) }) : null,
|
|
2109
3149
|
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2110
|
-
/* @__PURE__ */
|
|
2111
|
-
/* @__PURE__ */ jsx(Title, { order: 3, children: "Platform Content" }),
|
|
2112
|
-
/* @__PURE__ */ jsx(
|
|
2113
|
-
Button,
|
|
2114
|
-
{
|
|
2115
|
-
size: "xs",
|
|
2116
|
-
disabled: !isPlatformContentDirty,
|
|
2117
|
-
loading: updateDistribution.isPending,
|
|
2118
|
-
onClick: () => patch({ platformContent }),
|
|
2119
|
-
children: "Save"
|
|
2120
|
-
}
|
|
2121
|
-
)
|
|
2122
|
-
] }),
|
|
3150
|
+
/* @__PURE__ */ jsx(Title, { order: 3, children: "Platform Content" }),
|
|
2123
3151
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2124
|
-
/* @__PURE__ */ jsx(
|
|
2125
|
-
TextInput,
|
|
2126
|
-
{
|
|
2127
|
-
label: "Title",
|
|
2128
|
-
value: platformContent.title ?? "",
|
|
2129
|
-
onChange: (event) => updatePlatformField("title", event.currentTarget.value)
|
|
2130
|
-
}
|
|
2131
|
-
),
|
|
2132
|
-
/* @__PURE__ */ jsx(
|
|
2133
|
-
Textarea,
|
|
2134
|
-
{
|
|
2135
|
-
label: "Description",
|
|
2136
|
-
autosize: true,
|
|
2137
|
-
minRows: 2,
|
|
2138
|
-
value: platformContent.description ?? "",
|
|
2139
|
-
onChange: (event) => updatePlatformField("description", event.currentTarget.value)
|
|
2140
|
-
}
|
|
2141
|
-
),
|
|
2142
|
-
/* @__PURE__ */ jsx(
|
|
2143
|
-
TagsInput,
|
|
2144
|
-
{
|
|
2145
|
-
label: "Tags",
|
|
2146
|
-
value: platformContent.tags ?? [],
|
|
2147
|
-
onChange: (value) => updatePlatformField("tags", value)
|
|
2148
|
-
}
|
|
2149
|
-
),
|
|
2150
|
-
dist.platform === "youtube" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2151
|
-
/* @__PURE__ */ jsx(
|
|
2152
|
-
TagsInput,
|
|
2153
|
-
{
|
|
2154
|
-
label: "Timestamps",
|
|
2155
|
-
placeholder: "0:00 Intro",
|
|
2156
|
-
value: platformContent.timestamps ?? [],
|
|
2157
|
-
onChange: (value) => updatePlatformField("timestamps", value)
|
|
2158
|
-
}
|
|
2159
|
-
),
|
|
2160
|
-
/* @__PURE__ */ jsx(
|
|
2161
|
-
TextInput,
|
|
2162
|
-
{
|
|
2163
|
-
label: "End Screen Video ID",
|
|
2164
|
-
value: platformContent.endScreenVideoId ?? "",
|
|
2165
|
-
onChange: (event) => updatePlatformField("endScreenVideoId", event.currentTarget.value)
|
|
2166
|
-
}
|
|
2167
|
-
),
|
|
2168
|
-
/* @__PURE__ */ jsx(
|
|
2169
|
-
Textarea,
|
|
2170
|
-
{
|
|
2171
|
-
label: "Pinned Comment",
|
|
2172
|
-
autosize: true,
|
|
2173
|
-
minRows: 2,
|
|
2174
|
-
value: platformContent.pinnedComment ?? "",
|
|
2175
|
-
onChange: (event) => updatePlatformField("pinnedComment", event.currentTarget.value)
|
|
2176
|
-
}
|
|
2177
|
-
)
|
|
2178
|
-
] }),
|
|
2179
|
-
dist.platform === "linkedin" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2180
|
-
/* @__PURE__ */ jsx(
|
|
2181
|
-
Textarea,
|
|
2182
|
-
{
|
|
2183
|
-
label: "Hook",
|
|
2184
|
-
autosize: true,
|
|
2185
|
-
minRows: 2,
|
|
2186
|
-
value: platformContent.hook ?? "",
|
|
2187
|
-
onChange: (event) => updatePlatformField("hook", event.currentTarget.value)
|
|
2188
|
-
}
|
|
2189
|
-
),
|
|
2190
|
-
/* @__PURE__ */ jsx(
|
|
2191
|
-
TagsInput,
|
|
2192
|
-
{
|
|
2193
|
-
label: "Hashtags",
|
|
2194
|
-
value: platformContent.hashtags ?? [],
|
|
2195
|
-
onChange: (value) => updatePlatformField("hashtags", value)
|
|
2196
|
-
}
|
|
2197
|
-
),
|
|
2198
|
-
/* @__PURE__ */ jsx(
|
|
2199
|
-
Textarea,
|
|
2200
|
-
{
|
|
2201
|
-
label: "First Comment",
|
|
2202
|
-
autosize: true,
|
|
2203
|
-
minRows: 2,
|
|
2204
|
-
value: platformContent.firstComment ?? "",
|
|
2205
|
-
onChange: (event) => updatePlatformField("firstComment", event.currentTarget.value)
|
|
2206
|
-
}
|
|
2207
|
-
)
|
|
2208
|
-
] }),
|
|
2209
|
-
dist.platform === "instagram" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2210
|
-
/* @__PURE__ */ jsx(
|
|
2211
|
-
Textarea,
|
|
2212
|
-
{
|
|
2213
|
-
label: "Alt Text",
|
|
2214
|
-
autosize: true,
|
|
2215
|
-
minRows: 2,
|
|
2216
|
-
value: platformContent.altText ?? "",
|
|
2217
|
-
onChange: (event) => updatePlatformField("altText", event.currentTarget.value)
|
|
2218
|
-
}
|
|
2219
|
-
),
|
|
2220
|
-
/* @__PURE__ */ jsx(
|
|
2221
|
-
TagsInput,
|
|
2222
|
-
{
|
|
2223
|
-
label: "Hashtags",
|
|
2224
|
-
value: platformContent.hashtags ?? [],
|
|
2225
|
-
onChange: (value) => updatePlatformField("hashtags", value)
|
|
2226
|
-
}
|
|
2227
|
-
),
|
|
2228
|
-
/* @__PURE__ */ jsx(
|
|
2229
|
-
TextInput,
|
|
2230
|
-
{
|
|
2231
|
-
label: "Story Link",
|
|
2232
|
-
value: platformContent.storyLink ?? "",
|
|
2233
|
-
onChange: (event) => updatePlatformField("storyLink", event.currentTarget.value)
|
|
2234
|
-
}
|
|
2235
|
-
)
|
|
2236
|
-
] }),
|
|
2237
|
-
dist.platform === "x" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2238
|
-
/* @__PURE__ */ jsx(
|
|
2239
|
-
Textarea,
|
|
2240
|
-
{
|
|
2241
|
-
label: "Hook Tweet",
|
|
2242
|
-
autosize: true,
|
|
2243
|
-
minRows: 2,
|
|
2244
|
-
value: platformContent.hookTweet ?? "",
|
|
2245
|
-
onChange: (event) => updatePlatformField("hookTweet", event.currentTarget.value)
|
|
2246
|
-
}
|
|
2247
|
-
),
|
|
2248
|
-
/* @__PURE__ */ jsx(
|
|
2249
|
-
TagsInput,
|
|
2250
|
-
{
|
|
2251
|
-
label: "Thread Tweets",
|
|
2252
|
-
value: platformContent.threadTweets ?? [],
|
|
2253
|
-
onChange: (value) => updatePlatformField("threadTweets", value)
|
|
2254
|
-
}
|
|
2255
|
-
),
|
|
2256
|
-
/* @__PURE__ */ jsx(
|
|
2257
|
-
TextInput,
|
|
2258
|
-
{
|
|
2259
|
-
label: "Reply Thread",
|
|
2260
|
-
value: platformContent.replyThread ?? "",
|
|
2261
|
-
onChange: (event) => updatePlatformField("replyThread", event.currentTarget.value)
|
|
2262
|
-
}
|
|
2263
|
-
)
|
|
2264
|
-
] })
|
|
3152
|
+
hasPlatformContent ? platformFields.map((field) => /* @__PURE__ */ jsx(PlatformContentField, { label: field.label, value: platformContent[field.key] }, field.key)) : /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No platform content recorded yet." })
|
|
2265
3153
|
] }) }),
|
|
2266
3154
|
dist.adaptedBody ? /* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2267
3155
|
/* @__PURE__ */ jsx(Title, { order: 3, children: "Adapted Content" }),
|
|
@@ -2295,24 +3183,24 @@ function ContentDistributionDetailPage({
|
|
|
2295
3183
|
/* @__PURE__ */ jsx(Card, { withBorder: true, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
2296
3184
|
/* @__PURE__ */ jsxs(Group, { justify: "space-between", children: [
|
|
2297
3185
|
/* @__PURE__ */ jsx(Title, { order: 3, children: "Media" }),
|
|
2298
|
-
|
|
2299
|
-
|
|
3186
|
+
mediaItems.length > 0 ? /* @__PURE__ */ jsxs(Text, { size: "xs", c: "dimmed", children: [
|
|
3187
|
+
mediaItems.length,
|
|
2300
3188
|
" image",
|
|
2301
|
-
|
|
3189
|
+
mediaItems.length !== 1 ? "s" : ""
|
|
2302
3190
|
] }) : null
|
|
2303
3191
|
] }),
|
|
2304
3192
|
/* @__PURE__ */ jsx(Divider, {}),
|
|
2305
|
-
|
|
2306
|
-
/* @__PURE__ */ jsx(
|
|
2307
|
-
Image,
|
|
3193
|
+
mediaItems.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "sm", c: "dimmed", children: "No media yet." }) : /* @__PURE__ */ jsx(SimpleGrid, { cols: { base: 1, xs: 2, sm: 3 }, spacing: "sm", children: mediaItems.map((item, index) => /* @__PURE__ */ jsxs(Card, { withBorder: true, radius: "md", style: { overflow: "hidden" }, children: [
|
|
3194
|
+
item.url === null ? /* @__PURE__ */ jsx(Center, { h: 120, children: /* @__PURE__ */ jsx(Loader, { size: "sm" }) }) : /* @__PURE__ */ jsx(
|
|
3195
|
+
Image$1,
|
|
2308
3196
|
{
|
|
2309
|
-
src: url,
|
|
3197
|
+
src: item.url,
|
|
2310
3198
|
alt: `Media variant ${index + 1}`,
|
|
2311
3199
|
w: "100%",
|
|
2312
3200
|
radius: "sm",
|
|
2313
3201
|
style: { cursor: "pointer" },
|
|
2314
3202
|
onClick: () => {
|
|
2315
|
-
setPreviewUrl(url);
|
|
3203
|
+
setPreviewUrl(item.url);
|
|
2316
3204
|
openMedia();
|
|
2317
3205
|
}
|
|
2318
3206
|
}
|
|
@@ -2323,32 +3211,45 @@ function ContentDistributionDetailPage({
|
|
|
2323
3211
|
{
|
|
2324
3212
|
variant: "subtle",
|
|
2325
3213
|
size: "sm",
|
|
3214
|
+
disabled: item.url === null,
|
|
2326
3215
|
onClick: () => {
|
|
2327
|
-
setPreviewUrl(url);
|
|
3216
|
+
setPreviewUrl(item.url);
|
|
2328
3217
|
openMedia();
|
|
2329
3218
|
},
|
|
2330
3219
|
children: /* @__PURE__ */ jsx(IconMaximize, { size: 16 })
|
|
2331
3220
|
}
|
|
2332
3221
|
) }),
|
|
2333
|
-
/* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
|
|
2334
|
-
|
|
2335
|
-
|
|
3222
|
+
/* @__PURE__ */ jsx(Tooltip, { label: "Copy URL", children: /* @__PURE__ */ jsx(
|
|
3223
|
+
ActionIcon,
|
|
3224
|
+
{
|
|
3225
|
+
variant: "subtle",
|
|
3226
|
+
size: "sm",
|
|
3227
|
+
disabled: item.url === null,
|
|
3228
|
+
onClick: () => {
|
|
3229
|
+
if (item.url !== null) void navigator.clipboard.writeText(item.url);
|
|
3230
|
+
},
|
|
3231
|
+
children: /* @__PURE__ */ jsx(IconCopy, { size: 16 })
|
|
3232
|
+
}
|
|
3233
|
+
) }),
|
|
3234
|
+
/* @__PURE__ */ jsx(Tooltip, { label: "Download", children: /* @__PURE__ */ jsx(
|
|
2336
3235
|
ActionIcon,
|
|
2337
3236
|
{
|
|
2338
3237
|
variant: "subtle",
|
|
2339
|
-
color: "var(--color-error)",
|
|
2340
3238
|
size: "sm",
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
3239
|
+
disabled: item.url === null,
|
|
3240
|
+
component: "a",
|
|
3241
|
+
href: item.url ?? void 0,
|
|
3242
|
+
download: true,
|
|
3243
|
+
target: "_blank",
|
|
3244
|
+
children: /* @__PURE__ */ jsx(IconDownload, { size: 16 })
|
|
2344
3245
|
}
|
|
2345
3246
|
) })
|
|
2346
3247
|
] })
|
|
2347
|
-
] },
|
|
3248
|
+
] }, item.key)) })
|
|
2348
3249
|
] }) })
|
|
2349
3250
|
] }),
|
|
2350
|
-
/* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
|
|
3251
|
+
/* @__PURE__ */ jsx(CustomModal, { opened: mediaOpened, onClose: closeMedia, size: "xl", children: previewUrl ? /* @__PURE__ */ jsx(Image$1, { src: previewUrl, alt: "Media preview", fit: "contain" }) : null })
|
|
2351
3252
|
] });
|
|
2352
3253
|
}
|
|
2353
3254
|
|
|
2354
|
-
export { AlternatesPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, PayloadBody, ProcessingStateStrip, ReviewActionBar, firstLine, formatElapsed, lastLine, swapFirstLine, swapLastLine };
|
|
3255
|
+
export { AlternatesPanel, AssetCropEditor, CAPTION_STEP_KEY, CAPTION_WORKFLOW_ID, CONTENT_DERIVATIVES_BUCKET, CROP_ASPECT_OPTIONS, CaptionPanel, ContentDistributionDetailPage, ContentDistributionsPage, ContentItemReviewPage, ContentItemsPage, ContentOverviewPage, ContentPipelineBoardPage, ContentPipelineBoardPage as ContentPipelineWorkspacePage, ContentPipelinesPage, ContentReviewCard, DERIVATIVE_JPEG_QUALITY, DERIVATIVE_MAX_WIDTH, IMAGE_ANALYSIS_WORKFLOW_ID, PayloadBody, PostPreview, ProcessingStateStrip, ReviewActionBar, SelectedAssetStrip, SlideAnalysisPanel, SourceAssetsPanel, cropStyle, derivativeObjectKey, firstLine, formatElapsed, isDerivativeStale, lastLine, readStoredAnalysis, renderCropToJpeg, swapFirstLine, swapLastLine };
|