@fonderie/react-admin-screens 1.3.1 → 1.5.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.d.cts CHANGED
@@ -18,8 +18,9 @@ interface IAdminShellProps {
18
18
  footer?: ReactNode;
19
19
  operators?: boolean;
20
20
  currentOperator?: string;
21
+ publicConfigUrl?: string;
21
22
  }
22
- declare function AdminShell({ client, configClient, courierClient, authClient, billingClient, auditClient, environment, page, onNavigate, appName, envLabel, footer, operators, currentOperator, }: IAdminShellProps): react.JSX.Element;
23
+ declare function AdminShell({ client, configClient, courierClient, authClient, billingClient, auditClient, environment, page, onNavigate, appName, envLabel, footer, operators, currentOperator, publicConfigUrl, }: IAdminShellProps): react.JSX.Element;
23
24
 
24
25
  interface IAttentionScreenProps {
25
26
  client: AdminClient;
package/dist/index.d.ts CHANGED
@@ -18,8 +18,9 @@ interface IAdminShellProps {
18
18
  footer?: ReactNode;
19
19
  operators?: boolean;
20
20
  currentOperator?: string;
21
+ publicConfigUrl?: string;
21
22
  }
22
- declare function AdminShell({ client, configClient, courierClient, authClient, billingClient, auditClient, environment, page, onNavigate, appName, envLabel, footer, operators, currentOperator, }: IAdminShellProps): react.JSX.Element;
23
+ declare function AdminShell({ client, configClient, courierClient, authClient, billingClient, auditClient, environment, page, onNavigate, appName, envLabel, footer, operators, currentOperator, publicConfigUrl, }: IAdminShellProps): react.JSX.Element;
23
24
 
24
25
  interface IAttentionScreenProps {
25
26
  client: AdminClient;
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  // src/screens/AdminShell.tsx
2
2
  import { ConfigEditorScreen, ConfigListScreen } from "@fonderie/react-config-admin-screens";
3
- import { TemplateEditorScreen, TemplateListScreen } from "@fonderie/react-courier-admin-screens";
3
+ import {
4
+ TemplateCreateScreen,
5
+ TemplateEditorScreen,
6
+ TemplateListScreen
7
+ } from "@fonderie/react-courier-admin-screens";
4
8
  import { useEffect as useEffect2, useState as useState9 } from "react";
5
9
 
6
10
  // src/styles.ts
@@ -1554,7 +1558,9 @@ import { useAdminSubscriber } from "@fonderie/react-admin";
1554
1558
  import { useState as useState5 } from "react";
1555
1559
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1556
1560
  function SubscriberBilling({ client, subscriber }) {
1557
- const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20 });
1561
+ const [currency, setCurrency] = useState5("");
1562
+ const [currencyDraft, setCurrencyDraft] = useState5("");
1563
+ const { subscription, wallet, ledger, hasMoreLedger, isLoading, error, loadMoreLedger, grant } = useAdminSubscriber(client, subscriber, { limit: 20, ...currency ? { currency } : {} });
1558
1564
  const [amount, setAmount] = useState5("");
1559
1565
  const [note, setNote] = useState5("");
1560
1566
  const [granted, setGranted] = useState5(null);
@@ -1595,7 +1601,42 @@ function SubscriberBilling({ client, subscriber }) {
1595
1601
  subscription?.providerSubscriptionId ? /* @__PURE__ */ jsx11("div", { style: { ...styles.statHint, fontFamily: "var(--fonderie-mono,monospace)" }, children: subscription.providerSubscriptionId }) : null
1596
1602
  ] }),
