@meetreeve/ui 0.8.1 → 0.9.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
@@ -1423,6 +1423,10 @@ var import_jsx_runtime7 = require("react/jsx-runtime");
1423
1423
  var DEFAULT_CONNECT_ERROR = "Couldn't start the connection. Please try again.";
1424
1424
  var DEFAULT_DISCONNECT_ERROR = "Couldn't disconnect. Please try again.";
1425
1425
  var DEFAULT_LOAD_ERROR = "Couldn't load connections. Please try again.";
1426
+ var DEFAULT_GRANTS_LOAD_ERROR = "Couldn't load sharing. Please try again.";
1427
+ var DEFAULT_GRANT_ERROR = "Couldn't share. Please try again.";
1428
+ var DEFAULT_REVOKE_ERROR = "Couldn't revoke access. Please try again.";
1429
+ var DEFAULT_ORG_DISABLED_REASON = "Org-wide connections aren't available to you.";
1426
1430
  var SKELETON_COUNT = 3;
1427
1431
  function capabilityLabel(capability) {
1428
1432
  if (capability === "read") return "Read only";
@@ -1437,6 +1441,17 @@ function formatSince(since) {
1437
1441
  function errorMessageFrom(err, fallback) {
1438
1442
  return err instanceof Error && err.message ? err.message : fallback;
1439
1443
  }
1444
+ function ownerLabel(entry) {
1445
+ const connection = entry.connection;
1446
+ if (connection.status !== "connected") return null;
1447
+ if (connection.kind === "org") return "Org";
1448
+ if (connection.owned_by_viewer) return "Yours";
1449
+ if (connection.shared_by) return `Shared by ${connection.shared_by.name}`;
1450
+ return null;
1451
+ }
1452
+ function isSharedWithViewer(entry) {
1453
+ return entry.connection.status === "connected" && Boolean(entry.connection.shared_by) && !entry.connection.owned_by_viewer;
1454
+ }
1440
1455
  function ConnectorPicker({
1441
1456
  client,
1442
1457
  purpose,
@@ -1448,6 +1463,7 @@ function ConnectorPicker({
1448
1463
  const [entries, setEntries] = (0, import_react9.useState)(null);
1449
1464
  const [loadError, setLoadError] = (0, import_react9.useState)(null);
1450
1465
  const [uiState, setUiState] = (0, import_react9.useState)({});
1466
+ const grantsSupported = typeof client.listGrants === "function" && typeof client.grant === "function" && typeof client.revokeGrant === "function";
1451
1467
  const clientRef = (0, import_react9.useRef)(client);
1452
1468
  clientRef.current = client;
1453
1469
  const purposeRef = (0, import_react9.useRef)(purpose);
@@ -1506,10 +1522,10 @@ function ConnectorPicker({
1506
1522
  };
1507
1523
  }, [gated, refreshAfterReturn]);
1508
1524
  const handleConnect = (0, import_react9.useCallback)(
1509
- async (entry) => {
1525
+ async (entry, kind) => {
1510
1526
  setUiState((prev) => ({ ...prev, [entry.slug]: { kind: "connecting" } }));
1511
1527
  try {
1512
- const result = await clientRef.current.connect(entry);
1528
+ const result = await clientRef.current.connect(entry, { kind });
1513
1529
  if (result?.connect_url) {
1514
1530
  window.open(result.connect_url, "_blank", "noopener,noreferrer");
1515
1531
  return;
@@ -1549,121 +1565,429 @@ function ConnectorPicker({
1549
1565
  },
1550
1566
  [load]
1551
1567
  );
1568
+ const listGrants = (0, import_react9.useCallback)(
1569
+ (entry) => clientRef.current.listGrants?.(entry) ?? Promise.resolve([]),
1570
+ []
1571
+ );
1572
+ const addGrant = (0, import_react9.useCallback)(
1573
+ (entry, input) => clientRef.current.grant?.(entry, input) ?? Promise.resolve(),
1574
+ []
1575
+ );
1576
+ const revokeGrant = (0, import_react9.useCallback)(
1577
+ (entry, grantId) => clientRef.current.revokeGrant?.(entry, grantId) ?? Promise.resolve(),
1578
+ []
1579
+ );
1552
1580
  if (gated) return null;
1553
1581
  const containerClassName = cn(
1554
1582
  "flex flex-col",
1555
1583
  layout === "tiles" ? "gap-3 sm:grid sm:grid-cols-2 lg:grid-cols-3" : "divide-y divide-border"
1556
1584
  );
1557
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-testid": "connector-picker", "data-layout": layout, className: containerClassName, children: [
1558
- entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1559
- "div",
1560
- {
1561
- "data-testid": "connector-picker-skeleton-item",
1562
- "aria-hidden": "true",
1563
- className: "flex items-center gap-3 rounded-lg border border-border p-3",
1564
- children: [
1565
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
1566
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
1567
- ]
1568
- },
1569
- i
1570
- )),
1571
- loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1572
- "div",
1573
- {
1574
- role: "alert",
1575
- "data-testid": "connector-picker-load-error",
1576
- className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
1577
- children: [
1578
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: loadError }),
1579
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
1580
- ]
1581
- }
1582
- ),
1583
- entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
1584
- entries?.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1585
- ConnectorTile,
1586
- {
1587
- entry,
1588
- ui: uiState[entry.slug],
1589
- layout,
1590
- onConnect: handleConnect,
1591
- onDisconnect: handleDisconnect
1592
- },
1593
- entry.slug
1594
- ))
1585
+ const ownEntries = entries?.filter((entry) => !isSharedWithViewer(entry)) ?? null;
1586
+ const sharedEntries = entries?.filter(isSharedWithViewer) ?? [];
1587
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-testid": "connector-picker", "data-layout": layout, className: "flex flex-col gap-4", children: [
1588
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: containerClassName, children: [
1589
+ entries === null && !loadError && Array.from({ length: SKELETON_COUNT }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1590
+ "div",
1591
+ {
1592
+ "data-testid": "connector-picker-skeleton-item",
1593
+ "aria-hidden": "true",
1594
+ className: "flex items-center gap-3 rounded-lg border border-border p-3",
1595
+ children: [
1596
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-8 w-8 shrink-0 animate-pulse rounded bg-foreground/10" }),
1597
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "h-3 flex-1 animate-pulse rounded bg-foreground/10" })
1598
+ ]
1599
+ },
1600
+ i
1601
+ )),
1602
+ loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1603
+ "div",
1604
+ {
1605
+ role: "alert",
1606
+ "data-testid": "connector-picker-load-error",
1607
+ className: "flex items-center justify-between gap-3 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive",
1608
+ children: [
1609
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: loadError }),
1610
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", onClick: () => void load(), className: "shrink-0 underline", children: "Retry" })
1611
+ ]
1612
+ }
1613
+ ),
1614
+ entries !== null && entries.length === 0 && !loadError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { "data-testid": "connector-picker-empty", className: "text-sm text-muted-foreground", children: "No connections available." }),
1615
+ ownEntries?.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1616
+ ConnectorTile,
1617
+ {
1618
+ entry,
1619
+ ui: uiState[entry.slug],
1620
+ layout,
1621
+ grantsSupported,
1622
+ onConnect: handleConnect,
1623
+ onDisconnect: handleDisconnect,
1624
+ listGrants,
1625
+ addGrant,
1626
+ revokeGrant
1627
+ },
1628
+ entry.slug
1629
+ ))
1630
+ ] }),
1631
+ sharedEntries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("section", { "data-testid": "connector-picker-shared-with-me", className: "flex flex-col gap-2", children: [
1632
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h3", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Shared with me" }),
1633
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: containerClassName, children: sharedEntries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1634
+ ConnectorTile,
1635
+ {
1636
+ entry,
1637
+ ui: uiState[entry.slug],
1638
+ layout,
1639
+ shared: true,
1640
+ grantsSupported,
1641
+ onConnect: handleConnect,
1642
+ onDisconnect: handleDisconnect,
1643
+ listGrants,
1644
+ addGrant,
1645
+ revokeGrant
1646
+ },
1647
+ entry.slug
1648
+ )) })
1649
+ ] })
1595
1650
  ] });
