@protobase/ui 0.12.3 → 0.12.5

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/app/app.js CHANGED
@@ -11,6 +11,7 @@ import { useRecord } from "../data/use-record.js";
11
11
  import { recordTitle } from "../live/model-helpers.js";
12
12
  import { humanize } from "../live/naming.js";
13
13
  import { ToastProvider } from "../toasts/toast-provider.js";
14
+ import { startErrorReporting } from "./error-reporting.js";
14
15
  import { useProjectUi } from "./pages/project-ui.js";
15
16
  import { AssistantButton, AssistantPanel } from "./assistant-shell.js";
16
17
  import { bottomBarFromMeta } from "./bottom-bar-from-meta.js";
@@ -101,6 +102,10 @@ const Routes = ({ workspace, sidebarMode, assistant, runtime }) => {
101
102
  page: params.size > 0 ? `${path}?${params}` : path,
102
103
  onClose: () => setAssistantOpen(false)
103
104
  });
105
+ const errorReporting = meta.errorReporting;
106
+ useEffect(() => {
107
+ if (errorReporting) startErrorReporting(errorReporting);
108
+ }, [errorReporting]);
104
109
  const runtimeUrl = meta.runtime?.url;
105
110
  const runtimeButton = runtimeUrl && /* @__PURE__ */ jsx(RuntimeUpdateButton, {
106
111
  url: runtimeUrl,
@@ -0,0 +1,25 @@
1
+ import { redactSecrets } from "@protobase/schema";
2
+ //#region src/app/error-reporting.ts
3
+ let loading;
4
+ let capture;
5
+ /**
6
+ * Sends the app's uncaught errors to the error tracker `/meta` names, from the first call on; the Sentry SDK loads only
7
+ * then, so an app without a DSN never loads it. Errors only, with no performance tracing and no session tracking,
8
+ * every event without its secrets (see `redactSecrets`).
9
+ */
10
+ const startErrorReporting = ({ dsn, environment }) => {
11
+ loading ??= import("@sentry/browser").then((sentry) => {
12
+ sentry.init({
13
+ dsn,
14
+ ...environment && { environment },
15
+ integrations: (defaults) => defaults.filter((integration) => integration.name !== "BrowserSession"),
16
+ beforeSend: (event) => redactSecrets(event)
17
+ });
18
+ capture = (error, componentStack) => sentry.captureException(error, componentStack ? { contexts: { react: { componentStack } } } : void 0);
19
+ });
20
+ return loading;
21
+ };
22
+ /** Reports an error a boundary caught while drawing, with the components it was drawn in; nothing before error reporting started. */
23
+ const reportRenderError = (error, componentStack) => capture?.(error, componentStack ?? void 0);
24
+ //#endregion
25
+ export { reportRenderError, startErrorReporting };
@@ -1,3 +1,4 @@
1
+ import { reportRenderError } from "../error-reporting.js";
1
2
  import { Problem } from "./problem.js";
2
3
  import { Component } from "react";
3
4
  import { jsxs } from "react/jsx-runtime";
@@ -8,6 +9,9 @@ var CustomBoundary = class extends Component {
8
9
  static getDerivedStateFromError() {
9
10
  return { failed: true };
10
11
  }
12
+ componentDidCatch(error, info) {
13
+ reportRenderError(error, info.componentStack);
14
+ }
11
15
  render() {
12
16
  if (!this.state.failed) return this.props.children;
13
17
  return this.props.fallback === void 0 ? /* @__PURE__ */ jsxs(Problem, { children: [this.props.name, " could not be drawn."] }) : this.props.fallback;
@@ -114,6 +114,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
114
114
  const permissions = usePermissions(model.name);
115
115
  const restore = useRestore(model);
116
116
  const canUpdate = can(permissions, "update") && stored.permissions?.update !== false;
117
+ const canDelete = can(permissions, "delete") && stored.permissions?.delete !== false;
117
118
  const remove = useDeleteFlow({
118
119
  model,
119
120
  view,
@@ -210,7 +211,7 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
210
211
  onClick: editor.discard,
211
212
  children: "Discard"
212
213
  }),
213
- can(permissions, "delete") && stored.permissions?.delete !== false && /* @__PURE__ */ jsx(Button, {
214
+ canDelete && /* @__PURE__ */ jsx(Button, {
214
215
  variant: "danger",
215
216
  className: "max-md:min-h-11",
216
217
  onClick: () => void remove.start({
@@ -231,122 +232,125 @@ const LoadedRecord = ({ model, recordKey, stored }) => {
231
232
  })
232
233
  ] });
233
234
  return /* @__PURE__ */ jsxs("div", {
234
- className: "min-h-0 flex-1 overflow-y-auto",
235
+ className: "flex min-h-0 flex-1 flex-col",
235
236
  children: [
236
237
  remove.dialogs,
237
- /* @__PURE__ */ jsxs("div", {
238
- className: "mx-auto max-w-[1100px] px-4 pb-28 pt-6 md:px-10 md:py-8",
239
- children: [
240
- /* @__PURE__ */ jsx(RecordHeader, {
241
- collection: view?.names?.plural ?? humanize(model.name),
242
- title,
243
- status: {
244
- label: status ? enumLabel(status, hint("status")?.valueLabels) : singular,
245
- tone: status ? toneFor(status) : "neutral"
246
- },
247
- lastEditedBy: { ago: updated === void 0 ? "recently" : formatRelativeTime(updated) },
248
- actions: /* @__PURE__ */ jsx("div", {
249
- className: "hidden items-center gap-4 md:flex",
250
- children: saveControls
251
- })
252
- }),
253
- editor.message && /* @__PURE__ */ jsx("div", {
254
- className: "mt-4",
255
- children: /* @__PURE__ */ jsx(ErrorBanner, {
256
- title: "Not saved",
257
- error: new Error(editor.message)
258
- })
259
- }),
260
- editor.linesConflict && /* @__PURE__ */ jsx("div", {
261
- className: "mt-4",
262
- children: /* @__PURE__ */ jsx(ErrorBanner, {
263
- title: "The lines were changed by someone else",
264
- error: /* @__PURE__ */ new Error("The lines were reloaded. Your new order is kept: Save again to apply it, or Discard to drop it.")
265
- })
266
- }),
267
- editor.restored && /* @__PURE__ */ jsxs("div", {
268
- className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
269
- role: "status",
270
- children: [/* @__PURE__ */ jsx("span", { children: "Your unsaved changes were restored." }), /* @__PURE__ */ jsxs("span", {
271
- className: "flex gap-2",
272
- children: [/* @__PURE__ */ jsx(Button, {
273
- size: "sm",
274
- onClick: editor.dismissRestored,
275
- children: "Keep"
276
- }), /* @__PURE__ */ jsx(Button, {
277
- size: "sm",
278
- onClick: editor.discard,
279
- children: "Discard"
238
+ /* @__PURE__ */ jsx("div", {
239
+ className: "min-h-0 flex-1 overflow-y-auto",
240
+ children: /* @__PURE__ */ jsxs("div", {
241
+ className: "mx-auto max-w-[1100px] px-4 pb-8 pt-6 md:px-10 md:py-8",
242
+ children: [
243
+ /* @__PURE__ */ jsx(RecordHeader, {
244
+ collection: view?.names?.plural ?? humanize(model.name),
245
+ title,
246
+ status: {
247
+ label: status ? enumLabel(status, hint("status")?.valueLabels) : singular,
248
+ tone: status ? toneFor(status) : "neutral"
249
+ },
250
+ lastEditedBy: { ago: updated === void 0 ? "recently" : formatRelativeTime(updated) },
251
+ actions: /* @__PURE__ */ jsx("div", {
252
+ className: "hidden items-center gap-4 md:flex",
253
+ children: saveControls
254
+ })
255
+ }),
256
+ editor.message && /* @__PURE__ */ jsx("div", {
257
+ className: "mt-4",
258
+ children: /* @__PURE__ */ jsx(ErrorBanner, {
259
+ title: "Not saved",
260
+ error: new Error(editor.message)
261
+ })
262
+ }),
263
+ editor.linesConflict && /* @__PURE__ */ jsx("div", {
264
+ className: "mt-4",
265
+ children: /* @__PURE__ */ jsx(ErrorBanner, {
266
+ title: "The lines were changed by someone else",
267
+ error: /* @__PURE__ */ new Error("The lines were reloaded. Your new order is kept: Save again to apply it, or Discard to drop it.")
268
+ })
269
+ }),
270
+ editor.restored && /* @__PURE__ */ jsxs("div", {
271
+ className: "mt-4 flex items-center justify-between gap-3 rounded-lg border bg-surface px-3 py-2 text-[13px]",
272
+ role: "status",
273
+ children: [/* @__PURE__ */ jsx("span", { children: "Your unsaved changes were restored." }), /* @__PURE__ */ jsxs("span", {
274
+ className: "flex gap-2",
275
+ children: [/* @__PURE__ */ jsx(Button, {
276
+ size: "sm",
277
+ onClick: editor.dismissRestored,
278
+ children: "Keep"
279
+ }), /* @__PURE__ */ jsx(Button, {
280
+ size: "sm",
281
+ onClick: editor.discard,
282
+ children: "Discard"
283
+ })]
280
284
  })]
281
- })]
282
- }),
283
- /* @__PURE__ */ jsxs("div", {
284
- className: "grid grid-cols-1 gap-8 md:grid-cols-[minmax(0,1fr)_300px] md:gap-12",
285
- children: [/* @__PURE__ */ jsxs("main", {
286
- className: "divide-y divide-border",
287
- children: [layout.flatMap((item, index) => {
288
- if (item.kind === "related") return [/* @__PURE__ */ jsx(RelatedSection, {
289
- item,
290
- recordKey
291
- }, `related:${item.title}`)];
292
- if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
293
- item,
294
- scope
295
- }, `custom:${index}`)];
296
- if (item.kind !== "section") return [];
297
- const fields = item.fields.filter((name) => model.fields[name] && visible(name));
298
- if (fields.length === 0) return [];
299
- return [/* @__PURE__ */ jsx(Section, {
300
- title: item.title,
301
- help: item.help,
302
- children: /* @__PURE__ */ jsx("div", {
303
- className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
304
- children: fields.map(fieldView)
285
+ }),
286
+ /* @__PURE__ */ jsxs("div", {
287
+ className: "grid grid-cols-1 gap-8 md:grid-cols-[minmax(0,1fr)_300px] md:gap-12",
288
+ children: [/* @__PURE__ */ jsxs("main", {
289
+ className: "divide-y divide-border",
290
+ children: [layout.flatMap((item, index) => {
291
+ if (item.kind === "related") return [/* @__PURE__ */ jsx(RelatedSection, {
292
+ item,
293
+ recordKey
294
+ }, `related:${item.title}`)];
295
+ if (item.kind === "custom") return item.sidebar ? [] : [/* @__PURE__ */ jsx(CustomSection, {
296
+ item,
297
+ scope
298
+ }, `custom:${index}`)];
299
+ if (item.kind !== "section") return [];
300
+ const fields = item.fields.filter((name) => model.fields[name] && visible(name));
301
+ if (fields.length === 0) return [];
302
+ return [/* @__PURE__ */ jsx(Section, {
303
+ title: item.title,
304
+ help: item.help,
305
+ children: /* @__PURE__ */ jsx("div", {
306
+ className: "grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2",
307
+ children: fields.map(fieldView)
308
+ })
309
+ }, item.title)];
310
+ }), linesModel && /* @__PURE__ */ jsx(RecordLines, {
311
+ lines: linesModel,
312
+ draft: linesDraft,
313
+ vatRate: Number(stored.record.vatRate ?? 21) / 100,
314
+ saving: editor.saving
315
+ })]
316
+ }), /* @__PURE__ */ jsxs("aside", {
317
+ className: "space-y-4 md:pt-8",
318
+ children: [
319
+ /* @__PURE__ */ jsx(RecordActions, {
320
+ model,
321
+ stored,
322
+ recordKey
323
+ }),
324
+ firstSidebar === -1 && summary,
325
+ layout.map((item, index) => item.kind === "custom" && item.sidebar ? /* @__PURE__ */ jsx(CustomSection, {
326
+ item,
327
+ scope
328
+ }, `custom:${index}`) : index === firstSidebar ? /* @__PURE__ */ jsx(Fragment, { children: summary }, "summary") : null),
329
+ /* @__PURE__ */ jsx(MetadataCard, {
330
+ title: "Details",
331
+ rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
332
+ const field = name ? model.fields[name] : void 0;
333
+ return name && field && visible(name) ? [{
334
+ label: hint(name)?.label ?? humanize(name),
335
+ value: displayValue(field, stored.record[name], hint(name))
336
+ }] : [];
337
+ })
338
+ }),
339
+ related.records.length > 0 && /* @__PURE__ */ jsx(SidebarCard, {
340
+ title: "Related records",
341
+ children: /* @__PURE__ */ jsx(RelatedRecords, {
342
+ records: related.records,
343
+ onNavigate: open
344
+ })
305
345
  })
306
- }, item.title)];
307
- }), linesModel && /* @__PURE__ */ jsx(RecordLines, {
308
- lines: linesModel,
309
- draft: linesDraft,
310
- vatRate: Number(stored.record.vatRate ?? 21) / 100,
311
- saving: editor.saving
346
+ ]
312
347
  })]
313
- }), /* @__PURE__ */ jsxs("aside", {
314
- className: "space-y-4 md:pt-8",
315
- children: [
316
- /* @__PURE__ */ jsx(RecordActions, {
317
- model,
318
- stored,
319
- recordKey
320
- }),
321
- firstSidebar === -1 && summary,
322
- layout.map((item, index) => item.kind === "custom" && item.sidebar ? /* @__PURE__ */ jsx(CustomSection, {
323
- item,
324
- scope
325
- }, `custom:${index}`) : index === firstSidebar ? /* @__PURE__ */ jsx(Fragment, { children: summary }, "summary") : null),
326
- /* @__PURE__ */ jsx(MetadataCard, {
327
- title: "Details",
328
- rows: [auditField(model, "created"), updatedAt].flatMap((name) => {
329
- const field = name ? model.fields[name] : void 0;
330
- return name && field && visible(name) ? [{
331
- label: hint(name)?.label ?? humanize(name),
332
- value: displayValue(field, stored.record[name], hint(name))
333
- }] : [];
334
- })
335
- }),
336
- related.records.length > 0 && /* @__PURE__ */ jsx(SidebarCard, {
337
- title: "Related records",
338
- children: /* @__PURE__ */ jsx(RelatedRecords, {
339
- records: related.records,
340
- onNavigate: open
341
- })
342
- })
343
- ]
344
- })]
345
- })
346
- ]
348
+ })
349
+ ]
350
+ })
347
351
  }),
348
- /* @__PURE__ */ jsx("div", {
349
- className: "fixed inset-x-0 bottom-0 z-30 flex items-center gap-2 border-t bg-background p-3 md:hidden",
352
+ (editor.dirty || canDelete || canUpdate) && /* @__PURE__ */ jsx("div", {
353
+ className: "flex shrink-0 items-center gap-2 border-t bg-background p-3 md:hidden",
350
354
  children: saveControls
351
355
  })
352
356
  ]
@@ -1,4 +1,5 @@
1
1
  import { ActionCard } from "../action-card/action-card.js";
2
+ import { reportRenderError } from "../app/error-reporting.js";
2
3
  import { useProjectUi } from "../app/pages/project-ui.js";
3
4
  import { Component } from "react";
4
5
  import { jsx } from "react/jsx-runtime";
@@ -20,6 +21,9 @@ var WidgetBoundary = class extends Component {
20
21
  static getDerivedStateFromError() {
21
22
  return { failed: true };
22
23
  }
24
+ componentDidCatch(error, info) {
25
+ reportRenderError(error, info.componentStack);
26
+ }
23
27
  render() {
24
28
  return this.state.failed ? /* @__PURE__ */ jsx(Fallback, {
25
29
  part: this.props.part,
@@ -20,6 +20,11 @@ export type MetaData = {
20
20
  runtime?: {
21
21
  url: string;
22
22
  };
23
+ /** The error tracker the app sends its errors to, when the app reports them (see `startErrorReporting`). */
24
+ errorReporting?: {
25
+ dsn: string;
26
+ environment?: string;
27
+ };
23
28
  /** The plugins the app loads for the user (see `PluginGate`). */
24
29
  plugins?: PluginRef[];
25
30
  /** The other APIs the app draws resources from for the user, and how far their `/meta` has loaded (see `withSources`). */
@@ -34,6 +34,7 @@ const useMeta = () => {
34
34
  ...result.meta.bottomBar && { bottomBar: result.meta.bottomBar },
35
35
  ...result.meta.assistant && { assistant: result.meta.assistant },
36
36
  ...result.meta.runtime && { runtime: result.meta.runtime },
37
+ ...result.meta.errorReporting && { errorReporting: result.meta.errorReporting },
37
38
  ...result.meta.plugins && { plugins: result.meta.plugins },
38
39
  ...result.meta.sources && { sources: result.meta.sources.map((source) => ({
39
40
  source,
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.12.3";
2
+ var version = "0.12.5";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };