@multiplatform.one/frappe-ui 7.8.0 → 7.11.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.
Files changed (84) hide show
  1. package/dist/cjs/FormSidebar.cjs +7 -7
  2. package/dist/cjs/FormSidebar.native.js +7 -7
  3. package/dist/cjs/FormSidebar.native.js.map +1 -1
  4. package/dist/cjs/FrappeChartCard.cjs +29 -3
  5. package/dist/cjs/FrappeChartCard.native.js +29 -3
  6. package/dist/cjs/FrappeChartCard.native.js.map +1 -1
  7. package/dist/cjs/FrappeKPICard.cjs +6 -5
  8. package/dist/cjs/FrappeKPICard.native.js +6 -5
  9. package/dist/cjs/FrappeKPICard.native.js.map +1 -1
  10. package/dist/cjs/FrappeListView.cjs +6 -6
  11. package/dist/cjs/FrappeListView.native.js +6 -6
  12. package/dist/cjs/FrappeListView.native.js.map +1 -1
  13. package/dist/cjs/fields/DateRange/index.cjs +30 -2
  14. package/dist/cjs/fields/DateRange/index.native.js +34 -2
  15. package/dist/cjs/fields/DateRange/index.native.js.map +1 -1
  16. package/dist/cjs/shared/devWarn.native.js.map +1 -1
  17. package/dist/esm/FormSidebar.mjs +7 -7
  18. package/dist/esm/FormSidebar.mjs.map +1 -1
  19. package/dist/esm/FormSidebar.native.js +7 -7
  20. package/dist/esm/FormSidebar.native.js.map +1 -1
  21. package/dist/esm/FrappeChartCard.mjs +30 -4
  22. package/dist/esm/FrappeChartCard.mjs.map +1 -1
  23. package/dist/esm/FrappeChartCard.native.js +30 -4
  24. package/dist/esm/FrappeChartCard.native.js.map +1 -1
  25. package/dist/esm/FrappeKPICard.mjs +6 -5
  26. package/dist/esm/FrappeKPICard.mjs.map +1 -1
  27. package/dist/esm/FrappeKPICard.native.js +6 -5
  28. package/dist/esm/FrappeKPICard.native.js.map +1 -1
  29. package/dist/esm/FrappeListView.mjs +6 -6
  30. package/dist/esm/FrappeListView.mjs.map +1 -1
  31. package/dist/esm/FrappeListView.native.js +6 -6
  32. package/dist/esm/FrappeListView.native.js.map +1 -1
  33. package/dist/esm/fields/DateRange/index.mjs +30 -2
  34. package/dist/esm/fields/DateRange/index.mjs.map +1 -1
  35. package/dist/esm/fields/DateRange/index.native.js +34 -2
  36. package/dist/esm/fields/DateRange/index.native.js.map +1 -1
  37. package/dist/esm/shared/devWarn.mjs.map +1 -1
  38. package/dist/esm/shared/devWarn.native.js.map +1 -1
  39. package/dist/jsx/FormSidebar.mjs +7 -7
  40. package/dist/jsx/FormSidebar.mjs.map +1 -1
  41. package/dist/jsx/FormSidebar.native.js +7 -7
  42. package/dist/jsx/FormSidebar.native.js.map +1 -1
  43. package/dist/jsx/FrappeChartCard.mjs +30 -4
  44. package/dist/jsx/FrappeChartCard.mjs.map +1 -1
  45. package/dist/jsx/FrappeChartCard.native.js +29 -3
  46. package/dist/jsx/FrappeChartCard.native.js.map +1 -1
  47. package/dist/jsx/FrappeKPICard.mjs +6 -5
  48. package/dist/jsx/FrappeKPICard.mjs.map +1 -1
  49. package/dist/jsx/FrappeKPICard.native.js +6 -5
  50. package/dist/jsx/FrappeKPICard.native.js.map +1 -1
  51. package/dist/jsx/FrappeListView.mjs +6 -6
  52. package/dist/jsx/FrappeListView.mjs.map +1 -1
  53. package/dist/jsx/FrappeListView.native.js +6 -6
  54. package/dist/jsx/FrappeListView.native.js.map +1 -1
  55. package/dist/jsx/fields/DateRange/index.mjs +30 -2
  56. package/dist/jsx/fields/DateRange/index.mjs.map +1 -1
  57. package/dist/jsx/fields/DateRange/index.native.js +34 -2
  58. package/dist/jsx/fields/DateRange/index.native.js.map +1 -1
  59. package/dist/jsx/shared/devWarn.mjs.map +1 -1
  60. package/dist/jsx/shared/devWarn.native.js.map +1 -1
  61. package/package.json +18 -18
  62. package/src/FormSidebar.tsx +7 -7
  63. package/src/FrappeChartCard.spec.tsx +179 -0
  64. package/src/FrappeChartCard.tsx +55 -6
  65. package/src/FrappeKPICard.spec.tsx +25 -0
  66. package/src/FrappeKPICard.tsx +17 -11
  67. package/src/FrappeListView.tsx +6 -6
  68. package/src/FrappeProvider.catchUp.spec.tsx +502 -0
  69. package/src/FrappeProvider.tsx +2 -2
  70. package/src/fields/Date/Date.spec.tsx +8 -1
  71. package/src/fields/Date/nativePresentation.spec.tsx +8 -1
  72. package/src/fields/Date/typeParity.spec.tsx +8 -1
  73. package/src/fields/DateRange/DateRange.spec.tsx +87 -15
  74. package/src/fields/DateRange/index.tsx +47 -3
  75. package/src/shared/devWarn.ts +3 -1
  76. package/types/FrappeChartCard.d.ts +17 -1
  77. package/types/FrappeChartCard.d.ts.map +1 -1
  78. package/types/FrappeKPICard.d.ts +11 -6
  79. package/types/FrappeKPICard.d.ts.map +1 -1
  80. package/types/FrappeProvider.d.ts +2 -2
  81. package/types/FrappeProvider.d.ts.map +1 -1
  82. package/types/fields/DateRange/index.d.ts.map +1 -1
  83. package/types/shared/devWarn.d.ts +1 -1
  84. package/types/shared/devWarn.d.ts.map +1 -1