1596
1651
  }
1597
1652
  function ConnectorTile({
1598
1653
  entry,
1599
1654
  ui,
1600
1655
  layout,
1656
+ shared = false,
1657
+ grantsSupported,
1601
1658
  onConnect,
1602
- onDisconnect
1659
+ onDisconnect,
1660
+ listGrants,
1661
+ addGrant,
1662
+ revokeGrant
1603
1663
  }) {
1604
1664
  const isConnected = entry.connection.status === "connected";
1605
1665
  const isConnecting = ui?.kind === "connecting";
1606
1666
  const isDisconnecting = ui?.kind === "disconnecting";
1607
1667
  const errorMessage = ui?.kind === "error" ? ui.message : null;
1668
+ const [kind, setKind] = (0, import_react9.useState)("personal");
1669
+ const [sharingOpen, setSharingOpen] = (0, import_react9.useState)(false);
1670
+ const orgPermitted = entry.org_connect?.permitted === true;
1671
+ const orgDisabledReason = entry.org_connect?.reason || DEFAULT_ORG_DISABLED_REASON;
1672
+ const attribution = ownerLabel(entry);
1673
+ const canDisconnect = isConnected && !shared;
1674
+ const canManageSharing = isConnected && entry.connection.owned_by_viewer === true && grantsSupported;
1608
1675
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1609
1676
  "div",
1610
1677
  {
1611
1678
  "data-testid": `connector-tile-${entry.slug}`,
1612
1679
  "data-connected": isConnected || void 0,
1613
1680
  "data-deprecated": entry.deprecated || void 0,
1614
- className: cn("flex items-center gap-3 p-3", layout === "tiles" && "rounded-lg border border-border"),
1681
+ className: cn("flex flex-col p-3", layout === "tiles" && "rounded-lg border border-border"),
1615
1682
  children: [
1616
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
1617
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1618
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
1619
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "truncate text-sm font-medium", children: entry.name }),
1620
- entry.deprecated && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1621
- "span",
1683
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-3", children: [
1684
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(BrandGlyph, { brand: { name: entry.name, logo: entry.logo }, size: "lg" }),
1685
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "min-w-0 flex-1", children: [
1686
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-2", children: [
1687
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "truncate text-sm font-medium", children: entry.name }),
1688
+ entry.deprecated && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1689
+ "span",
1690
+ {
1691
+ "data-testid": `connector-deprecated-${entry.slug}`,
1692
+ className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
1693
+ children: "No longer available"
1694
+ }
1695
+ )
1696
+ ] }),
1697
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1698
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1699
+ entry.category && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1700
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: entry.category })
1702
+ ] })
1703
+ ] }),
1704
+ isConnected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1705
+ "p",
1622
1706
  {
1623
- "data-testid": `connector-deprecated-${entry.slug}`,
1624
- className: "shrink-0 rounded-full bg-foreground/10 px-2 py-0.5 text-[11px] text-muted-foreground",
1625
- children: "No longer available"
1707
+ "data-testid": `connector-connected-since-${entry.slug}`,
1708
+ className: "text-xs text-muted-foreground",
1709
+ children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1626
1710
  }
