@smartcat/sanity-plugin 1.3.0 → 1.4.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/index.cjs CHANGED
@@ -868,9 +868,12 @@ const ITEMS_QUERY = `*[_id == $id][0]{
868
868
  sendExistingTranslations,
869
869
  "doc": ${progress.itemDocDerefRespectingDraft()}
870
870
  }
871
- }`, SIBLINGS_QUERY = `*[_type == "translation.metadata" && references($ids)]{
872
- "variants": translations[]{ "language": _key, "doc": value-> }
873
871
  }`;
872
+ function siblingsQuery$1(fieldLanguageKey) {
873
+ return `*[_type == "translation.metadata" && references($ids)]{
874
+ "variants": translations[]{ "language": coalesce(${fieldLanguageKey}, _key), "doc": value-> }
875
+ }`;
876
+ }
874
877
  async function prepareExport({
875
878
  client,
876
879
  schema,
@@ -899,7 +902,7 @@ async function prepareExport({
899
902
  ).map((item) => publishedId(item.doc._id));
900
903
  if (flaggedDocLevelIds.length > 0) {
901
904
  const groups = await client.fetch(
902
- SIBLINGS_QUERY,
905
+ siblingsQuery$1(fieldLanguageKey),
903
906
  { ids: flaggedDocLevelIds }
904
907
  );
905
908
  for (const group of groups ?? []) {
@@ -1254,6 +1257,14 @@ function sameValue(a, b) {
1254
1257
  function stableString(value) {
1255
1258
  return Array.isArray(value) ? `[${value.map(stableString).join(",")}]` : value && typeof value == "object" ? `{${Object.entries(value).filter(([k]) => k !== "_key").sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableString(v)}`).join(",")}}` : JSON.stringify(value) ?? "undefined";
1256
1259
  }
1260
+ function DocTitle({ doc }) {
1261
+ const schema = sanity.useSchema(), schemaType = doc ? schema.get(doc._type) : void 0, preview = sanity.useValuePreview({
1262
+ enabled: !!(doc && schemaType),
1263
+ schemaType,
1264
+ value: doc ? { _id: doc._id, _type: doc._type } : void 0
1265
+ }), title = typeof preview.value?.title == "string" ? preview.value.title : void 0;
1266
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: title ?? (preview.isLoading ? "\u2026" : UNTITLED) });
1267
+ }
1257
1268
  function languageTitle(languages, id) {
1258
1269
  return languages.find((l) => l.id === id)?.title || id;
1259
1270
  }
@@ -1355,6 +1366,347 @@ function ItemProgress({
1355
1366
  target._key
1356
1367
  )) });
1357
1368
  }
1369
+ function OpenInStudioButton({ id, type }) {
1370
+ const { onClick, href } = router.useIntentLink({ intent: "edit", params: { id, type } });
1371
+ return /* @__PURE__ */ jsxRuntime.jsx(
1372
+ ui.Button,
1373
+ {
1374
+ as: "a",
1375
+ href,
1376
+ onClick,
1377
+ mode: "bleed",
1378
+ icon: icons.LaunchIcon,
1379
+ text: "Open in Studio",
1380
+ fontSize: 0,
1381
+ padding: 2
1382
+ }
1383
+ );
1384
+ }
1385
+ const CodeArea = styled__default.default.textarea`
1386
+ width: 100%;
1387
+ height: 70vh;
1388
+ resize: none;
1389
+ border: none;
1390
+ outline: none;
1391
+ padding: 0;
1392
+ background: transparent;
1393
+ color: inherit;
1394
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1395
+ font-size: 12px;
1396
+ line-height: 1.5;
1397
+ white-space: pre;
1398
+ tab-size: 2;
1399
+ `;
1400
+ function siblingsQuery(fieldLanguageKey) {
1401
+ return `*[_type == "translation.metadata" && references($id)][0].translations[]{"language": coalesce(${fieldLanguageKey}, _key), "doc": value->}`;
1402
+ }
1403
+ function ViewLocjsonButton(props) {
1404
+ const { docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, targetLanguages, languages } = props, [open, setOpen] = react.useState(!1), [content, setContent] = react.useState(null), [filename, setFilename] = react.useState(null), [title, setTitle] = react.useState("Source only"), [error, setError] = react.useState(null), build = react.useCallback(
1405
+ async (targetLanguage) => {
1406
+ setOpen(!0), setContent(null), setFilename(null), setError(null), setTitle(targetLanguage ? `Source + ${languageTitle(languages, targetLanguage)}` : "Source only");
1407
+ try {
1408
+ const raw = await client.fetch(
1409
+ sourceIsPublished ? "*[_id == $id][0]" : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
1410
+ { id: docId }
1411
+ );
1412
+ if (!raw) throw new Error(`Document not found: ${docId}`);
1413
+ const isDraft = typeof raw._id == "string" && raw._id.startsWith("drafts."), doc = isDraft ? { ...raw, _id: docId } : raw, fields = locjson.getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] });
1414
+ let target;
1415
+ if (targetLanguage)
1416
+ if (locjson.localizationMode(schema.get(type)) === "document") {
1417
+ const sibling = (await client.fetch(siblingsQuery(fieldI18nLanguageField), { id: docId }) ?? []).find((v) => v.language === targetLanguage)?.doc ?? void 0;
1418
+ target = { language: targetLanguage, doc: sibling };
1419
+ } else
1420
+ target = { language: targetLanguage };
1421
+ const locjson$1 = locjson.serializeToLocjson(doc, fields, {
1422
+ sourceLanguage,
1423
+ fieldLanguageKey: fieldI18nLanguageField,
1424
+ target
1425
+ }), smartcatCode = targetLanguage ? languages.find((l) => l.id === targetLanguage)?.smartcatLanguage || targetLanguage : void 0, smartcatName = locjson.buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc), {
1426
+ draft: isDraft,
1427
+ language: smartcatCode
1428
+ });
1429
+ setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson$1, null, 2));
1430
+ } catch (err) {
1431
+ setError(err instanceof Error ? err.message : String(err));
1432
+ }
1433
+ },
1434
+ [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, languages]
1435
+ ), download = react.useCallback(() => {
1436
+ if (content == null || !filename) return;
1437
+ const url = URL.createObjectURL(new Blob([content], { type: "application/json" })), anchor = document.createElement("a");
1438
+ anchor.href = url, anchor.download = filename, anchor.click(), URL.revokeObjectURL(url);
1439
+ }, [content, filename]);
1440
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1441
+ /* @__PURE__ */ jsxRuntime.jsx(
1442
+ ui.MenuButton,
1443
+ {
1444
+ id: `locjson-menu-${docId}`,
1445
+ button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", icon: icons.JsonIcon, iconRight: icons.ChevronDownIcon, title: "View LocJSON", fontSize: 0, padding: 2 }),
1446
+ menu: /* @__PURE__ */ jsxRuntime.jsxs(ui.Menu, { children: [
1447
+ /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: "Source only", onClick: () => build() }),
1448
+ targetLanguages.map((lang) => /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: `Source + ${languageTitle(languages, lang)}`, onClick: () => build(lang) }, lang))
1449
+ ] }),
1450
+ popover: { portal: !0 }
1451
+ }
1452
+ ),
1453
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1454
+ ui.Dialog,
1455
+ {
1456
+ id: `locjson-${docId}`,
1457
+ header: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1458
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { weight: "semibold", children: [
1459
+ "LocJSON \xB7 ",
1460
+ type,
1461
+ " \xB7 ",
1462
+ title
1463
+ ] }),
1464
+ /* @__PURE__ */ jsxRuntime.jsx(
1465
+ ui.Button,
1466
+ {
1467
+ icon: icons.DownloadIcon,
1468
+ text: "Download",
1469
+ mode: "ghost",
1470
+ fontSize: 1,
1471
+ padding: 2,
1472
+ disabled: content == null,
1473
+ onClick: download
1474
+ }
1475
+ )
1476
+ ] }),
1477
+ onClose: () => setOpen(!1),
1478
+ width: 3,
1479
+ children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: error ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: error }) : content === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsx(
1480
+ CodeArea,
1481
+ {
1482
+ readOnly: !0,
1483
+ spellCheck: !1,
1484
+ value: content,
1485
+ onFocus: (e) => e.currentTarget.select()
1486
+ }
1487
+ ) })
1488
+ }
1489
+ )
1490
+ ] });
1491
+ }
1492
+ const QUERY_LIMIT = 50, DISPLAY_LIMIT = 25, MAX_TOKENS = 6, MIN_QUERY_LENGTH = 2, TITLE_FIELDS = "[title, name, heading, label, question, slug.current, _type, _id]", SCORE_FIELDS = "[title, name, heading, label, question, slug.current]", EXCLUDED_TYPES = ["smartcat.translationProject", "smartcat.settings", "translation.metadata"];
1493
+ function buildSearch(rawQuery, searchContent, translatableTypes) {
1494
+ if (rawQuery.trim().length < MIN_QUERY_LENGTH) return null;
1495
+ const tokens = rawQuery.trim().split(/\s+/).map((t) => t.replace(/["*]/g, "").replace(/^-+/, "").trim()).filter(Boolean).slice(0, MAX_TOKENS);
1496
+ if (tokens.length === 0) return null;
1497
+ const target = searchContent ? "@" : TITLE_FIELDS, params = {}, tokenClauses = [], scoreClauses = [];
1498
+ tokens.forEach((tok, i) => {
1499
+ params[`t${i}`] = `${tok}*`, tokenClauses.push(`${target} match text::query($t${i})`), scoreClauses.push(`boost(${SCORE_FIELDS} match text::query($t${i}), 2)`);
1500
+ });
1501
+ let typeFilter;
1502
+ return translatableTypes ? (params.types = translatableTypes, typeFilter = "_type in $types") : (params.excluded = EXCLUDED_TYPES, typeFilter = '!(_type in $excluded) && !(_type match "sanity.*")'), { query: `*[${[typeFilter, ...tokenClauses].join(" && ")}] | score(${scoreClauses.join(", ")}) | order(_score desc)[0...${QUERY_LIMIT}]{_id, _type, "hasDraft": _id in path("drafts.**") || count(*[_id == "drafts." + ^._id]) > 0}`, params };
1503
+ }
1504
+ function dedupeHits(rows) {
1505
+ const byDoc = /* @__PURE__ */ new Map();
1506
+ for (const row of rows) {
1507
+ const docId = row._id.replace(/^drafts\./, ""), existing = byDoc.get(docId);
1508
+ existing ? existing.hasDraft = existing.hasDraft || row.hasDraft : byDoc.set(docId, { docId, _type: row._type, hasDraft: row.hasDraft });
1509
+ }
1510
+ return [...byDoc.values()].slice(0, DISPLAY_LIMIT);
1511
+ }
1512
+ const MEMBERSHIP_QUERY = `*[_type == "${constants.TRANSLATION_PROJECT_TYPE}" && count(items[${progress.ITEM_ID} in $ids]) > 0] | order(name asc){
1513
+ _id,
1514
+ name,
1515
+ "docIds": items[${progress.ITEM_ID} in $ids]{"id": ${progress.ITEM_ID}}
1516
+ }`;
1517
+ function ProjectChip({ project, current }) {
1518
+ const router$1 = router.useRouter(), onClick = react.useCallback(
1519
+ (e) => {
1520
+ e.preventDefault(), router$1.navigate({ projectId: project._id });
1521
+ },
1522
+ [router$1, project._id]
1523
+ );
1524
+ return /* @__PURE__ */ jsxRuntime.jsxs("a", { href: "#", onClick, children: [
1525
+ project.name || "Untitled project",
1526
+ current ? " (this project)" : ""
1527
+ ] });
1528
+ }
1529
+ function AddItemsSearch({
1530
+ projectId,
1531
+ translatableTypes,
1532
+ existingDocIds,
1533
+ sourceLanguage,
1534
+ languages,
1535
+ targetLanguages,
1536
+ documentI18nLanguageField,
1537
+ fieldI18nLanguageField
1538
+ }) {
1539
+ const client = sanity.useClient({ apiVersion: constants.API_VERSION }), schema = sanity.useSchema(), toast = ui.useToast(), [query, setQuery] = react.useState(""), [searchContent, setSearchContent] = react.useState(!1), [results, setResults] = react.useState(null), [loading, setLoading] = react.useState(!1), [addingId, setAddingId] = react.useState(null), [addedIds, setAddedIds] = react.useState(/* @__PURE__ */ new Set()), [projectsByDoc, setProjectsByDoc] = react.useState(/* @__PURE__ */ new Map());
1540
+ react.useEffect(() => {
1541
+ const built = buildSearch(query, searchContent, translatableTypes);
1542
+ if (!built) {
1543
+ setResults(null), setLoading(!1);
1544
+ return;
1545
+ }
1546
+ setLoading(!0);
1547
+ let cancelled = !1;
1548
+ const handle = setTimeout(() => {
1549
+ client.fetch(built.query, built.params).then((res) => {
1550
+ cancelled || setResults(dedupeHits(res));
1551
+ }).catch((err) => {
1552
+ cancelled || (setResults([]), toast.push({ status: "error", title: "Search failed", description: String(err) }));
1553
+ }).finally(() => {
1554
+ cancelled || setLoading(!1);
1555
+ });
1556
+ }, 250);
1557
+ return () => {
1558
+ cancelled = !0, clearTimeout(handle);
1559
+ };
1560
+ }, [query, searchContent, translatableTypes, client, toast]);
1561
+ const docIdsKey = (results ?? []).map((r) => r.docId).join(",");
1562
+ react.useEffect(() => {
1563
+ const ids = docIdsKey ? docIdsKey.split(",") : [];
1564
+ if (ids.length === 0) {
1565
+ setProjectsByDoc(/* @__PURE__ */ new Map());
1566
+ return;
1567
+ }
1568
+ let cancelled = !1;
1569
+ const load = () => client.fetch(MEMBERSHIP_QUERY, { ids }).then((rows) => {
1570
+ if (cancelled) return;
1571
+ const map = /* @__PURE__ */ new Map();
1572
+ for (const row of rows)
1573
+ for (const { id } of row.docIds ?? []) {
1574
+ const list = map.get(id) ?? [];
1575
+ list.push({ _id: row._id, name: row.name }), map.set(id, list);
1576
+ }
1577
+ setProjectsByDoc(map);
1578
+ }).catch(() => !cancelled && setProjectsByDoc(/* @__PURE__ */ new Map()));
1579
+ load();
1580
+ const sub = client.listen(MEMBERSHIP_QUERY, { ids }, { visibility: "query" }).subscribe({ next: load, error: () => {
1581
+ } });
1582
+ return () => {
1583
+ cancelled = !0, sub.unsubscribe();
1584
+ };
1585
+ }, [docIdsKey, client]);
1586
+ const existingKey = [...existingDocIds].sort().join(",");
1587
+ react.useEffect(() => {
1588
+ setAddedIds((prev) => {
1589
+ if (prev.size === 0) return prev;
1590
+ const next = new Set([...prev].filter((id) => !existingDocIds.has(id)));
1591
+ return next.size === prev.size ? prev : next;
1592
+ });
1593
+ }, [existingKey]);
1594
+ const addItem = async (hit, sendExistingTranslations) => {
1595
+ setAddingId(hit.docId);
1596
+ try {
1597
+ const item = {
1598
+ _type: "smartcat.projectItem",
1599
+ _key: uuid.uuid(),
1600
+ docId: hit.docId,
1601
+ docType: hit._type,
1602
+ // Prefer the draft as source when one exists (the plugin translates the
1603
+ // draft version); the caller chose whether to also upload existing
1604
+ // translations — matching a fresh "Add to translation project".
1605
+ sourceIsPublished: !hit.hasDraft,
1606
+ sendExistingTranslations
1607
+ };
1608
+ await client.patch(projectId).setIfMissing({ items: [] }).insert("after", "items[-1]", [item]).commit(), setAddedIds((prev) => new Set(prev).add(hit.docId)), toast.push({ status: "success", title: "Added to project" });
1609
+ } catch (err) {
1610
+ toast.push({ status: "error", title: "Failed to add item", description: String(err) });
1611
+ } finally {
1612
+ setAddingId(null);
1613
+ }
1614
+ };
1615
+ return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1616
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, border: !0, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 4, children: [
1617
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 3, children: [
1618
+ /* @__PURE__ */ jsxRuntime.jsx(
1619
+ ui.TextInput,
1620
+ {
1621
+ icon: icons.SearchIcon,
1622
+ placeholder: "Search documents to add \u2014 by title, or schema type\u2026",
1623
+ value: query,
1624
+ onChange: (e) => setQuery(e.currentTarget.value),
1625
+ clearButton: query.length > 0,
1626
+ onClear: () => setQuery("")
1627
+ }
1628
+ ),
1629
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { as: "label", align: "center", gap: 2, style: { cursor: "pointer" }, children: [
1630
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Checkbox, { checked: searchContent, onChange: (e) => setSearchContent(e.currentTarget.checked) }),
1631
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "Also search in content (slower; matches body text, not just titles)" })
1632
+ ] })
1633
+ ] }),
1634
+ loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : results === null ? null : results.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { muted: !0, align: "center", size: 1, children: [
1635
+ "No documents match \u201C",
1636
+ query.trim(),
1637
+ "\u201D."
1638
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 2, children: results.map((hit) => {
1639
+ const inThisProject = addedIds.has(hit.docId) || existingDocIds.has(hit.docId), memberships = projectsByDoc.get(hit.docId) ?? [], previewId = hit.hasDraft ? `drafts.${hit.docId}` : hit.docId;
1640
+ return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 2, radius: 2, shadow: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1641
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { flex: 1, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { space: 2, children: [
1642
+ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 2, children: [
1643
+ /* @__PURE__ */ jsxRuntime.jsx(
1644
+ ui.Badge,
1645
+ {
1646
+ tone: hit.hasDraft ? "caution" : "positive",
1647
+ paddingX: 2,
1648
+ title: hit.hasDraft ? "The draft version will be the translation source" : "The published version will be the translation source",
1649
+ children: hit.hasDraft ? "Draft" : "Published"
1650
+ }
1651
+ ),
1652
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { weight: "medium", textOverflow: "ellipsis", children: /* @__PURE__ */ jsxRuntime.jsx(DocTitle, { doc: { _id: previewId, _type: hit._type } }) }),
1653
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { tone: "primary", paddingX: 2, children: hit._type })
1654
+ ] }),
1655
+ memberships.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { size: 0, muted: !0, children: [
1656
+ "In:",
1657
+ " ",
1658
+ memberships.map((p, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
1659
+ i > 0 && ", ",
1660
+ /* @__PURE__ */ jsxRuntime.jsx(ProjectChip, { project: p, current: p._id === projectId })
1661
+ ] }, p._id))
1662
+ ] })
1663
+ ] }) }),
1664
+ /* @__PURE__ */ jsxRuntime.jsx(OpenInStudioButton, { id: hit.docId, type: hit._type }),
1665
+ /* @__PURE__ */ jsxRuntime.jsx(
1666
+ ViewLocjsonButton,
1667
+ {
1668
+ docId: hit.docId,
1669
+ type: hit._type,
1670
+ sourceIsPublished: !hit.hasDraft,
1671
+ client,
1672
+ schema,
1673
+ sourceLanguage,
1674
+ documentI18nLanguageField,
1675
+ fieldI18nLanguageField,
1676
+ targetLanguages,
1677
+ languages
1678
+ }
1679
+ ),
1680
+ inThisProject ? /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", icon: icons.CheckmarkIcon, text: "Added", fontSize: 1, padding: 2, disabled: !0 }) : /* @__PURE__ */ jsxRuntime.jsx(
1681
+ ui.MenuButton,
1682
+ {
1683
+ id: `add-${hit.docId}`,
1684
+ button: /* @__PURE__ */ jsxRuntime.jsx(
1685
+ ui.Button,
1686
+ {
1687
+ mode: "ghost",
1688
+ icon: icons.AddIcon,
1689
+ iconRight: icons.ChevronDownIcon,
1690
+ text: "Add",
1691
+ fontSize: 1,
1692
+ padding: 2,
1693
+ loading: addingId === hit.docId,
1694
+ disabled: addingId !== null
1695
+ }
1696
+ ),
1697
+ menu: /* @__PURE__ */ jsxRuntime.jsxs(ui.Menu, { children: [
1698
+ /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: "Add source", onClick: () => addItem(hit, !1) }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: "Add source + translations", onClick: () => addItem(hit, !0) })
1700
+ ] }),
1701
+ popover: { portal: !0 }
1702
+ }
1703
+ )
1704
+ ] }) }, hit.docId);
1705
+ }) })
1706
+ ] }) }),
1707
+ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: "You can also add content from a document\u2019s \u201C\u22EF \u2192 Add to translation project\u201D menu." })
1708
+ ] });
1709
+ }
1358
1710
  function logsToText(logs) {
1359
1711
  return logs.map((l) => {
1360
1712
  const prefix = l.indent ? " " : "", detail = [
@@ -1497,14 +1849,6 @@ function languageMappingLabel(language) {
1497
1849
  const code = language.smartcatLanguage || language.id;
1498
1850
  return code === language.id ? language.id : `${language.id} \u2192 ${code}`;
1499
1851
  }
1500
- function DocTitle({ doc }) {
1501
- const schema = sanity.useSchema(), schemaType = doc ? schema.get(doc._type) : void 0, preview = sanity.useValuePreview({
1502
- enabled: !!(doc && schemaType),
1503
- schemaType,
1504
- value: doc ? { _id: doc._id, _type: doc._type } : void 0
1505
- }), title = typeof preview.value?.title == "string" ? preview.value.title : void 0;
1506
- return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: title ?? (preview.isLoading ? "\u2026" : UNTITLED) });
1507
- }
1508
1852
  const IN_FLIGHT = ["queued", "importing", "downloaded"], CAN_IMPORT = ["sent", "translating", "completed"], rotate = styled.keyframes`
1509
1853
  to { transform: rotate(360deg); }
1510
1854
  `, SpinningSyncIcon = styled__default.default(icons.SyncIcon)`
@@ -1519,126 +1863,6 @@ function LastUpdate({ date }) {
1519
1863
  relative
1520
1864
  ] });
1521
1865
  }
1522
- function OpenInStudioButton({ id, type }) {
1523
- const { onClick, href } = router.useIntentLink({ intent: "edit", params: { id, type } });
1524
- return /* @__PURE__ */ jsxRuntime.jsx(
1525
- ui.Button,
1526
- {
1527
- as: "a",
1528
- href,
1529
- onClick,
1530
- mode: "bleed",
1531
- icon: icons.LaunchIcon,
1532
- text: "Open in Studio",
1533
- fontSize: 0,
1534
- padding: 2
1535
- }
1536
- );
1537
- }
1538
- const CodeArea = styled__default.default.textarea`
1539
- width: 100%;
1540
- height: 70vh;
1541
- resize: none;
1542
- border: none;
1543
- outline: none;
1544
- padding: 0;
1545
- background: transparent;
1546
- color: inherit;
1547
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1548
- font-size: 12px;
1549
- line-height: 1.5;
1550
- white-space: pre;
1551
- tab-size: 2;
1552
- `, VIEW_SIBLINGS_QUERY = '*[_type == "translation.metadata" && references($id)][0].translations[]{"language": _key, "doc": value->}';
1553
- function ViewLocjsonButton(props) {
1554
- const { docId, type, sourceIsPublished, client, schema, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, targetLanguages, languages } = props, [open, setOpen] = react.useState(!1), [content, setContent] = react.useState(null), [filename, setFilename] = react.useState(null), [title, setTitle] = react.useState("Source only"), [error, setError] = react.useState(null), build = react.useCallback(
1555
- async (targetLanguage) => {
1556
- setOpen(!0), setContent(null), setFilename(null), setError(null), setTitle(targetLanguage ? `Source + ${languageTitle(languages, targetLanguage)}` : "Source only");
1557
- try {
1558
- const raw = await client.fetch(
1559
- sourceIsPublished ? "*[_id == $id][0]" : 'coalesce(*[_id == "drafts." + $id][0], *[_id == $id][0])',
1560
- { id: docId }
1561
- );
1562
- if (!raw) throw new Error(`Document not found: ${docId}`);
1563
- const isDraft = typeof raw._id == "string" && raw._id.startsWith("drafts."), doc = isDraft ? { ...raw, _id: docId } : raw, fields = locjson.getTranslatableFields(schema.get(type), { exclude: [documentI18nLanguageField] });
1564
- let target;
1565
- if (targetLanguage)
1566
- if (locjson.localizationMode(schema.get(type)) === "document") {
1567
- const sibling = (await client.fetch(VIEW_SIBLINGS_QUERY, { id: docId }) ?? []).find((v) => v.language === targetLanguage)?.doc ?? void 0;
1568
- target = { language: targetLanguage, doc: sibling };
1569
- } else
1570
- target = { language: targetLanguage };
1571
- const locjson$1 = locjson.serializeToLocjson(doc, fields, {
1572
- sourceLanguage,
1573
- fieldLanguageKey: fieldI18nLanguageField,
1574
- target
1575
- }), smartcatCode = targetLanguage ? languages.find((l) => l.id === targetLanguage)?.smartcatLanguage || targetLanguage : void 0, smartcatName = locjson.buildLocjsonFilename(type, docId, resolveDocumentTitle(schema, doc), {
1576
- draft: isDraft,
1577
- language: smartcatCode
1578
- });
1579
- setFilename(smartcatName.split("/").pop() || smartcatName), setContent(JSON.stringify(locjson$1, null, 2));
1580
- } catch (err) {
1581
- setError(err instanceof Error ? err.message : String(err));
1582
- }
1583
- },
1584
- [client, schema, docId, type, sourceIsPublished, sourceLanguage, documentI18nLanguageField, fieldI18nLanguageField, languages]
1585
- ), download = react.useCallback(() => {
1586
- if (content == null || !filename) return;
1587
- const url = URL.createObjectURL(new Blob([content], { type: "application/json" })), anchor = document.createElement("a");
1588
- anchor.href = url, anchor.download = filename, anchor.click(), URL.revokeObjectURL(url);
1589
- }, [content, filename]);
1590
- return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1591
- /* @__PURE__ */ jsxRuntime.jsx(
1592
- ui.MenuButton,
1593
- {
1594
- id: `locjson-menu-${docId}`,
1595
- button: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { mode: "bleed", icon: icons.JsonIcon, iconRight: icons.ChevronDownIcon, title: "View LocJSON", fontSize: 0, padding: 2 }),
1596
- menu: /* @__PURE__ */ jsxRuntime.jsxs(ui.Menu, { children: [
1597
- /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: "Source only", onClick: () => build() }),
1598
- targetLanguages.map((lang) => /* @__PURE__ */ jsxRuntime.jsx(ui.MenuItem, { text: `Source + ${languageTitle(languages, lang)}`, onClick: () => build(lang) }, lang))
1599
- ] }),
1600
- popover: { portal: !0 }
1601
- }
1602
- ),
1603
- open && /* @__PURE__ */ jsxRuntime.jsx(
1604
- ui.Dialog,
1605
- {
1606
- id: `locjson-${docId}`,
1607
- header: /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { align: "center", gap: 3, children: [
1608
- /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { weight: "semibold", children: [
1609
- "LocJSON \xB7 ",
1610
- type,
1611
- " \xB7 ",
1612
- title
1613
- ] }),
1614
- /* @__PURE__ */ jsxRuntime.jsx(
1615
- ui.Button,
1616
- {
1617
- icon: icons.DownloadIcon,
1618
- text: "Download",
1619
- mode: "ghost",
1620
- fontSize: 1,
1621
- padding: 2,
1622
- disabled: content == null,
1623
- onClick: download
1624
- }
1625
- )
1626
- ] }),
1627
- onClose: () => setOpen(!1),
1628
- width: 3,
1629
- children: /* @__PURE__ */ jsxRuntime.jsx(ui.Box, { padding: 4, children: error ? /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, muted: !0, children: error }) : content === null ? /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", justify: "center", padding: 5, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Spinner, { muted: !0 }) }) : /* @__PURE__ */ jsxRuntime.jsx(
1630
- CodeArea,
1631
- {
1632
- readOnly: !0,
1633
- spellCheck: !1,
1634
- value: content,
1635
- onFocus: (e) => e.currentTarget.select()
1636
- }
1637
- ) })
1638
- }
1639
- )
1640
- ] });
1641
- }
1642
1866
  function ProjectView({
1643
1867
  projectId,
1644
1868
  onBack,
@@ -2091,7 +2315,19 @@ Smartcat language code: '${l.smartcatLanguage || l.id}'`,
2091
2315
  item._key
2092
2316
  );
2093
2317
  }) }),
2094
- /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 4, radius: 2, tone: "transparent", border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { muted: !0, align: "center", size: 1, children: "Add content from a document\u2019s \u201C\u22EF \u2192 Add to translation project\u201D menu." }) })
2318
+ /* @__PURE__ */ jsxRuntime.jsx(
2319
+ AddItemsSearch,
2320
+ {
2321
+ projectId,
2322
+ translatableTypes,
2323
+ existingDocIds: new Set(items.map((i) => i.docId)),
2324
+ sourceLanguage,
2325
+ languages,
2326
+ targetLanguages: targets,
2327
+ documentI18nLanguageField,
2328
+ fieldI18nLanguageField
2329
+ }
2330
+ )
2095
2331
  ] }),
2096
2332
  confirmDelete && /* @__PURE__ */ jsxRuntime.jsx(
2097
2333
  ui.Dialog,