4. Forms and ACL#
Forms use @palmyralabs/rt-forms for state/validation and @palmyralabs/rt-forms-mantine for the Mantine-styled input widgets. You do not wire Mantine’s useForm directly — the Palmyra form layer takes care of binding, validation, submit, and store integration.
Dialog create form#
The clinic sample uses a DialogNewForm template that renders a modal, collects attributes into a FieldGroupContainer, and POSTs to endPoint on submit.
// src/pages/masterdata/manufacturer/ManufacturerNewForm.tsx
import { DialogNewForm } from '@palmyralabs/template-tribble';
import { FieldGroupContainer } from '@palmyralabs/rt-forms';
import { TextField, TextArea, CurrencyField }
from '@palmyralabs/rt-forms-mantine';
import { ServiceEndpoint } from '@/config/ServiceEndpoints';
import { MobileRegex, fieldMsg } from '@/config/validation';
export default function ManufacturerNewForm(props: {
open: boolean;
onClose: () => void;
onRefresh: () => void;
pageName: string;
title: string;
}) {
const apiEndPoint = ServiceEndpoint.masterData.manufacturer.restApi;
return (
<DialogNewForm
size="lg"
endPoint={apiEndPoint}
open={props.open}
onClose={props.onClose}
onRefresh={props.onRefresh}
pageName={props.pageName}
title={props.title}
>
<FieldGroupContainer columns={2}>
<TextField
attribute="name"
label="Name"
autoFocus required
invalidMessage={fieldMsg.mandatory}
/>
<TextField
attribute="contactMobile"
label="Mobile"
validRule="number"
regExp={{ regex: MobileRegex, errorMessage: 'Invalid phone number' }}
/>
<TextField
attribute="contactEmail"
label="Email"
validRule="email"
/>
<CurrencyField
attribute="rating"
label="Rating"
min={0} max={5} defaultValue={0}
/>
<TextArea
attribute="address"
label="Address"
colspan={2}
/>
</FieldGroupContainer>
</DialogNewForm>
);
}The attribute prop on each field is the POJO attribute name declared in @PalmyraField. That’s the only cross-stack coupling — rename the Java field and the JSON key, and this form changes in one place.
Validation shapes declaratively:
required— non-empty.validRule="number" | "email"— built-in checks.regExp={{ regex, errorMessage }}— custom pattern.invalidMessage— per-field override for the error text.
Edit / delete flows#
Edit forms reuse the same field components inside DialogEditForm (rt-forms-mantine), passing keyValue={id} so the form hydrates from GET /mstManufacturer/{id} and submits via PUT. Delete is an imperative call on the grid store triggered from the row-action toolbar — no form at all.
ACL codes#
The backend’s palmyra-dbacl-mgmt extension returns each user’s permission codes at login. The frontend stashes them in local state and exposes a hook.
// src/config/AclPermissions.ts
export const AclPermission = {
manufacturer: {
GET_ALL : 'CUTAP001',
CREATE : 'CUTAP002',
UPDATE : 'CUTAP003',
DELETE : 'CUTAP004',
},
stockEntry: {
GET_ALL : 'CUTAP101',
CREATE : 'CUTAP102',
},
};// src/admin/components/checkAclAccess/AclAccessCheck.ts
import { topic } from '@palmyralabs/ts-utils';
let cache = readFromStorage();
topic.subscribe('resetAcl', () => { cache = readFromStorage(); });
export function useAclAccess() {
return {
hasAclAccess: (code: string) => cache.includes(code),
};
}
function readFromStorage(): string[] {
try { return JSON.parse(localStorage.getItem('acl') ?? '[]'); }
catch { return []; }
}The topic.subscribe('resetAcl', ...) pub-sub refreshes the cache after login/logout — no full page reload needed.
Gating UI on codes#
import { useAclAccess } from '@/admin/components/checkAclAccess/AclAccessCheck';
import { AclPermission } from '@/config/AclPermissions';
function ManufacturerGridPage() {
const { hasAclAccess } = useAclAccess();
return (
<SummaryGrid
{...gridProps}
getPluginOptions={() => ({
onNewClick: hasAclAccess(AclPermission.manufacturer.CREATE) ? open : undefined,
addText: hasAclAccess(AclPermission.manufacturer.CREATE) ? 'Add' : '',
export: { visible: hasAclAccess(AclPermission.manufacturer.GET_ALL) },
})}
/>
);
}A hidden control isn’t a security boundary — the backend’s ACL filter still rejects unauthorized requests with 403. The useAclAccess hook only avoids showing controls that the server would refuse.