1627
- )
1628
- ] }),
1629
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [
1630
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "data-testid": `connector-capability-${entry.slug}`, children: capabilityLabel(entry.capability) }),
1631
- entry.category && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1632
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
1633
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "truncate", children: entry.category })
1634
- ] })
1711
+ ),
1712
+ attribution && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1713
+ "p",
1714
+ {
1715
+ "data-testid": `connector-owner-${entry.slug}`,
1716
+ className: "text-xs text-muted-foreground",
1717
+ children: attribution
1718
+ }
1719
+ ),
1720
+ !isConnected && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1721
+ "div",
1722
+ {
1723
+ "data-testid": `connector-kind-toggle-${entry.slug}`,
1724
+ role: "group",
1725
+ "aria-label": "Connect as",
1726
+ className: "mt-1 flex items-center gap-1",
1727
+ children: [
1728
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1729
+ "button",
1730
+ {
1731
+ type: "button",
1732
+ "data-testid": `connector-kind-personal-${entry.slug}`,
1733
+ "aria-pressed": kind === "personal",
1734
+ disabled: isConnecting,
1735
+ onClick: () => setKind("personal"),
1736
+ className: cn(
1737
+ "rounded-full border px-2 py-0.5 text-[11px]",
1738
+ kind === "personal" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1739
+ "disabled:opacity-50"
1740
+ ),
1741
+ children: "Personal"
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1745
+ "button",
1746
+ {
1747
+ type: "button",
1748
+ "data-testid": `connector-kind-org-${entry.slug}`,
1749
+ "aria-pressed": kind === "org",
1750
+ disabled: isConnecting || !orgPermitted,
1751
+ title: orgPermitted ? void 0 : orgDisabledReason,
1752
+ onClick: () => setKind("org"),
1753
+ className: cn(
1754
+ "rounded-full border px-2 py-0.5 text-[11px]",
1755
+ kind === "org" ? "border-primary bg-primary/10 font-medium" : "border-border text-muted-foreground",
1756
+ "disabled:opacity-50"
1757
+ ),
1758
+ children: [
1759
+ "Org",
1760
+ !orgPermitted && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "sr-only", children: [
1761
+ " (",
1762
+ orgDisabledReason,
1763
+ ")"
1764
+ ] })
1765
+ ]
1766
+ }
1767
+ )
1768
+ ]
1769
+ }
1770
+ ),
1771
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1635
1772
  ] }),
