@protobase/ui 0.58.3 → 0.58.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.
@@ -2,25 +2,24 @@ import { useAdminMeta } from "../meta-gate.js";
2
2
  import { useAppAddress } from "../use-app-address.js";
3
3
  import { ConfirmAction } from "../pages/confirm-action.js";
4
4
  import { isDanger } from "../pages/action-prompt.js";
5
- import { actionApplies } from "../pages/action-when.js";
6
- import { actionFor, linkTarget, openLink } from "../pages/action-href.js";
5
+ import { actionFor } from "../pages/action-href.js";
7
6
  import { useActionRunner } from "../pages/use-run-action.js";
8
7
  import { text } from "../list-messages.js";
8
+ import { actionLink } from "../pages/action-link.js";
9
9
  import { rowRecord } from "./row-record.js";
10
10
  import { useState } from "react";
11
- import { fillAddress } from "@protobase/schema/internal";
12
11
  import { jsx } from "react/jsx-runtime";
13
12
  //#region src/app/list-actions/use-list-actions.tsx
14
13
  /**
15
14
  * The named actions of `model`'s view for its list, those without inputs (which ask in a dialog of their own): each
16
15
  * in the row menu of the rows it shows for, disabled with its reason where `when` disables it, and in red when it is a
17
- * delete or a danger action; a link is a link there. Updates and handlers marked `bulk` are offered for a selection
16
+ * delete or a danger action; a link is a link there, and a disabled one a disabled item without one. Updates and handlers marked `bulk` are offered for a selection
18
17
  * while one selected row applies, and run on those that do. `prompt` is the question one asks before it runs.
19
18
  */