@@ -0,0 +1,179 @@
1
+ /**
2
+ * MPO-287: a `dataSource` with no data source in scope is a misconfiguration,
3
+ * not static mode — the same defect MPO-237 fixed one component over. The
4
+ * card asked for a computed series and there is nowhere to compute it, so
5
+ * `config.data` must never be painted as the series: a plausible shape
6
+ * presented confidently is worse than a visible gap, because nothing
7
+ * downstream can tell it was never computed.
8
+ *
9
+ * Static mode (no `dataSource` at all) is the documented, intended path and
10
+ * is asserted here too, so the fix cannot over-correct into it. A fixture
11
+ * corpus is a data source (MPO-228), so it stands in for a baseURL and must
12
+ * not trip the guard.
13
+ *
14
+ * The chart body itself is not assertable here: ChartSurface measures its
15
+ * own width through `onLayout`, which never fires in happy-dom, so no SVG
16
+ * reaches the DOM. Two things stand in. The card's plain title renders only
17
+ * when a series reached `ChartCard` — the skeleton, the empty surface and
18
+ * the not-configured surface each carry different text. And `formatLabel`
19
+ * is called once per computed group, so spying on it proves whether the
20
+ * aggregate ran at all.
21
+ */
22
+
23
+ import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
24
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
25
+ import { cleanup, screen, waitFor } from "@testing-library/react";
26
+ import i18next from "i18next";
27
+ import { initReactI18next } from "react-i18next";
28
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
29
+ import { FrappeChartCard } from "./FrappeChartCard";
30
+ import { FrappeProvider } from "./FrappeProvider";
31
+ import { __resetFrappeDevWarnSeen } from "./shared/devWarn";
32
+
33
+ beforeAll(async () => {
34
+ await i18next.use(initReactI18next).init({
35
+ lng: "en",
36
+ interpolation: { escapeValue: false },
37
+ resources: { en: { translation: {} } },
38
+ });
39
+ });
40
+
41
+ beforeEach(() => {
42
+ __resetFrappeDevWarnSeen();
43
+ });
44
+
45
+ afterEach(() => {
46
+ cleanup();
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ const config = {
51
+ id: "todo-by-status",
52
+ title: "ToDos by Status",
53
+ type: "bar" as const,
54
+ data: [
55
+ { label: "Open", value: 41 },
56
+ { label: "Closed", value: 7 },
57
+ ],
58
+ };
59
+
60
+ const dataSource = { doctype: "ToDo", groupBy: "status" };
61
+
62
+ const todos = [
63
+ { name: "TD-1", doctype: "ToDo", status: "Open" },
64
+ { name: "TD-2", doctype: "ToDo", status: "Open" },
65
+ { name: "TD-3", doctype: "ToDo", status: "Closed" },
66
+ ];
67
+
68
+ describe("FrappeChartCard with a dataSource and no baseURL (MPO-287)", () => {
69
+ it("does not render config.data as though it were the computed series", () => {
70
+ vi.spyOn(console, "warn").mockImplementation(() => {});
71
+ renderWithProviders(<FrappeChartCard config={config} dataSource={dataSource} />);
72
+
73
+ expect(screen.queryByText("ToDos by Status")).toBeNull();
74
+ expect(screen.getByText("ToDos by Status is not configured")).toBeInTheDocument();
75
+ expect(screen.getByText(/no Frappe baseURL is in scope/)).toBeInTheDocument();
76
+ });
77
+
78
+ it("logs a development warning naming the missing baseURL", () => {
79
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
80
+ renderWithProviders(<FrappeChartCard config={config} dataSource={dataSource} />);
81
+
82
+ const line = warn.mock.calls.map((call) => String(call[0])).join("\n");
83
+ expect(line).toContain("[frappe-ui] chart-datasource-without-baseurl");
84
+ expect(line).toContain("baseURL");
85
+ expect(line).toContain("component=FrappeChartCard");
86
+ expect(line).toContain("id=todo-by-status");
87
+ expect(line).toContain("doctype=ToDo");
88
+ });
89
+
90
+ it("warns once per card, not once per render", () => {
91
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
92
+ const { rerender } = renderWithProviders(
93
+ <FrappeChartCard config={config} dataSource={dataSource} />,
94
+ );
95
+ rerender(<FrappeChartCard config={{ ...config }} dataSource={dataSource} />);
96
+ rerender(<FrappeChartCard config={{ ...config }} dataSource={dataSource} />);
97
+
98
+ const lines = warn.mock.calls.filter((call) =>
99
+ String(call[0]).includes("chart-datasource-without-baseurl"),
100
+ );
101
+ expect(lines).toHaveLength(1);
102
+ });
103
+
104
+ it("stays silent in production", () => {
105
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
106
+ const previous = process.env.NODE_ENV;
107
+ process.env.NODE_ENV = "production";
108
+ try {
109
+ renderWithProviders(<FrappeChartCard config={config} dataSource={dataSource} />);
110
+ } finally {
111
+ process.env.NODE_ENV = previous;
112
+ }
113
+
114
+ expect(
115
+ warn.mock.calls.filter((call) =>
116
+ String(call[0]).includes("chart-datasource-without-baseurl"),
117
+ ),
118
+ ).toHaveLength(0);
119
+ });
120
+ });
121
+
122
+ describe("FrappeChartCard configured paths stay untouched (MPO-287)", () => {
123
+ it("renders config.data with no warning when no dataSource is set", () => {
124
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
125
+ renderWithProviders(<FrappeChartCard config={config} />);
126
+
127
+ expect(screen.getByText("ToDos by Status")).toBeInTheDocument();
128
+ expect(screen.queryByText("ToDos by Status is not configured")).toBeNull();
129
+ expect(
130
+ warn.mock.calls.filter((call) =>
131
+ String(call[0]).includes("chart-datasource-without-baseurl"),
132
+ ),
133
+ ).toHaveLength(0);
134
+ });
135
+
136
+ it("computes the series, and does not warn, when the provider supplies a baseURL", async () => {
137
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
138
+ const formatLabel = vi.fn((label: string) => label);
139
+ renderWithProviders(
140
+ <FrappeProvider baseURL="http://test" fixtures={new InMemoryFixtureProvider({ ToDo: todos })}>
141
+ <FrappeChartCard config={config} dataSource={{ ...dataSource, formatLabel }} />
142
+ </FrappeProvider>,
143
+ );
144
+
145
+ await waitFor(() => expect(screen.getByText("ToDos by Status")).toBeInTheDocument());
146
+ expect([...new Set(formatLabel.mock.calls.map((call) => call[0]))].sort()).toEqual([
147
+ "Closed",
148
+ "Open",
149
+ ]);
150
+ expect(screen.queryByText("ToDos by Status is not configured")).toBeNull();
151
+ expect(
152
+ warn.mock.calls.filter((call) =>
153
+ String(call[0]).includes("chart-datasource-without-baseurl"),
154
+ ),
155
+ ).toHaveLength(0);
156
+ });
157
+
158
+ it("treats a fixture corpus as the data source when no baseURL is set (MPO-228)", async () => {
159
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
160
+ const formatLabel = vi.fn((label: string) => label);
161
+ renderWithProviders(
162
+ <FrappeProvider fixtures={new InMemoryFixtureProvider({ ToDo: todos })}>
163
+ <FrappeChartCard config={config} dataSource={{ ...dataSource, formatLabel }} />
164
+ </FrappeProvider>,
165
+ );
166
+
167
+ await waitFor(() => expect(screen.getByText("ToDos by Status")).toBeInTheDocument());
168
+ expect([...new Set(formatLabel.mock.calls.map((call) => call[0]))].sort()).toEqual([
169
+ "Closed",
170
+ "Open",
171
+ ]);
172
+ expect(screen.queryByText("ToDos by Status is not configured")).toBeNull();
173
+ expect(
174
+ warn.mock.calls.filter((call) =>
175
+ String(call[0]).includes("chart-datasource-without-baseurl"),
176
+ ),
177
+ ).toHaveLength(0);
178
+ });
179
+ });
@@ -15,6 +15,14 @@
15
15
  * (permission failures called out), catalog empty state when the aggregate
16
16
  * returns no groups, and a subtle stale dim while a post-event refetch is
17
17
  * in flight.
18
+ *
19
+ * A `dataSource` with no data source in scope — no `baseURL` from the prop
20
+ * or a FrappeProvider, and no fixture corpus either — is a misconfiguration,
21
+ * not static mode: the caller asked for a computed series and there is
22
+ * nowhere to compute it. The card renders the not-configured surface and
23
+ * warns in dev. It never falls back to `config.data`, which would present
24
+ * an uncomputed shape as the series. A fixture corpus answers every read
25
+ * locally, so it stands in for a baseURL (MPO-228).
18
26
  */
19
27
  import {
20
28
  AsyncBoundary,
@@ -23,12 +31,13 @@ import {
23
31
  type ChartCardProps,
24
32
  type ChartConfig,
25
33
  } from "@multiplatform.one/components";
26
- import React, { useMemo } from "react";
34
+ import React, { useEffect, useMemo } from "react";
27
35
  import { useTranslation } from "react-i18next";
28
36
  import { YStack, type YStackProps } from "tamagui";
29
37
  import { useResolvedKnobs } from "@multiplatform.one/theme";
30
38
  import { ChartCard } from "./ChartCard";
31
39
  import { useFrappeConfig } from "./FrappeProvider";
40
+ import { frappeDevWarn } from "./shared/devWarn";
32
41
  import {
33
42
  useFrappeAggregate,
34
43
  type FrappeAggregateConnection,
@@ -52,10 +61,18 @@ export interface FrappeChartDataSource {
52
61
  }
53
62
 
54
63
  export interface FrappeChartCardProps extends Omit<ChartCardProps, "config"> {
55
- /** Chart chrome (title, type, height…). In live mode `data` is overridden. */
64
+ /**
65
+ * Chart chrome (title, type, height…). In live mode `data` is overridden.
66
+ * With a `dataSource` and no data source in scope, `data` is not rendered
67
+ * at all.
68
+ */
56
69
  config: Omit<ChartConfig, "data"> & { data?: ChartConfig["data"] };
57
70
  /** When set, the chart series goes live against this doctype. */
58
71
  dataSource?: FrappeChartDataSource;
72
+ /**
73
+ * Falls back to the FrappeProvider's. Required whenever `dataSource` is
74
+ * set, unless the provider carries a fixture corpus.
75
+ */
59
76
  baseURL?: string;
60
77
  auth?: { useToken?: boolean; token?: string; type?: string };
61
78
  syncOptions?: unknown;
@@ -94,8 +111,9 @@ export function FrappeChartCard({
94
111
  const { t } = useTranslation();
95
112
 
96
113
  const finalBaseURL = baseURL ?? providerConfig?.baseURL;
114
+ const fixtures = providerConfig?.fixtures;
97
115
  const connection: FrappeAggregateConnection | undefined =
98
- dataSource && finalBaseURL
116
+ dataSource && (finalBaseURL || fixtures)
99
117
  ? {
100
118
  baseURL: finalBaseURL,
101
119
  socketPort: providerConfig?.socketPort,
@@ -104,10 +122,22 @@ export function FrappeChartCard({
104
122
  auth: auth ?? providerConfig?.auth,
105
123
  syncOptions: syncOptions ?? providerConfig?.syncOptions,
106
124
  realtime: providerConfig?.realtime,
107
- fixtures: providerConfig?.fixtures,
125
+ fixtures,
108
126
  }
109
127
  : undefined;
110
128
 
129
+ const missingConnection = !!dataSource && !connection;
130
+ const doctype = dataSource?.doctype;
131
+ useEffect(() => {
132
+ if (!missingConnection) return;
133
+ frappeDevWarn("chart-datasource-without-baseurl", {
134
+ component: "FrappeChartCard",
135
+ id: config.id,
136
+ doctype,
137
+ suggest: "set baseURL on FrappeChartCard or on the FrappeProvider above it",
138
+ });
139
+ }, [missingConnection, config.id, doctype]);
140
+
111
141
  const { groups, isLoading, isStale, error, isPermissionError } = useFrappeAggregate(connection, {
112
142
  doctype: dataSource?.doctype ?? "",
113
143
  aggregate: dataSource?.aggregate,
@@ -128,11 +158,30 @@ export function FrappeChartCard({
128
158
  return { ...config, data } as ChartConfig;
129
159
  }, [config, dataSource, groups]);
130
160
 
131
- // Static passthrough — sanitizing bridge, palette untouched.
132
- if (!dataSource || !connection) {
161
+ // Static passthrough — sanitizing bridge, palette untouched. The caller
162
+ // supplied the series and asked for no computation.
163
+ if (!dataSource) {
133
164
  return <ChartCard config={liveConfig} {...chartCardProps} />;
134
165
  }
135
166
 
167
+ // A requested series with nowhere to compute it. `config.data` is the
168
+ // caller's placeholder, so drawing it here would publish a shape nothing
169
+ // downstream could tell was never computed.
170
+ if (!connection) {
171
+ return (
172
+ <AsyncBoundary
173
+ compact
174
+ error
175
+ errorTitle={t("{{title}} is not configured", { title: config.title })}
176
+ errorDescription={t(
177
+ "This chart reads {{doctype}} live, but no Frappe baseURL is in scope, so the series was never computed.",
178
+ { doctype: dataSource.doctype },
179
+ )}
180
+ {...(chartCardProps as YStackProps)}
181
+ />
182
+ );
183
+ }
184
+
136
185
  if (isLoading) {
137
186
  return (
138
187
  <ChartCardSkeleton
@@ -125,4 +125,29 @@ describe("FrappeKPICard configured paths stay untouched (MPO-237)", () => {
125
125
  warn.mock.calls.filter((call) => String(call[0]).includes("kpi-datasource-without-baseurl")),
126
126
  ).toHaveLength(0);
127
127
  });
128
+
129
+ it("treats a fixture corpus as the data source when no baseURL is set (MPO-228)", async () => {
130
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
131
+ renderWithProviders(
132
+ <FrappeProvider
133
+ fixtures={
134
+ new InMemoryFixtureProvider({
135
+ ToDo: [
136
+ { name: "TD-1", doctype: "ToDo", status: "Open" },
137
+ { name: "TD-2", doctype: "ToDo", status: "Open" },
138
+ ],
139
+ })
140
+ }
141
+ >
142
+ <FrappeKPICard config={config} dataSource={{ doctype: "ToDo" }} />
143
+ </FrappeProvider>,
144
+ );
145
+
146
+ await waitFor(() => expect(screen.getByText("2")).toBeInTheDocument());
147
+ expect(screen.queryByText("999")).toBeNull();
148
+ expect(screen.queryByText("Open ToDos is not configured")).toBeNull();
149
+ expect(
150
+ warn.mock.calls.filter((call) => String(call[0]).includes("kpi-datasource-without-baseurl")),
151
+ ).toHaveLength(0);
152
+ });
128
153
  });
@@ -12,11 +12,13 @@
12
12
  * (the card dims while a post-event refetch is in flight), and "—" when the
13
13
  * aggregate has no result (e.g. avg over an empty set).
14
14
  *
15
- * A `dataSource` with no `baseURL` in scope (neither the prop nor a
16
- * FrappeProvider) is a misconfiguration, not static mode: the caller asked
17
- * for a computed figure and there is nowhere to compute it. The card renders
18
- * the not-configured surface and warns in dev. It never falls back to
19
- * `config.value`, which would present an uncomputed number as the KPI.
15
+ * A `dataSource` with no data source in scope — no `baseURL` from the prop
16
+ * or a FrappeProvider, and no fixture corpus either — is a misconfiguration,
17
+ * not static mode: the caller asked for a computed figure and there is
18
+ * nowhere to compute it. The card renders the not-configured surface and
19
+ * warns in dev. It never falls back to `config.value`, which would present
20
+ * an uncomputed number as the KPI. A fixture corpus answers every read
21
+ * locally, so it stands in for a baseURL (MPO-228).
20
22
  */
21
23
  import {
22
24
  AsyncBoundary,
@@ -57,7 +59,10 @@ export interface FrappeKPICardProps extends Omit<YStackProps, "children"> {
57
59
  config: KPICardConfig;
58
60
  /** When set, the card's value goes live against this doctype. */
59
61
  dataSource?: FrappeKPIDataSource;
60
- /** Falls back to the FrappeProvider's. Required whenever `dataSource` is set. */
62
+ /**
63
+ * Falls back to the FrappeProvider's. Required whenever `dataSource` is
64
+ * set, unless the provider carries a fixture corpus.
65
+ */
61
66
  baseURL?: string;
62
67
  auth?: { useToken?: boolean; token?: string; type?: string };
63
68
  syncOptions?: unknown;
@@ -87,8 +92,9 @@ export function FrappeKPICard({
87
92
  const { t } = useTranslation();
88
93
 
89
94
  const finalBaseURL = baseURL ?? providerConfig?.baseURL;
95
+ const fixtures = providerConfig?.fixtures;
90
96
  const connection: FrappeAggregateConnection | undefined =
91
- dataSource && finalBaseURL
97
+ dataSource && (finalBaseURL || fixtures)
92
98
  ? {
93
99
  baseURL: finalBaseURL,
94
100
  socketPort: providerConfig?.socketPort,
@@ -97,21 +103,21 @@ export function FrappeKPICard({
97
103
  auth: auth ?? providerConfig?.auth,
98
104
  syncOptions: syncOptions ?? providerConfig?.syncOptions,
99
105
  realtime: providerConfig?.realtime,
100
- fixtures: providerConfig?.fixtures,
106
+ fixtures,
101
107
  }
102
108
  : undefined;
103
109
 
104
- const missingBaseURL = !!dataSource && !finalBaseURL;
110
+ const missingConnection = !!dataSource && !connection;
105
111
  const doctype = dataSource?.doctype;
106
112
  useEffect(() => {
107
- if (!missingBaseURL) return;
113
+ if (!missingConnection) return;
108
114
  frappeDevWarn("kpi-datasource-without-baseurl", {
109
115
  component: "FrappeKPICard",
110
116
  id: config.id,
111
117
  doctype,
112
118
  suggest: "set baseURL on FrappeKPICard or on the FrappeProvider above it",
113
119
  });
114
- }, [missingBaseURL, config.id, doctype]);
120
+ }, [missingConnection, config.id, doctype]);
115
121
 
116
122
  const { value, isLoading, isStale, error, isPermissionError } = useFrappeAggregate(connection, {
117
123
  doctype: dataSource?.doctype ?? "",
@@ -298,7 +298,7 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
298
298
  {title}
299
299
  </Text>
300
300
  {subtitleParts.length > 0 && (
301
- <Text {...knobProps.body} color={componentColors.text.muted} numberOfLines={1}>
301
+ <Text {...knobProps.body} color={knobProps.textAccentColor} numberOfLines={1}>
302
302
  {subtitleParts.map((part, index) => (
303
303
  <React.Fragment key={index}>
304
304
  {index > 0 ? " · " : null}
@@ -432,7 +432,7 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
432
432
  {...stackProps}
433
433
  >
434
434
  <Spinner size="large" />
435
- <Text {...knobProps.body} color={componentColors.text.muted} marginTop="$2">
435
+ <Text {...knobProps.body} color={knobProps.textAccentColor} marginTop="$2">
436
436
  {t("Loading {{doctype}} schema...", { doctype })}
437
437
  </Text>
438
438
  </YStack>
@@ -517,17 +517,17 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
517
517
  {...knobProps.panelPadding}
518
518
  {...knobProps.gap}
519
519
  >
520
- <Text {...knobProps.label} color={componentColors.text.muted}>
520
+ <Text {...knobProps.label} color={knobProps.textAccentColor}>
521
521
  {t("Filters")}
522
522
  </Text>
523
523
  {standardFilterFields.length > 0 && (
524
524
  <YStack {...knobProps.gap}>
525
525
  {standardFilterFields.map((field) => (
526
526
  <YStack key={field.fieldname} {...knobProps.gap}>
527
- <Text {...knobProps.label} color={componentColors.text.muted}>
527
+ <Text {...knobProps.label} color={knobProps.textAccentColor}>
528
528
  {field.label || field.fieldname}
529
529
  </Text>
530
- <Text {...knobProps.body} color={componentColors.text.muted}>
530
+ <Text {...knobProps.body} color={knobProps.textAccentColor}>
531
531
  {field.fieldtype === "Select" ? field.options || "—" : "—"}
532
532
  </Text>
533
533
  </YStack>
@@ -536,7 +536,7 @@ export function FrappeListView<TDoc extends { name: string; doctype: string }>({
536
536
  )}
537
537
  {savedFiltersHook.filters.length > 0 && (
538
538
  <YStack {...knobProps.gap}>
539
- <Text {...knobProps.label} color={componentColors.text.muted}>
539
+ <Text {...knobProps.label} color={knobProps.textAccentColor}>
540
540
  {t("Saved Filters")}
541
541
  </Text>
542
542
  <SavedFilterDropdown