1636
- isConnected && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1637
- "p",
1773
+ isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex shrink-0 flex-col items-end gap-1", children: [
1774
+ canDisconnect && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1775
+ "button",
1776
+ {
1777
+ type: "button",
1778
+ "data-testid": `connector-disconnect-${entry.slug}`,
1779
+ onClick: () => onDisconnect(entry),
1780
+ disabled: isDisconnecting,
1781
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1782
+ children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1783
+ }
1784
+ ),
1785
+ canManageSharing && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1786
+ "button",
1787
+ {
1788
+ type: "button",
1789
+ "data-testid": `connector-manage-sharing-${entry.slug}`,
1790
+ "aria-expanded": sharingOpen,
1791
+ onClick: () => setSharingOpen((open) => !open),
1792
+ className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-muted-foreground hover:bg-foreground/10",
1793
+ children: sharingOpen ? "Hide sharing" : "Manage sharing"
1794
+ }
1795
+ )
1796
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1797
+ "button",
1638
1798
  {
1639
- "data-testid": `connector-connected-since-${entry.slug}`,
1640
- className: "text-xs text-muted-foreground",
1641
- children: entry.connection.since ? `Connected since ${formatSince(entry.connection.since)}` : "Connected"
1799
+ type: "button",
1800
+ "data-testid": `connector-connect-${entry.slug}`,
1801
+ onClick: () => onConnect(entry, kind),
1802
+ disabled: isConnecting,
1803
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1804
+ children: isConnecting ? "Connecting\u2026" : "Connect"
1642
1805
  }
1643
- ),
1644
- errorMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { role: "alert", "data-testid": `connector-error-${entry.slug}`, className: "text-xs text-destructive", children: errorMessage })
1806
+ )
1645
1807
  ] }),