20
19
  const useListActions = (model) => {
21
20
  const { views } = useAdminMeta();
22
21
  const runner = useActionRunner(model.name, void 0);
23
- const { go, href } = useAppAddress();
22
+ const app = useAppAddress();
24
23
  const [asking, setAsking] = useState();
25
24
  const listed = (views[model.name]?.actions ?? []).filter((action) => !action.inputs?.length);
26
25
  const perform = (run) => "row" in run ? runner.run(run.action, run.row) : runner.runEach(run.action, run.selection, run.skipped);
@@ -36,33 +35,18 @@ const useListActions = (model) => {
36
35
  destructive: isDanger(action)
37
36
  };
38
37
  const run = action.run;
39
- if (run?.kind === "link") {
40
- const available = (row) => shown(row) !== void 0 && actionApplies(action, rowRecord(model, row).record);
41
- if ("to" in run) {
42
- const address = (row) => fillAddress(run.to, rowRecord(model, row).record);
43
- return {
44
- ...base,
45
- available,
46
- link: (row) => ({
47
- href: href(address(row)),
48
- onFollow: () => go(address(row))
49
- })
50
- };
51
- }
52
- const target = (row) => linkTarget(run.href, rowRecord(model, row).record) ?? "";
53
- return {
54
- ...base,
55
- available,
56
- link: (row) => ({
57
- href: target(row),
58
- onFollow: () => openLink(target(row))
59
- })
60
- };
61
- }
38
+ const blocked = (row) => runner.blockedOn(action, rowRecord(model, row));
39
+ const available = (row) => shown(row) !== void 0;
40
+ if (run?.kind === "link") return {
41
+ ...base,
42
+ available,
43
+ blocked,
44
+ link: (row) => actionLink(run, rowRecord(model, row).record, app)
45
+ };
62
46
  return {
63
47
  ...base,
64
- available: (row) => shown(row) !== void 0,
65
- blocked: (row) => runner.blockedOn(action, rowRecord(model, row)),
48
+ available,
49
+ blocked,
66
50
  onSelect: (row) => start({
67
51
  action,
68
52
  row: rowRecord(model, row)
@@ -0,0 +1,25 @@
1
+ import { linkTarget } from "./action-href.js";
2
+ import { fillAddress } from "@protobase/schema/internal";
3
+ //#region src/app/pages/action-link.ts
4
+ /**
5
+ * A link action on `record` as a link: an address in the app is followed in place, a page outside it, an `https:` or
6
+ * `http:` one in a new tab, is left to the browser. A link whose `href` has no target on the record is never shown
7
+ * (`actionFor` leaves it out), so asking for it is a mistake.
8
+ */
9
+ const actionLink = (run, record, { href, go }) => {
10
+ if ("to" in run) {
11
+ const to = fillAddress(run.to, record);
12
+ return {
13
+ href: href(to),
14
+ onFollow: () => go(to)
15
+ };
16
+ }
17
+ const target = linkTarget(run.href, record);
18
+ if (target === void 0) throw new Error("A link action without a target on the record is not shown");
19
+ return {
20
+ href: target,
21
+ newTab: /^https?:/i.test(target)
22
+ };
23
+ };
24
+ //#endregion
25
+ export { actionLink };
@@ -1,4 +1,5 @@
1
1
  import { ActionRail } from "../../record-view/action-rail.js";
2
+ import { RouteLink } from "../../primitives/route-link.js";
2
3
  import { AppLink, useAppAddress } from "../use-app-address.js";
3
4
  import { ActionMenu } from "../../record-view/action-menu.js";
4
5
  import { ActionPrompt, asksFirst, isDanger } from "./action-prompt.js";
@@ -6,6 +7,7 @@ import { useActionRunner } from "./use-run-action.js";
6
7
  import { useTopBarSlot } from "../../app-shell/top-bar-slot.js";
7
8
  import "../../app-shell/top-bar.js";
8
9
  import { text } from "../record-messages.js";
10
+ import { actionLink } from "./action-link.js";
9
11
  import { actionIcon } from "../action-icon.js";
10
12
  import { useState } from "react";
11
13
  import { Pencil, Trash2 } from "lucide-react";
@@ -19,7 +21,8 @@ import { createPortal } from "react-dom";
19
21
  * however long the record is, when the shell has a top bar.
20
22
  */
21
23
  const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
22
- const { go } = useAppAddress();
24
+ const app = useAppAddress();
25
+ const { go } = app;
23
26
  const slot = useTopBarSlot();
24
27
  const scope = {
25
28
  model,
@@ -33,14 +36,24 @@ const RecordActions = ({ model, stored, recordKey, edit, onDelete }) => {
33
36
  resource: model.name
34
37
  }, { replace: true }));
35
38
  const [asking, setAsking] = useState();
36
- const named = runner.actions.map((action) => ({
37
- name: action.label,
38
- icon: actionIcon(action.icon),
39
- blocked: runner.blocked(action),
40
- running: runner.running === action.name,
41
- ...isDanger(action) && { tone: "danger" },
42
- onRun: () => asksFirst(action) ? setAsking(action) : void runner.run(action)
43
- }));
39
+ const named = runner.actions.map((action) => {
40
+ const blocked = runner.blocked(action);
41
+ const link = action.run?.kind === "link" && !blocked ? actionLink(action.run, stored.record, app) : void 0;
42
+ return {
43
+ name: action.label,
44
+ icon: actionIcon(action.icon),
45
+ blocked,
46
+ running: runner.running === action.name,
47
+ ...isDanger(action) && { tone: "danger" },
48
+ onRun: () => asksFirst(action) ? setAsking(action) : void runner.run(action),
49
+ ...link && { render: ({ className, children, role }) => /* @__PURE__ */ jsx(RouteLink, {
50
+ to: link,
51
+ role,
52
+ className,
53
+ children
54
+ }) }
55
+ };
56
+ });
44
57
  const deletes = runner.actions.some((action) => action.run?.kind === "delete");
45
58
  const actions = [
46
59
  ...edit ? [{
@@ -40,7 +40,7 @@ const RowMenu = ({ row, actions }) => {
40
40
  const itemClass = cn("block w-full px-3 py-1.5 text-left hover:bg-muted disabled:cursor-default disabled:opacity-60 disabled:hover:bg-transparent", action.destructive && "text-danger-text");
41
41
  return /* @__PURE__ */ jsx("li", {
42
42
  role: "none",
43
- children: action.link ? /* @__PURE__ */ jsx(RouteLink, {
43
+ children: action.link && blocked === void 0 ? /* @__PURE__ */ jsx(RouteLink, {
44
44
  role: "menuitem",
45
45
  to: action.link(row),
46
46
  onClick: () => setOpen(false),
@@ -54,7 +54,7 @@ const RowMenu = ({ row, actions }) => {
54
54
  className: itemClass,
55
55
  onClick: () => {
56
56
  setOpen(false);
57
- action.onSelect(row);
57
+ action.onSelect?.(row);
58
58
  },
59
59
  children: label
60
60
  })
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.58.3";
2
+ var version = "0.58.5";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };
@@ -6,15 +6,22 @@ import { jsx } from "react/jsx-runtime";
6
6
  * with Ctrl, Cmd, Shift or Alt opens it in a new tab or window, or saves it, as on any link. Every navigation a click
7
7
  * starts is one of these, never a button that moves the page, so the browser shows where it goes and can open it.
8
8
  */
9
- const RouteLink = ({ to, onClick, ...rest }) => /* @__PURE__ */ jsx("a", {
10
- href: to.href,
11
- onClick: (event) => {
12
- onClick?.(event);
13
- if (event.defaultPrevented || !inApp(event)) return;
14
- event.preventDefault();
15
- to.onFollow();
16
- },
17
- ...rest
18
- });
9
+ const RouteLink = ({ to, onClick, ...rest }) => {
10
+ const { href, onFollow, newTab } = to;
11
+ return /* @__PURE__ */ jsx("a", {
12
+ href,
13
+ ...newTab && {
14
+ target: "_blank",
15
+ rel: "noreferrer"
16
+ },
17
+ onClick: (event) => {
18
+ onClick?.(event);
19
+ if (!onFollow || event.defaultPrevented || !inApp(event)) return;
20
+ event.preventDefault();
21
+ onFollow();
22
+ },
23
+ ...rest
24
+ });
25
+ };
19
26
  //#endregion
20
27
  export { RouteLink };