AsyncTreeMenuEditor#

@palmyralabs/rt-forms · src/palmyra/menu/AsyncTreeMenuEditor.tsx

Overview#

Admin-side counterpart to AsyncTreeMenu. Renders the same tree with checkbox selection so an administrator can edit which menu entries a given role/group can access.

Internally it builds its own TreeQueryStore from a factory (storeFactory.getTreeStore), seeded with endPointOptions: { groupId }, so the backend returns each node pre-marked with the group’s current mask value. On mount, nodes with mask == 2 are selected by default; descendants inherit via propagateSelect.

The getValue() imperative method walks the current selection and returns the tree in a normalised shape ready to POST back to the permission endpoint.

Props — IAsyncTreeEditorInput#

interface IAsyncTreeEditorInput {
  storeFactory: StoreFactory<IChildTreeRequest, StoreOptions>;
  endPoint:     IEndPoint;
  groupId:      number;
  readOnly?:    boolean;    // disables click-to-toggle (still renders the tree)
  fineGrained?: boolean;    // hides the placeholder "crud - under construction" per-leaf control
}

Ref — IAsyncTreeEditor#

interface IAsyncTreeEditor {
  getValue(): Node;
}

interface Node {
  id:       number;
  parent:   number;
  name:     string;
  loaded:   boolean;
  isBranch: true;
  children: (Node | number)[];
  selected: 0 | 1 | 2;    // 0 = none, 1 = half / partial, 2 = all
}

getValue() returns the tree rooted at a synthetic node { id: -1, name: 'root', children: [...] }. Each child carries menuId, parent, name, mask (0 / 1 / 2), menuCode, and its own children array — the shape the backend’s ACL-save endpoint expects.

Example — edit and save a group’s menu access#

import { useRef } from 'react';
import { AsyncTreeMenuEditor, type IAsyncTreeEditor } from '@palmyralabs/rt-forms';
import { Button, Group } from '@mantine/core';
import AppStoreFactory from '@/wire/StoreFactory';

export function GroupMenuAccessEditor({ groupId, onSaved }: {
  groupId: number;
  onSaved: () => void;
}) {
  const editorRef = useRef<IAsyncTreeEditor>(null);

  const save = async () => {
    const tree = editorRef.current?.getValue();
    await fetch(`/api/palmyra/group/${groupId}/menu`, {
      method:  'PUT',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify(tree),
    });
    onSaved();
  };

  return (
    <>
      <AsyncTreeMenuEditor
        ref={editorRef}
        storeFactory={AppStoreFactory}
        endPoint="/menu"
        groupId={groupId}
      />
      <Group justify="flex-end" mt="md">
        <Button onClick={save}>Save access</Button>
      </Group>
    </>
  );
}

Read-only variant#

Pass readOnly to freeze the current selection — useful for audit/review screens where an administrator looks but can’t change:

<AsyncTreeMenuEditor
  storeFactory={AppStoreFactory}
  endPoint="/menu"
  groupId={groupId}
  readOnly
/>

Caveats#

  • The component expects getRoot() to return rows with a mask column (0 / 1 / 2); without it every node renders unselected.
  • The per-leaf CRUD control is currently a placeholder ("crud - under construction") — fineGrained={true} hides it. Plan your UX around coarse-grained selection for now.