1597
1603
  /* @__PURE__ */ jsxs11("div", { style: styles.card, children: [
1598
- /* @__PURE__ */ jsx11("div", { style: styles.statLabel, children: "Credits" }),
1604
+ /* @__PURE__ */ jsxs11(
1605
+ "div",
1606
+ {
1607
+ style: {
1608
+ display: "flex",
1609
+ justifyContent: "space-between",
1610
+ alignItems: "center",
1611
+ gap: 8
1612
+ },
1613
+ children: [
1614
+ /* @__PURE__ */ jsx11("div", { style: styles.statLabel, children: "Credits" }),
1615
+ /* @__PURE__ */ jsx11(
1616
+ "form",
1617
+ {
1618
+ style: { display: "flex", gap: 4 },
1619
+ onSubmit: (e) => {
1620
+ e.preventDefault();
1621
+ setCurrency(currencyDraft.trim().toUpperCase());
1622
+ },
1623
+ children: /* @__PURE__ */ jsx11(
1624
+ "input",
1625
+ {
1626
+ value: currencyDraft,
1627
+ onChange: (e) => setCurrencyDraft(e.target.value),
1628
+ placeholder: wallet?.currency ?? "currency",
1629
+ maxLength: 8,
1630
+ style: { ...styles.input, height: 26, width: 84, fontSize: 12 },
1631
+ "aria-label": "Wallet currency",
1632
+ title: "Show the wallet in another currency"
1633
+ }
1634
+ )
1635
+ }
1636
+ )
1637
+ ]
1638
+ }
1639
+ ),
1599
1640
  wallet ? /* @__PURE__ */ jsxs11(Fragment6, { children: [
1600
1641
  /* @__PURE__ */ jsxs11("div", { style: styles.statValue, children: [
1601
1642
  wallet.balance,
@@ -1717,7 +1758,11 @@ function UsersScreen({
1717
1758
  openUserId ? { id: openUserId } : null
1718
1759
  );
1719
1760
  const plans = useSubscriptionIndex(billingClient);
1720
- const list = useAdminUsers(client, { limit: pageSize });
1761
+ const [showDeleted, setShowDeleted] = useState6(false);
1762
+ const list = useAdminUsers(client, {
1763
+ limit: pageSize,
1764
+ ...showDeleted ? { deleted: true } : {}
1765
+ });
1721
1766
  const { user, isLoading, error, suspend, unsuspend, revokeSessions } = useAdminUser(
1722
1767
  client,
1723
1768
  selected ?? {}
@@ -1775,7 +1820,35 @@ function UsersScreen({
1775
1820
  disabled: list.isLoading,
1776
1821
  children: "Refresh"
1777
1822
  }
1778
- )
1823
+ ),
1824
+ !selected ? /* @__PURE__ */ jsx12(
1825
+ "fieldset",
1826
+ {
1827
+ style: {
1828
+ display: "flex",
1829
+ gap: 6,
1830
+ border: "none",
1831
+ margin: "0 0 0 auto",
1832
+ padding: 0,
1833
+ minWidth: 0
1834
+ },
1835
+ "aria-label": "Which accounts",
1836
+ children: [false, true].map((d) => /* @__PURE__ */ jsx12(
1837
+ "button",
1838
+ {
1839
+ type: "button",
1840
+ "aria-pressed": showDeleted === d,
1841
+ onClick: () => setShowDeleted(d),
1842
+ style: {
1843
+ ...showDeleted === d ? styles.buttonPrimary : styles.button,
1844
+ height: 28
1845
+ },
1846
+ children: d ? "Deleted" : "Active"
1847
+ },
1848
+ String(d)
1849
+ ))
1850
+ }
1851
+ ) : null
1779
1852
  ]
1780
1853
  }
1781
1854
  ),
@@ -1889,7 +1962,14 @@ function UsersScreen({
1889
1962
  /* @__PURE__ */ jsx12("td", { style: styles.td, children: new Date(user.createdAt).toLocaleString() })
1890
1963
  ] })
1891
1964
  ] }) }),
1892
- /* @__PURE__ */ jsxs12("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1965
+ user.deletedAt ? /* @__PURE__ */ jsxs12("div", { style: { ...styles.notice, marginTop: 12 }, role: "status", children: [
1966
+ /* @__PURE__ */ jsxs12("strong", { children: [
1967
+ "Deleted on ",
1968
+ new Date(user.deletedAt).toLocaleString(),
1969
+ "."
1970
+ ] }),
1971
+ " The account cannot sign in, and it is erased when the retention purge runs. Its billing records stay."
1972
+ ] }) : /* @__PURE__ */ jsxs12("div", { style: { ...styles.toolbar, marginTop: 12 }, children: [
1893
1973
  user.suspended ? /* @__PURE__ */ jsx12(
1894
1974
  "button",
1895
1975
  {
@@ -1930,46 +2010,48 @@ function UsersScreen({
1930
2010
  }
1931
2011
  )
1932
2012
  ] }) : null,
1933
- /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Live sessions" }),
1934
- sessions.sessions.length === 0 ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
1935
- /* @__PURE__ */ jsx12("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1936
- " ",
1937
- describeLocation(s.location) ? /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
1938
- describeLocation(s.location),
1939
- " "
1940
- ] }) : null,
1941
- /* @__PURE__ */ jsx12("span", { style: styles.muted, children: s.userAgent ?? "" }),
1942
- " ",
1943
- /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
1944
- "since ",
1945
- new Date(s.createdAt).toLocaleString()
1946
- ] })
1947
- ] }, s.id)) }),
1948
- /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
1949
- history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
1950
- /* @__PURE__ */ jsx12(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
1951
- " ",
1952
- /* @__PURE__ */ jsx12("span", { style: styles.muted, children: e.method }),
1953
- " ",
1954
- /* @__PURE__ */ jsx12("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
1955
- " ",
1956
- describeLocation(e.location) ? /* @__PURE__ */ jsx12("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
1957
- e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
2013
+ !user.deletedAt ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
2014
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Live sessions" }),
2015
+ sessions.sessions.length === 0 ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: sessions.sessions.map((s) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
2016
+ /* @__PURE__ */ jsx12("span", { style: styles.mono, children: s.ipAddress ?? "\u2014" }),
1958
2017
  " ",
1959
- /* @__PURE__ */ jsx12(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
1960
- ] }) : null,
1961
- " ",
1962
- /* @__PURE__ */ jsx12("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
1963
- ] }, e.id)) }),
1964
- history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx12(
1965
- "button",
1966
- {
1967
- type: "button",
1968
- style: { ...styles.button, marginTop: 8 },
1969
- onClick: () => void history.loadMore(),
1970
- children: "Load more"
1971
- }
1972
- ) : null
2018
+ describeLocation(s.location) ? /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
2019
+ describeLocation(s.location),
2020
+ " "
2021
+ ] }) : null,
2022
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: s.userAgent ?? "" }),
2023
+ " ",
2024
+ /* @__PURE__ */ jsxs12("span", { style: styles.muted, children: [
2025
+ "since ",
2026
+ new Date(s.createdAt).toLocaleString()
2027
+ ] })
2028
+ ] }, s.id)) }),
2029
+ /* @__PURE__ */ jsx12("h2", { style: styles.subtitle, children: "Recent sign-ins" }),
2030
+ history.events.length === 0 && !history.isLoading ? /* @__PURE__ */ jsx12("p", { style: styles.muted, children: "None recorded." }) : /* @__PURE__ */ jsx12("ul", { style: styles.list, children: history.events.map((e) => /* @__PURE__ */ jsxs12("li", { style: styles.row, children: [
2031
+ /* @__PURE__ */ jsx12(Pill, { tone: e.outcome === "success" ? "ok" : "bad", children: e.outcome }),
2032
+ " ",
2033
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: e.method }),
2034
+ " ",
2035
+ /* @__PURE__ */ jsx12("span", { style: styles.mono, children: e.ipAddress ?? "\u2014" }),
2036
+ " ",
2037
+ describeLocation(e.location) ? /* @__PURE__ */ jsx12("span", { style: styles.muted, children: describeLocation(e.location) }) : null,
2038
+ e.location?.proxy || e.location?.hosting ? /* @__PURE__ */ jsxs12(Fragment7, { children: [
2039
+ " ",
2040
+ /* @__PURE__ */ jsx12(Pill, { tone: "warn", children: e.location.proxy ? "proxy/VPN" : "hosting" })
2041
+ ] }) : null,
2042
+ " ",
2043
+ /* @__PURE__ */ jsx12("span", { style: styles.muted, children: new Date(e.createdAt).toLocaleString() })
2044
+ ] }, e.id)) }),
2045
+ history.hasMore && !history.isLoading ? /* @__PURE__ */ jsx12(
2046
+ "button",
2047
+ {
2048
+ type: "button",
2049
+ style: { ...styles.button, marginTop: 8 },
2050
+ onClick: () => void history.loadMore(),
2051
+ children: "Load more"
2052
+ }
2053
+ ) : null
2054
+ ] }) : null
1973
2055
  ] }) : null
