@liiift-studio/sanity-font-manager 2.7.0 → 2.8.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/LICENSE +21 -0
- package/README.md +695 -437
- package/dist/index.js +2330 -7084
- package/dist/index.mjs +2331 -7085
- package/package.json +84 -83
- package/src/components/BatchUploadFonts.jsx +655 -655
- package/src/components/BulkActions.jsx +99 -99
- package/src/components/ExistingDocumentResolver.jsx +152 -152
- package/src/components/FontReviewCard.jsx +455 -455
- package/src/components/FontScriptUploaderComponent.jsx +463 -463
- package/src/components/GenerateCollectionsPairsComponent.jsx +259 -259
- package/src/components/KeyValueInput.jsx +95 -95
- package/src/components/KeyValueReferenceInput.jsx +267 -267
- package/src/components/NestedObjectArraySelector.jsx +146 -146
- package/src/components/PriceInput.jsx +26 -26
- package/src/components/PrimaryCollectionGeneratorTypeface.jsx +116 -116
- package/src/components/RegenerateSubfamiliesComponent.jsx +185 -185
- package/src/components/SetOTF.jsx +87 -87
- package/src/components/SingleUploaderTool.jsx +674 -674
- package/src/components/StatusDisplay.jsx +26 -26
- package/src/components/StyleCountInput.jsx +16 -16
- package/src/components/UpdateScriptsComponent.jsx +76 -76
- package/src/components/UploadButton.jsx +43 -43
- package/src/components/UploadModal.jsx +309 -309
- package/src/components/UploadScriptsComponent.jsx +539 -539
- package/src/components/UploadStep1Settings.jsx +272 -272
- package/src/components/UploadStep2Review.jsx +478 -478
- package/src/components/UploadStep3Execute.jsx +234 -234
- package/src/components/UploadStep3bInstances.jsx +396 -396
- package/src/components/UploadSummary.jsx +196 -196
- package/src/components/VariableInstanceReferencesInput.jsx +190 -190
- package/src/hooks/useNestedObjects.js +92 -92
- package/src/hooks/useSanityClient.js +9 -9
- package/src/index.js +120 -120
- package/src/schema/openTypeField.js +1995 -1995
- package/src/schema/styleCountField.js +12 -12
- package/src/schema/stylesField.js +302 -302
- package/src/schema/stylisticSetField.js +301 -301
- package/src/utils/buildUploadPlan.js +326 -326
- package/src/utils/executeUploadPlan.js +430 -430
- package/src/utils/executionReducer.js +56 -56
- package/src/utils/fontHelpers.js +281 -281
- package/src/utils/generateCssFile.js +207 -207
- package/src/utils/generateFontData.js +98 -98
- package/src/utils/generateFontFile.js +38 -38
- package/src/utils/generateKeywords.js +185 -185
- package/src/utils/generateSubset.js +45 -45
- package/src/utils/getEmptyFontKit.js +101 -101
- package/src/utils/parseFont.js +56 -56
- package/src/utils/parseVariableFontInstances.js +301 -301
- package/src/utils/planReducer.js +531 -531
- package/src/utils/planTypes.js +183 -183
- package/src/utils/processFontFiles.js +530 -530
- package/src/utils/regenerateFontData.js +146 -146
- package/src/utils/resolveExistingFont.js +87 -87
- package/src/utils/retitleFontEntries.js +154 -154
- package/src/utils/sanitizeForSanityId.js +65 -65
- package/src/utils/setupDecompressors.js +27 -27
- package/src/utils/updateFontPrices.js +94 -94
- package/src/utils/updateTypefaceDocument.js +162 -162
- package/src/utils/uploadFontFiles.js +405 -405
- package/src/utils/utils.js +24 -24
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
// Bulk actions bar — expand/collapse all, search, filter with counts
|
|
2
|
-
|
|
3
|
-
import React, { useMemo } from 'react';
|
|
4
|
-
import { Flex, Box, Button, TextInput, Select, Text, Label } from '@sanity/ui';
|
|
5
|
-
import { SearchIcon } from '@sanity/icons';
|
|
6
|
-
import { FONT_STATUS, RECOMMENDATION } from '../utils/planTypes';
|
|
7
|
-
|
|
8
|
-
/** Determines whether a font entry will create or update a document */
|
|
9
|
-
function isUpdateEntry(entry) {
|
|
10
|
-
const d = entry.decisions?.existingDocument;
|
|
11
|
-
const choice = d?.userChoice;
|
|
12
|
-
const rec = d?.recommendation;
|
|
13
|
-
return choice === 'update' || (!choice && (rec === RECOMMENDATION.USE_EXACT || rec === RECOMMENDATION.USE_CANDIDATE));
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Sticky bulk actions bar for the review step.
|
|
18
|
-
*/
|
|
19
|
-
export default function BulkActions({
|
|
20
|
-
fonts,
|
|
21
|
-
dispatch,
|
|
22
|
-
searchQuery,
|
|
23
|
-
onSearchChange,
|
|
24
|
-
filterBy,
|
|
25
|
-
onFilterChange,
|
|
26
|
-
allExpanded,
|
|
27
|
-
onToggleExpandAll,
|
|
28
|
-
visibleTempIds,
|
|
29
|
-
}) {
|
|
30
|
-
const fontEntries = useMemo(() => Object.values(fonts), [fonts]);
|
|
31
|
-
const fontCount = fontEntries.length;
|
|
32
|
-
const visibleCount = visibleTempIds.length;
|
|
33
|
-
|
|
34
|
-
// Compute counts for each filter category
|
|
35
|
-
const filterCounts = useMemo(() => {
|
|
36
|
-
const createCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && !isUpdateEntry(f)).length;
|
|
37
|
-
const updateCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && isUpdateEntry(f)).length;
|
|
38
|
-
const errorCount = fontEntries.filter(f => f.status === FONT_STATUS.ERROR).length;
|
|
39
|
-
const conflictCount = fontEntries.filter(f => f._idConflict).length;
|
|
40
|
-
const italicCount = fontEntries.filter(f => f.style === 'Italic' && f.status !== FONT_STATUS.ERROR).length;
|
|
41
|
-
const regularCount = fontEntries.filter(f => f.style === 'Regular' && f.status !== FONT_STATUS.ERROR).length;
|
|
42
|
-
|
|
43
|
-
// Subfamily counts
|
|
44
|
-
const subfamilyCounts = {};
|
|
45
|
-
fontEntries.forEach(f => {
|
|
46
|
-
if (f.status === FONT_STATUS.ERROR) return;
|
|
47
|
-
const sf = f.subfamily || 'Regular';
|
|
48
|
-
subfamilyCounts[sf] = (subfamilyCounts[sf] || 0) + 1;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return { createCount, updateCount, errorCount, conflictCount, italicCount, regularCount, subfamilyCounts };
|
|
52
|
-
}, [fontEntries]);
|
|
53
|
-
|
|
54
|
-
const subfamilies = useMemo(() =>
|
|
55
|
-
Object.keys(filterCounts.subfamilyCounts).sort((a, b) => {
|
|
56
|
-
if (a === 'Regular') return -1;
|
|
57
|
-
if (b === 'Regular') return 1;
|
|
58
|
-
return a.localeCompare(b);
|
|
59
|
-
}),
|
|
60
|
-
[filterCounts]
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<Flex gap={2} align="center" wrap="wrap" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--card-bg-color)', paddingBottom: 8, paddingTop: 4 }}>
|
|
65
|
-
{/* Search */}
|
|
66
|
-
<Box style={{ flex: 1, minWidth: 150 }}>
|
|
67
|
-
<TextInput
|
|
68
|
-
icon={SearchIcon}
|
|
69
|
-
placeholder="Search fonts..."
|
|
70
|
-
value={searchQuery}
|
|
71
|
-
onChange={(e) => onSearchChange(e.target.value)}
|
|
72
|
-
fontSize={1}
|
|
73
|
-
/>
|
|
74
|
-
</Box>
|
|
75
|
-
|
|
76
|
-
{/* Filter by */}
|
|
77
|
-
<Flex align="center" gap={1}>
|
|
78
|
-
<Label size={0} style={{ whiteSpace: 'nowrap' }}>Filter</Label>
|
|
79
|
-
<Select value={filterBy} onChange={(e) => onFilterChange(e.target.value)} fontSize={1} style={{ minWidth: 140 }}>
|
|
80
|
-
<option value="all">All ({fontCount})</option>
|
|
81
|
-
{filterCounts.createCount > 0 && <option value="create">Create ({filterCounts.createCount})</option>}
|
|
82
|
-
{filterCounts.updateCount > 0 && <option value="update">Update ({filterCounts.updateCount})</option>}
|
|
83
|
-
{filterCounts.regularCount > 0 && <option value="style:regular">Regular ({filterCounts.regularCount})</option>}
|
|
84
|
-
{filterCounts.italicCount > 0 && <option value="style:italic">Italic ({filterCounts.italicCount})</option>}
|
|
85
|
-
{filterCounts.errorCount > 0 && <option value="error">Errors ({filterCounts.errorCount})</option>}
|
|
86
|
-
{filterCounts.conflictCount > 0 && <option value="conflict">Conflicts ({filterCounts.conflictCount})</option>}
|
|
87
|
-
{subfamilies.length > 1 && subfamilies.map(sf => (
|
|
88
|
-
<option key={sf} value={`sf:${sf}`}>{sf} ({filterCounts.subfamilyCounts[sf]})</option>
|
|
89
|
-
))}
|
|
90
|
-
</Select>
|
|
91
|
-
</Flex>
|
|
92
|
-
|
|
93
|
-
{/* Visible count */}
|
|
94
|
-
{visibleCount !== fontCount && (
|
|
95
|
-
<Text size={0} muted>{visibleCount} of {fontCount}</Text>
|
|
96
|
-
)}
|
|
97
|
-
</Flex>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
1
|
+
// Bulk actions bar — expand/collapse all, search, filter with counts
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
import { Flex, Box, Button, TextInput, Select, Text, Label } from '@sanity/ui';
|
|
5
|
+
import { SearchIcon } from '@sanity/icons';
|
|
6
|
+
import { FONT_STATUS, RECOMMENDATION } from '../utils/planTypes';
|
|
7
|
+
|
|
8
|
+
/** Determines whether a font entry will create or update a document */
|
|
9
|
+
function isUpdateEntry(entry) {
|
|
10
|
+
const d = entry.decisions?.existingDocument;
|
|
11
|
+
const choice = d?.userChoice;
|
|
12
|
+
const rec = d?.recommendation;
|
|
13
|
+
return choice === 'update' || (!choice && (rec === RECOMMENDATION.USE_EXACT || rec === RECOMMENDATION.USE_CANDIDATE));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sticky bulk actions bar for the review step.
|
|
18
|
+
*/
|
|
19
|
+
export default function BulkActions({
|
|
20
|
+
fonts,
|
|
21
|
+
dispatch,
|
|
22
|
+
searchQuery,
|
|
23
|
+
onSearchChange,
|
|
24
|
+
filterBy,
|
|
25
|
+
onFilterChange,
|
|
26
|
+
allExpanded,
|
|
27
|
+
onToggleExpandAll,
|
|
28
|
+
visibleTempIds,
|
|
29
|
+
}) {
|
|
30
|
+
const fontEntries = useMemo(() => Object.values(fonts), [fonts]);
|
|
31
|
+
const fontCount = fontEntries.length;
|
|
32
|
+
const visibleCount = visibleTempIds.length;
|
|
33
|
+
|
|
34
|
+
// Compute counts for each filter category
|
|
35
|
+
const filterCounts = useMemo(() => {
|
|
36
|
+
const createCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && !isUpdateEntry(f)).length;
|
|
37
|
+
const updateCount = fontEntries.filter(f => f.status !== FONT_STATUS.ERROR && isUpdateEntry(f)).length;
|
|
38
|
+
const errorCount = fontEntries.filter(f => f.status === FONT_STATUS.ERROR).length;
|
|
39
|
+
const conflictCount = fontEntries.filter(f => f._idConflict).length;
|
|
40
|
+
const italicCount = fontEntries.filter(f => f.style === 'Italic' && f.status !== FONT_STATUS.ERROR).length;
|
|
41
|
+
const regularCount = fontEntries.filter(f => f.style === 'Regular' && f.status !== FONT_STATUS.ERROR).length;
|
|
42
|
+
|
|
43
|
+
// Subfamily counts
|
|
44
|
+
const subfamilyCounts = {};
|
|
45
|
+
fontEntries.forEach(f => {
|
|
46
|
+
if (f.status === FONT_STATUS.ERROR) return;
|
|
47
|
+
const sf = f.subfamily || 'Regular';
|
|
48
|
+
subfamilyCounts[sf] = (subfamilyCounts[sf] || 0) + 1;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return { createCount, updateCount, errorCount, conflictCount, italicCount, regularCount, subfamilyCounts };
|
|
52
|
+
}, [fontEntries]);
|
|
53
|
+
|
|
54
|
+
const subfamilies = useMemo(() =>
|
|
55
|
+
Object.keys(filterCounts.subfamilyCounts).sort((a, b) => {
|
|
56
|
+
if (a === 'Regular') return -1;
|
|
57
|
+
if (b === 'Regular') return 1;
|
|
58
|
+
return a.localeCompare(b);
|
|
59
|
+
}),
|
|
60
|
+
[filterCounts]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Flex gap={2} align="center" wrap="wrap" style={{ position: 'sticky', top: 0, zIndex: 10, background: 'var(--card-bg-color)', paddingBottom: 8, paddingTop: 4 }}>
|
|
65
|
+
{/* Search */}
|
|
66
|
+
<Box style={{ flex: 1, minWidth: 150 }}>
|
|
67
|
+
<TextInput
|
|
68
|
+
icon={SearchIcon}
|
|
69
|
+
placeholder="Search fonts..."
|
|
70
|
+
value={searchQuery}
|
|
71
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
72
|
+
fontSize={1}
|
|
73
|
+
/>
|
|
74
|
+
</Box>
|
|
75
|
+
|
|
76
|
+
{/* Filter by */}
|
|
77
|
+
<Flex align="center" gap={1}>
|
|
78
|
+
<Label size={0} style={{ whiteSpace: 'nowrap' }}>Filter</Label>
|
|
79
|
+
<Select value={filterBy} onChange={(e) => onFilterChange(e.target.value)} fontSize={1} style={{ minWidth: 140 }}>
|
|
80
|
+
<option value="all">All ({fontCount})</option>
|
|
81
|
+
{filterCounts.createCount > 0 && <option value="create">Create ({filterCounts.createCount})</option>}
|
|
82
|
+
{filterCounts.updateCount > 0 && <option value="update">Update ({filterCounts.updateCount})</option>}
|
|
83
|
+
{filterCounts.regularCount > 0 && <option value="style:regular">Regular ({filterCounts.regularCount})</option>}
|
|
84
|
+
{filterCounts.italicCount > 0 && <option value="style:italic">Italic ({filterCounts.italicCount})</option>}
|
|
85
|
+
{filterCounts.errorCount > 0 && <option value="error">Errors ({filterCounts.errorCount})</option>}
|
|
86
|
+
{filterCounts.conflictCount > 0 && <option value="conflict">Conflicts ({filterCounts.conflictCount})</option>}
|
|
87
|
+
{subfamilies.length > 1 && subfamilies.map(sf => (
|
|
88
|
+
<option key={sf} value={`sf:${sf}`}>{sf} ({filterCounts.subfamilyCounts[sf]})</option>
|
|
89
|
+
))}
|
|
90
|
+
</Select>
|
|
91
|
+
</Flex>
|
|
92
|
+
|
|
93
|
+
{/* Visible count */}
|
|
94
|
+
{visibleCount !== fontCount && (
|
|
95
|
+
<Text size={0} muted>{visibleCount} of {fontCount}</Text>
|
|
96
|
+
)}
|
|
97
|
+
</Flex>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
// Existing document resolution UI — toggle between update existing and create new
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Card, Stack, Flex, Text, Badge, Button, Label, Switch, Tooltip, Box } from '@sanity/ui';
|
|
5
|
-
import { InfoOutlineIcon } from '@sanity/icons';
|
|
6
|
-
import { RECOMMENDATION } from '../utils/planTypes';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Displays the existing document resolution decision as a toggle.
|
|
10
|
-
* When "update" is on, shows the matched document. When off, indicates a new document will be created.
|
|
11
|
-
*/
|
|
12
|
-
export default function ExistingDocumentResolver({ decision, tempId, dispatch }) {
|
|
13
|
-
if (!decision) return null;
|
|
14
|
-
|
|
15
|
-
const { recommendation, exact, candidates, userChoice, selectedCandidate, lookupFailed } = decision;
|
|
16
|
-
|
|
17
|
-
const effectiveAction = userChoice ||
|
|
18
|
-
(recommendation === RECOMMENDATION.USE_EXACT || recommendation === RECOMMENDATION.USE_CANDIDATE ? 'update' : 'create');
|
|
19
|
-
const isUpdating = effectiveAction === 'update';
|
|
20
|
-
const hasMatch = exact || candidates?.length > 0;
|
|
21
|
-
|
|
22
|
-
const handleToggle = () => {
|
|
23
|
-
dispatch({ type: 'SET_FONT_ACTION', tempId, decision: isUpdating ? 'create' : 'update' });
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const handleSelectCandidate = (candidate) => {
|
|
27
|
-
dispatch({ type: 'SET_FONT_CANDIDATE', tempId, candidate });
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Lookup failed
|
|
31
|
-
if (lookupFailed) {
|
|
32
|
-
return (
|
|
33
|
-
<Card tone="caution" border padding={2} radius={1}>
|
|
34
|
-
<Text size={0}>Could not check for existing documents — will create new.</Text>
|
|
35
|
-
</Card>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// No match at all — just creating
|
|
40
|
-
if (recommendation === RECOMMENDATION.CREATE && !userChoice) {
|
|
41
|
-
return (
|
|
42
|
-
<Stack space={2}>
|
|
43
|
-
<Label size={0}>Existing Document</Label>
|
|
44
|
-
<Card tone="default" border padding={2} radius={1}>
|
|
45
|
-
<Text size={1}>No existing document found — will create new.</Text>
|
|
46
|
-
</Card>
|
|
47
|
-
</Stack>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Has a match (exact, candidate, or ambiguous) — show toggle
|
|
52
|
-
if (hasMatch) {
|
|
53
|
-
const matchDoc = exact || selectedCandidate || candidates?.[0];
|
|
54
|
-
const matchType = exact ? 'Exact Match' : candidates?.length > 1 ? 'Multiple Matches' : 'Likely Match';
|
|
55
|
-
const matchTone = exact ? 'positive' : 'caution';
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Stack space={2}>
|
|
59
|
-
<Label size={0}>Existing Document</Label>
|
|
60
|
-
|
|
61
|
-
{/* Toggle */}
|
|
62
|
-
<Flex align="center" gap={2}>
|
|
63
|
-
<Switch
|
|
64
|
-
checked={isUpdating}
|
|
65
|
-
onChange={handleToggle}
|
|
66
|
-
style={{ cursor: 'pointer' }}
|
|
67
|
-
/>
|
|
68
|
-
<Stack space={1}>
|
|
69
|
-
<Text size={1} weight="semibold">
|
|
70
|
-
{isUpdating ? 'Update existing document' : 'Create new document'}
|
|
71
|
-
</Text>
|
|
72
|
-
<Text size={0} muted>
|
|
73
|
-
{isUpdating
|
|
74
|
-
? 'Files will be uploaded to the matched document below.'
|
|
75
|
-
: 'A new document will be created. You may need to update the Document ID above to avoid conflicts.'
|
|
76
|
-
}
|
|
77
|
-
</Text>
|
|
78
|
-
</Stack>
|
|
79
|
-
</Flex>
|
|
80
|
-
|
|
81
|
-
{/* Match card — greyed out when not updating */}
|
|
82
|
-
<Card
|
|
83
|
-
tone={isUpdating ? matchTone : 'default'}
|
|
84
|
-
border
|
|
85
|
-
padding={2}
|
|
86
|
-
radius={1}
|
|
87
|
-
style={{ opacity: isUpdating ? 1 : 0.5, transition: 'opacity 0.15s ease' }}
|
|
88
|
-
>
|
|
89
|
-
<Stack space={2}>
|
|
90
|
-
<Flex align="center" gap={2}>
|
|
91
|
-
<Badge tone={isUpdating ? matchTone : 'default'} fontSize={0}>{matchType}</Badge>
|
|
92
|
-
<Text size={1} style={{ fontFamily: 'monospace' }}>{matchDoc?._id}</Text>
|
|
93
|
-
</Flex>
|
|
94
|
-
<Text size={0} muted>
|
|
95
|
-
{matchDoc?.title} · {matchDoc?.weightName} · {matchDoc?.style}
|
|
96
|
-
{matchDoc?.subfamily ? ` · ${matchDoc.subfamily}` : ''}
|
|
97
|
-
</Text>
|
|
98
|
-
</Stack>
|
|
99
|
-
</Card>
|
|
100
|
-
|
|
101
|
-
{/* Multiple candidates — show selector when updating */}
|
|
102
|
-
{isUpdating && candidates?.length > 1 && (
|
|
103
|
-
<Stack space={1}>
|
|
104
|
-
<Text size={0} muted>Select which document to update:</Text>
|
|
105
|
-
{candidates.map((candidate) => (
|
|
106
|
-
<Card
|
|
107
|
-
key={candidate._id}
|
|
108
|
-
border
|
|
109
|
-
radius={1}
|
|
110
|
-
padding={2}
|
|
111
|
-
tone={selectedCandidate?._id === candidate._id ? 'positive' : 'default'}
|
|
112
|
-
style={{ cursor: 'pointer' }}
|
|
113
|
-
onClick={() => handleSelectCandidate(candidate)}
|
|
114
|
-
>
|
|
115
|
-
<Flex align="center" gap={2}>
|
|
116
|
-
<input
|
|
117
|
-
type="radio"
|
|
118
|
-
name={`candidate-${tempId}`}
|
|
119
|
-
checked={selectedCandidate?._id === candidate._id}
|
|
120
|
-
onChange={() => handleSelectCandidate(candidate)}
|
|
121
|
-
style={{ cursor: 'pointer' }}
|
|
122
|
-
/>
|
|
123
|
-
<Stack space={1} style={{ flex: 1 }}>
|
|
124
|
-
<Text size={1} style={{ fontFamily: 'monospace' }}>{candidate._id}</Text>
|
|
125
|
-
<Text size={0} muted>
|
|
126
|
-
{candidate.title} · {candidate.weightName} · {candidate.style}
|
|
127
|
-
{candidate.subfamily ? ` · ${candidate.subfamily}` : ''}
|
|
128
|
-
</Text>
|
|
129
|
-
</Stack>
|
|
130
|
-
</Flex>
|
|
131
|
-
</Card>
|
|
132
|
-
))}
|
|
133
|
-
</Stack>
|
|
134
|
-
)}
|
|
135
|
-
</Stack>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// User chose create after initially having a match (fallback)
|
|
140
|
-
if (userChoice === 'create') {
|
|
141
|
-
return (
|
|
142
|
-
<Stack space={2}>
|
|
143
|
-
<Label size={0}>Existing Document</Label>
|
|
144
|
-
<Card border padding={2} radius={1}>
|
|
145
|
-
<Text size={1}>Will create new document</Text>
|
|
146
|
-
</Card>
|
|
147
|
-
</Stack>
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
1
|
+
// Existing document resolution UI — toggle between update existing and create new
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Card, Stack, Flex, Text, Badge, Button, Label, Switch, Tooltip, Box } from '@sanity/ui';
|
|
5
|
+
import { InfoOutlineIcon } from '@sanity/icons';
|
|
6
|
+
import { RECOMMENDATION } from '../utils/planTypes';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Displays the existing document resolution decision as a toggle.
|
|
10
|
+
* When "update" is on, shows the matched document. When off, indicates a new document will be created.
|
|
11
|
+
*/
|
|
12
|
+
export default function ExistingDocumentResolver({ decision, tempId, dispatch }) {
|
|
13
|
+
if (!decision) return null;
|
|
14
|
+
|
|
15
|
+
const { recommendation, exact, candidates, userChoice, selectedCandidate, lookupFailed } = decision;
|
|
16
|
+
|
|
17
|
+
const effectiveAction = userChoice ||
|
|
18
|
+
(recommendation === RECOMMENDATION.USE_EXACT || recommendation === RECOMMENDATION.USE_CANDIDATE ? 'update' : 'create');
|
|
19
|
+
const isUpdating = effectiveAction === 'update';
|
|
20
|
+
const hasMatch = exact || candidates?.length > 0;
|
|
21
|
+
|
|
22
|
+
const handleToggle = () => {
|
|
23
|
+
dispatch({ type: 'SET_FONT_ACTION', tempId, decision: isUpdating ? 'create' : 'update' });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const handleSelectCandidate = (candidate) => {
|
|
27
|
+
dispatch({ type: 'SET_FONT_CANDIDATE', tempId, candidate });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Lookup failed
|
|
31
|
+
if (lookupFailed) {
|
|
32
|
+
return (
|
|
33
|
+
<Card tone="caution" border padding={2} radius={1}>
|
|
34
|
+
<Text size={0}>Could not check for existing documents — will create new.</Text>
|
|
35
|
+
</Card>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// No match at all — just creating
|
|
40
|
+
if (recommendation === RECOMMENDATION.CREATE && !userChoice) {
|
|
41
|
+
return (
|
|
42
|
+
<Stack space={2}>
|
|
43
|
+
<Label size={0}>Existing Document</Label>
|
|
44
|
+
<Card tone="default" border padding={2} radius={1}>
|
|
45
|
+
<Text size={1}>No existing document found — will create new.</Text>
|
|
46
|
+
</Card>
|
|
47
|
+
</Stack>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Has a match (exact, candidate, or ambiguous) — show toggle
|
|
52
|
+
if (hasMatch) {
|
|
53
|
+
const matchDoc = exact || selectedCandidate || candidates?.[0];
|
|
54
|
+
const matchType = exact ? 'Exact Match' : candidates?.length > 1 ? 'Multiple Matches' : 'Likely Match';
|
|
55
|
+
const matchTone = exact ? 'positive' : 'caution';
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Stack space={2}>
|
|
59
|
+
<Label size={0}>Existing Document</Label>
|
|
60
|
+
|
|
61
|
+
{/* Toggle */}
|
|
62
|
+
<Flex align="center" gap={2}>
|
|
63
|
+
<Switch
|
|
64
|
+
checked={isUpdating}
|
|
65
|
+
onChange={handleToggle}
|
|
66
|
+
style={{ cursor: 'pointer' }}
|
|
67
|
+
/>
|
|
68
|
+
<Stack space={1}>
|
|
69
|
+
<Text size={1} weight="semibold">
|
|
70
|
+
{isUpdating ? 'Update existing document' : 'Create new document'}
|
|
71
|
+
</Text>
|
|
72
|
+
<Text size={0} muted>
|
|
73
|
+
{isUpdating
|
|
74
|
+
? 'Files will be uploaded to the matched document below.'
|
|
75
|
+
: 'A new document will be created. You may need to update the Document ID above to avoid conflicts.'
|
|
76
|
+
}
|
|
77
|
+
</Text>
|
|
78
|
+
</Stack>
|
|
79
|
+
</Flex>
|
|
80
|
+
|
|
81
|
+
{/* Match card — greyed out when not updating */}
|
|
82
|
+
<Card
|
|
83
|
+
tone={isUpdating ? matchTone : 'default'}
|
|
84
|
+
border
|
|
85
|
+
padding={2}
|
|
86
|
+
radius={1}
|
|
87
|
+
style={{ opacity: isUpdating ? 1 : 0.5, transition: 'opacity 0.15s ease' }}
|
|
88
|
+
>
|
|
89
|
+
<Stack space={2}>
|
|
90
|
+
<Flex align="center" gap={2}>
|
|
91
|
+
<Badge tone={isUpdating ? matchTone : 'default'} fontSize={0}>{matchType}</Badge>
|
|
92
|
+
<Text size={1} style={{ fontFamily: 'monospace' }}>{matchDoc?._id}</Text>
|
|
93
|
+
</Flex>
|
|
94
|
+
<Text size={0} muted>
|
|
95
|
+
{matchDoc?.title} · {matchDoc?.weightName} · {matchDoc?.style}
|
|
96
|
+
{matchDoc?.subfamily ? ` · ${matchDoc.subfamily}` : ''}
|
|
97
|
+
</Text>
|
|
98
|
+
</Stack>
|
|
99
|
+
</Card>
|
|
100
|
+
|
|
101
|
+
{/* Multiple candidates — show selector when updating */}
|
|
102
|
+
{isUpdating && candidates?.length > 1 && (
|
|
103
|
+
<Stack space={1}>
|
|
104
|
+
<Text size={0} muted>Select which document to update:</Text>
|
|
105
|
+
{candidates.map((candidate) => (
|
|
106
|
+
<Card
|
|
107
|
+
key={candidate._id}
|
|
108
|
+
border
|
|
109
|
+
radius={1}
|
|
110
|
+
padding={2}
|
|
111
|
+
tone={selectedCandidate?._id === candidate._id ? 'positive' : 'default'}
|
|
112
|
+
style={{ cursor: 'pointer' }}
|
|
113
|
+
onClick={() => handleSelectCandidate(candidate)}
|
|
114
|
+
>
|
|
115
|
+
<Flex align="center" gap={2}>
|
|
116
|
+
<input
|
|
117
|
+
type="radio"
|
|
118
|
+
name={`candidate-${tempId}`}
|
|
119
|
+
checked={selectedCandidate?._id === candidate._id}
|
|
120
|
+
onChange={() => handleSelectCandidate(candidate)}
|
|
121
|
+
style={{ cursor: 'pointer' }}
|
|
122
|
+
/>
|
|
123
|
+
<Stack space={1} style={{ flex: 1 }}>
|
|
124
|
+
<Text size={1} style={{ fontFamily: 'monospace' }}>{candidate._id}</Text>
|
|
125
|
+
<Text size={0} muted>
|
|
126
|
+
{candidate.title} · {candidate.weightName} · {candidate.style}
|
|
127
|
+
{candidate.subfamily ? ` · ${candidate.subfamily}` : ''}
|
|
128
|
+
</Text>
|
|
129
|
+
</Stack>
|
|
130
|
+
</Flex>
|
|
131
|
+
</Card>
|
|
132
|
+
))}
|
|
133
|
+
</Stack>
|
|
134
|
+
)}
|
|
135
|
+
</Stack>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// User chose create after initially having a match (fallback)
|
|
140
|
+
if (userChoice === 'create') {
|
|
141
|
+
return (
|
|
142
|
+
<Stack space={2}>
|
|
143
|
+
<Label size={0}>Existing Document</Label>
|
|
144
|
+
<Card border padding={2} radius={1}>
|
|
145
|
+
<Text size={1}>Will create new document</Text>
|
|
146
|
+
</Card>
|
|
147
|
+
</Stack>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|