1646
- isConnected ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1647
- "button",
1808
+ canManageSharing && sharingOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1809
+ ManageSharing,
1648
1810
  {
1649
- type: "button",
1650
- "data-testid": `connector-disconnect-${entry.slug}`,
1651
- onClick: () => onDisconnect(entry),
1652
- disabled: isDisconnecting,
1653
- className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1654
- children: isDisconnecting ? "Disconnecting\u2026" : "Disconnect"
1811
+ entry,
1812
+ listGrants,
1813
+ addGrant,
1814
+ revokeGrant
1655
1815
  }
1656
- ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1657
- "button",
1816
+ )
1817
+ ]
1818
+ }
1819
+ );
1820
+ }
1821
+ function ManageSharing({
1822
+ entry,
1823
+ listGrants,
1824
+ addGrant,
1825
+ revokeGrant
1826
+ }) {
1827
+ const [grants, setGrants] = (0, import_react9.useState)(null);
1828
+ const [error, setError] = (0, import_react9.useState)(null);
1829
+ const [busy, setBusy] = (0, import_react9.useState)(false);
1830
+ const [granteeType, setGranteeType] = (0, import_react9.useState)("user");
1831
+ const [granteeId, setGranteeId] = (0, import_react9.useState)("");
1832
+ const roleOptions = entry.org_connect?.role_options ?? [];
1833
+ const entryRef = (0, import_react9.useRef)(entry);
1834
+ entryRef.current = entry;
1835
+ const load = (0, import_react9.useCallback)(async () => {
1836
+ try {
1837
+ setGrants(await listGrants(entryRef.current));
1838
+ setError(null);
1839
+ } catch (err) {
1840
+ setError(errorMessageFrom(err, DEFAULT_GRANTS_LOAD_ERROR));
1841
+ }
1842
+ }, [listGrants]);
1843
+ (0, import_react9.useEffect)(() => {
1844
+ void load();
1845
+ }, [load]);
1846
+ const handleAdd = (0, import_react9.useCallback)(async () => {
1847
+ const id = granteeId.trim();
1848
+ if (!id) return;
1849
+ setBusy(true);
1850
+ try {
1851
+ await addGrant(entryRef.current, { granteeType, granteeId: id });
1852
+ setGranteeId("");
1853
+ await load();
1854
+ } catch (err) {
1855
+ setError(errorMessageFrom(err, DEFAULT_GRANT_ERROR));
1856
+ } finally {
1857
+ setBusy(false);
1858
+ }
1859
+ }, [addGrant, granteeId, granteeType, load]);
1860
+ const handleRevoke = (0, import_react9.useCallback)(
1861
+ async (grantId) => {
1862
+ setBusy(true);
1863
+ try {
1864
+ await revokeGrant(entryRef.current, grantId);
1865
+ await load();
1866
+ } catch (err) {
1867
+ setError(errorMessageFrom(err, DEFAULT_REVOKE_ERROR));
1868
+ } finally {
1869
+ setBusy(false);
1870
+ }
1871
+ },
1872
+ [load, revokeGrant]
1873
+ );
1874
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1875
+ "div",
1876
+ {
1877
+ "data-testid": `connector-sharing-panel-${entry.slug}`,
1878
+ className: "mt-2 flex flex-col gap-2 rounded-md border border-border bg-foreground/5 p-2",
1879
+ children: [
1880
+ grants === null && !error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1881
+ "p",
1658
1882
  {
1659
- type: "button",
1660
- "data-testid": `connector-connect-${entry.slug}`,
1661
- onClick: () => onConnect(entry),
1662
- disabled: isConnecting,
1663
- className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1664
- children: isConnecting ? "Connecting\u2026" : "Connect"
1883
+ "data-testid": `connector-grants-loading-${entry.slug}`,
1884
+ className: "text-xs text-muted-foreground",
1885
+ children: "Loading sharing\u2026"
1665
1886
  }
1666
- )
1887
+ ),
1888
+ error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1889
+ "p",
1890
+ {
1891
+ role: "alert",
1892
+ "data-testid": `connector-sharing-error-${entry.slug}`,
1893
+ className: "text-xs text-destructive",
1894
+ children: error
1895
+ }
1896
+ ),
1897
+ grants !== null && grants.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1898
+ "p",
1899
+ {
1900
+ "data-testid": `connector-grants-empty-${entry.slug}`,
1901
+ className: "text-xs text-muted-foreground",
1902
+ children: "Not shared with anyone yet."
1903
+ }
1904
+ ),
1905
+ grants?.map((grant) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1906
+ "div",
1907
+ {
1908
+ "data-testid": `connector-grant-${entry.slug}-${grant.id}`,
1909
+ className: "flex items-center justify-between gap-2 text-xs",
1910
+ children: [
1911
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "truncate", children: [
1912
+ grant.grantee_name || grant.grantee_id,
1913
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "text-muted-foreground", children: [
1914
+ " ",
1915
+ "\xB7 ",
1916
+ grant.grantee_type === "role" ? "Role" : "User"
1917
+ ] })
1918
+ ] }),
1919
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1920
+ "button",
1921
+ {
1922
+ type: "button",
1923
+ "data-testid": `connector-grant-revoke-${entry.slug}-${grant.id}`,
1924
+ onClick: () => void handleRevoke(grant.id),
1925
+ disabled: busy,
1926
+ className: "shrink-0 rounded-md px-2 py-0.5 font-medium text-destructive hover:bg-destructive/10 disabled:opacity-50",
1927
+ children: "Revoke"
1928
+ }
1929
+ )
1930
+ ]
1931
+ },
1932
+ grant.id
1933
+ )),
1934
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex items-center gap-1.5", children: [
1935
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1936
+ "select",
1937
+ {
1938
+ "data-testid": `connector-grant-type-${entry.slug}`,
1939
+ "aria-label": "Grantee type",
1940
+ value: granteeType,
1941
+ onChange: (event) => {
1942
+ setGranteeType(event.target.value);
1943
+ setGranteeId("");
1944
+ },
1945
+ disabled: busy,
1946
+ className: "rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
1947
+ children: [
1948
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "user", children: "User" }),
1949
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "role", children: "Role" })
1950
+ ]
1951
+ }
1952
+ ),
1953
+ granteeType === "user" ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1954
+ "input",
1955
+ {
1956
+ "data-testid": `connector-grant-grantee-${entry.slug}`,
1957
+ "aria-label": "User ID",
1958
+ value: granteeId,
1959
+ onChange: (event) => setGranteeId(event.target.value),
1960
+ placeholder: "User ID",
1961
+ disabled: busy,
1962
+ className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50"
1963
+ }
1964
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1965
+ "select",
1966
+ {
1967
+ "data-testid": `connector-grant-role-${entry.slug}`,
1968
+ "aria-label": "Role",
1969
+ value: granteeId,
1970
+ onChange: (event) => setGranteeId(event.target.value),
1971
+ disabled: busy,
1972
+ className: "min-w-0 flex-1 rounded-md border border-border bg-transparent px-1.5 py-1 text-xs disabled:opacity-50",
1973
+ children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: "Select a role\u2026" }),
1975
+ roleOptions.map((role) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: role.id, children: role.name }, role.id))
1976
+ ]
1977
+ }
1978
+ ),
1979
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1980
+ "button",
1981
+ {
1982
+ type: "button",
1983
+ "data-testid": `connector-grant-add-${entry.slug}`,
1984
+ onClick: () => void handleAdd(),
1985
+ disabled: busy || !granteeId.trim(),
1986
+ className: "shrink-0 rounded-md bg-primary px-2.5 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50",
1987
+ children: "Share"
1988
+ }
1989
+ )
1990
+ ] })
1667
1991
  ]
1668
1992
  }
1669
1993
  );