1974
2056
  ] });
1975
2057
  }
@@ -2242,10 +2324,8 @@ import { useAdminAudit } from "@fonderie/react-admin";
2242
2324
  import { useState as useState8 } from "react";
2243
2325
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
2244
2326
  function AuditScreen({ client, pageSize = 50 }) {
2245
- const [draft, setDraft] = useState8({ workspaceId: "", type: "", actorId: "" });
2246
- const [filter, setFilter] = useState8(
2247
- {}
2248
- );
2327
+ const [draft, setDraft] = useState8({ workspaceId: "", type: "", actorId: "", from: "", to: "" });
2328
+ const [filter, setFilter] = useState8({});
2249
2329
  const { events, hasMore, isLoading, error, refresh, loadMore } = useAdminAudit(client, {
2250
2330
  ...filter,
2251
2331
  limit: pageSize
@@ -2277,13 +2357,38 @@ function AuditScreen({ client, pageSize = 50 }) {
2277
2357
  setFilter({
2278
2358
  ...draft.workspaceId.trim() ? { workspaceId: draft.workspaceId.trim() } : {},
2279
2359
  ...draft.type.trim() ? { type: draft.type.trim() } : {},
2280
- ...draft.actorId.trim() ? { actorId: draft.actorId.trim() } : {}
2360
+ ...draft.actorId.trim() ? { actorId: draft.actorId.trim() } : {},
2361
+ // Dates are whole days in the operator's time zone: "to" includes that day.
2362
+ ...draft.from ? { from: /* @__PURE__ */ new Date(`${draft.from}T00:00:00`) } : {},
2363
+ ...draft.to ? { to: /* @__PURE__ */ new Date(`${draft.to}T23:59:59.999`) } : {}
2281
2364
  });
2282
2365
  },
2283
2366
  children: [
2284
2367
  field("workspaceId", "workspace id (all if empty)"),
2285
2368
  field("type", "event type"),
2286
2369
  field("actorId", "actor id"),
2370
+ /* @__PURE__ */ jsx15(
2371
+ "input",
2372
+ {
2373
+ type: "date",
2374
+ value: draft.from,
2375
+ onChange: (e) => setDraft({ ...draft, from: e.target.value }),
2376
+ style: styles.input,
2377
+ "aria-label": "From date",
2378
+ title: "From (inclusive)"
2379
+ }
2380
+ ),
2381
+ /* @__PURE__ */ jsx15(
2382
+ "input",
2383
+ {
2384
+ type: "date",
2385
+ value: draft.to,
2386
+ onChange: (e) => setDraft({ ...draft, to: e.target.value }),
2387
+ style: styles.input,
2388
+ "aria-label": "To date",
2389
+ title: "To (inclusive)"
2390
+ }
2391
+ ),
2287
2392
  /* @__PURE__ */ jsxs15("button", { type: "submit", style: styles.buttonPrimary, disabled: isLoading, children: [
2288
2393
  /* @__PURE__ */ jsx15(Icon, { name: "search", size: 14 }),
2289
2394
  "Filter"
@@ -2408,7 +2513,8 @@ function AdminShell({
2408
2513
  envLabel,
2409
2514
  footer,
2410
2515
  operators = false,
2411
- currentOperator
2516
+ currentOperator,
2517
+ publicConfigUrl
2412
2518
  }) {
2413
2519
  const narrow = useNarrow();
2414
2520
  const [drawer, setDrawer] = useState9(false);
@@ -2490,7 +2596,7 @@ function AdminShell({
2490
2596
  ) : /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass an AuthAdminClient to look up users here." });
2491
2597
  break;
2492
2598
  case "settings":
2493
- body = !configClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && editing.kind !== "template" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2599
+ body = !configClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a ConfigAdminClient to manage config and secrets here." }) : editing && (editing.kind === "config" || editing.kind === "secret") ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2494
2600
  /* @__PURE__ */ jsx16("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs16("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2495
2601
  /* @__PURE__ */ jsx16(Icon, { name: "back", size: 14 }),
2496
2602
  "Config & secrets"
@@ -2501,24 +2607,52 @@ function AdminShell({
2501
2607
  client: configClient,
2502
2608
  kind: editing.kind,
2503
2609
  configKey: editing.key,
2504
- ...environment !== void 0 ? { environment } : {},
2505
- onSaved: () => setEditing(null)
2506
- }
2610
+ ...editing.environment !== void 0 ? { environment: editing.environment } : environment !== void 0 ? { environment } : {},
2611
+ ...editing.environments ? { environments: editing.environments } : {},
2612
+ onSaved: () => setEditing(null),
2613
+ onDeleted: () => setEditing(null)
2614
+ },
2615
+ `${editing.kind}:${editing.key}:${editing.environment ?? ""}`
2507
2616
  )
2508
2617
  ] }) : /* @__PURE__ */ jsx16(
2509
2618
  ConfigListScreen,
2510
2619
  {
2511
2620
  client: configClient,
2512
2621
  ...environment !== void 0 ? { environment } : {},
2513
- onSelectConfig: (key) => setEditing({ kind: "config", key }),
2514
- onSelectSecret: (key) => setEditing({ kind: "secret", key }),
2515
- onCreateConfig: () => setEditing({ kind: "config", key: "" }),
2516
- onCreateSecret: () => setEditing({ kind: "secret", key: "" })
2622
+ ...publicConfigUrl ? { publicConfigUrl } : {},
2623
+ onSelectConfig: (key, env) => setEditing({ kind: "config", key, environment: env }),
2624
+ onSelectSecret: (key, env) => setEditing({ kind: "secret", key, environment: env }),
2625
+ onCreateConfig: (c) => setEditing({
2626
+ kind: "config",
2627
+ key: "",
2628
+ environments: c.environments,
2629
+ ...c.environment ? { environment: c.environment } : {}
2630
+ }),
2631
+ onCreateSecret: (c) => setEditing({
2632
+ kind: "secret",
2633
+ key: "",
2634
+ environments: c.environments,
2635
+ ...c.environment ? { environment: c.environment } : {}
2636
+ })
2517
2637
  }
2518
2638
  );
2519
2639
  break;
2520
2640
  case "templates":
2521
- body = !courierClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2641
+ body = !courierClient ? /* @__PURE__ */ jsx16("p", { style: { ...styles.status, padding: "32px 40px" }, children: "Pass a CourierAdminClient to manage templates here." }) : editing?.kind === "template-new" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2642
+ /* @__PURE__ */ jsx16("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs16("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2643
+ /* @__PURE__ */ jsx16(Icon, { name: "back", size: 14 }),
2644
+ "Templates"
2645
+ ] }) }),
2646
+ /* @__PURE__ */ jsx16(
2647
+ TemplateCreateScreen,
2648
+ {
2649
+ client: courierClient,
2650
+ ...editing.type ? { type: editing.type } : {},
2651
+ ...editing.locales ? { locales: editing.locales } : {},
2652
+ onCreated: (c) => setEditing({ kind: "template", type: c.type, locale: c.locale, system: false })
2653
+ }
2654
+ )
2655
+ ] }) : editing?.kind === "template" ? /* @__PURE__ */ jsxs16(Fragment10, { children: [
2522
2656
  /* @__PURE__ */ jsx16("div", { style: { padding: "24px 40px 0" }, children: /* @__PURE__ */ jsxs16("button", { type: "button", style: styles.buttonGhost, onClick: () => setEditing(null), children: [
2523
2657
  /* @__PURE__ */ jsx16(Icon, { name: "back", size: 14 }),
2524
2658
  "Templates"
@@ -2528,14 +2662,25 @@ function AdminShell({
2528
2662
  {
2529
2663
  client: courierClient,
2530
2664
  type: editing.type,
2531
- onSaved: () => setEditing(null)
2532
- }
2665
+ locale: editing.locale,
2666
+ system: editing.system,
2667
+ onSaved: () => setEditing(null),
2668
+ onDeleted: () => setEditing(null),
2669
+ onAddLocale: (type) => setEditing({ kind: "template-new", type })
2670
+ },
2671
+ `${editing.type}:${editing.locale ?? ""}`
2533
2672
  )
2534
2673
  ] }) : /* @__PURE__ */ jsx16(
2535
2674
  TemplateListScreen,
2536
2675
  {
2537
2676
  client: courierClient,
2538
- onSelectTemplate: (t2) => setEditing({ kind: "template", type: t2.type })
2677
+ onSelectTemplate: (t2) => setEditing({
2678
+ kind: "template",
2679
+ type: t2.type,
2680
+ locale: t2.locale ?? null,
2681
+ system: t2.system === true
2682
+ }),
2683
+ onCreateTemplate: (c) => setEditing({ kind: "template-new", locales: c.locales })
2539
2684
  }
2540
2685
  );
2541
2686
  break;