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