4. Employee screens#

What this step does. Repeats the Department five-file pattern for Employees, plus two idioms the first entity didn’t need: a dotted department.code column that reads the FK join inline, and a MantineServerLookup against /department that turns the Department dropdown into a live server query.

Same five-file layout as Department — grid + new-form at the top level, view subfolder for the detail page and its edit dialog. The interesting bits here are the flattened FK column in the grid and the MantineServerLookup against /department inside the form.

src/pages/employee/
  EmployeeGridPage.tsx
  EmployeeNewForm.tsx
  view/
    EmployeeViewPage.tsx
    EmployeeViewForm.tsx
    EmployeeEditForm.tsx

Grid page#

Columns use dotted attribute paths to project joined data — department.code reads the department code via the FK join without a second fetch. A module-level statusRenderer attaches as a cellRenderer on the status column.

// src/pages/employee/EmployeeGridPage.tsx
import { useRef } from 'react';
import { Badge } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { SummaryGrid } from '@palmyralabs/template-tribble';
import type { ColumnDefinition, IPageQueryable } from '@palmyralabs/rt-forms';
import { ServiceEndpoint } from '../../config/ServiceEndpoints';
import EmployeeNewForm from './EmployeeNewForm';

const statusColor: Record<string, string> = {
  ACTIVE: 'green', INACTIVE: 'gray', RESIGNED: 'red',
};
const statusRenderer = (p: any) => {
  const v = p.getValue();
  return <Badge color={statusColor[v] ?? 'gray'}>{v}</Badge>;
};

const fields: ColumnDefinition[] = [
  { attribute: 'loginName',       name: 'loginName',      label: 'Email',
    searchable: true, sortable: true, type: 'string' },
  { attribute: 'firstName',       name: 'firstName',      label: 'First Name',
    searchable: true, sortable: true, type: 'string' },
  { attribute: 'lastName',        name: 'lastName',       label: 'Last Name',
    searchable: true, sortable: true, type: 'string' },
  { attribute: 'department.code', name: 'departmentCode', label: 'Department',
    searchable: true, sortable: true, type: 'string' },
  { attribute: 'joiningDate',     name: 'joiningDate',    label: 'Joined',
    sortable: true, type: 'date',
    serverPattern:  'YYYY-MM-DD',
    displayPattern: 'DD MMM YYYY' },
  { attribute: 'status',          name: 'status',         label: 'Status',
    sortable: true, type: 'string', cellRenderer: statusRenderer },
];

export default function EmployeeGridPage() {
  const gridRef = useRef<IPageQueryable>(null);
  const [opened, { open, close }] = useDisclosure(false);

  const endPoint = ServiceEndpoint.employee.restApi;
  const onRefresh = () => gridRef.current?.refresh();

  const getPluginOptions = (): any => ({
    addText:    'Add Employee',
    onNewClick: open,
    export:     { visible: false },
    backOption: false,
  });

  return (
    <>
      <SummaryGrid
        title="Employee"
        pageName=""
        columns={fields}
        pageSize={[15, 30, 45]}
        gridRef={gridRef}
        getPluginOptions={getPluginOptions}
        options={{ endPoint }}
      />
      <EmployeeNewForm
        open={opened}
        onClose={close}
        onRefresh={onRefresh}
        size="lg"
        pageName="Employee"
        title="New Employee"
      />
    </>
  );
}

New-form dialog#

MantineServerLookup on department runs a debounced query against /department, displays code / name in the dropdown, and writes back a { department: { id } } FK on submit.

// src/pages/employee/EmployeeNewForm.tsx
import { DialogNewForm } from '@palmyralabs/template-tribble';
import { FieldGroupContainer } from '@palmyralabs/rt-forms';
import {
  MantineTextField, MantineDateInput,
  MantineSelect,    MantineServerLookup,
} from '@palmyralabs/rt-forms-mantine';
import { ServiceEndpoint, LookupEndPoint } from '../../config/ServiceEndpoints';
import { fieldMsg } from '../../config/ErrorMsgConfig';

interface Props {
  open:      boolean;
  onClose:   () => void;
  onRefresh: () => void;
  size?:     string;
  pageName:  string;
  title:     string;
}

export default function EmployeeNewForm(props: Props) {
  const apiEndPoint = ServiceEndpoint.employee.restApi;

  return (
    <DialogNewForm
      endPoint={apiEndPoint}
      open={props.open}
      onClose={props.onClose}
      onRefresh={props.onRefresh}
      size={props.size}
      pageName={props.pageName}
      title={props.title}
    >
      <div className="palmyra-form-field-container-wrapper">
        <FieldGroupContainer columns={2}>
          <MantineTextField attribute="loginName" label="Email" required autoFocus
                            validRule="email"
                            invalidMessage={fieldMsg.mandatory} />

          <MantineServerLookup
            attribute="department"
            label="Department"
            placeholder="Select Department"
            queryOptions={{
              endPoint:       LookupEndPoint.department,
              queryAttribute: 'name',
              labelAttribute: 'name',
              idAttribute:    'id',
              delay:          250,
            }}
            displayAttribute={['code', 'name']}
            required
            invalidMessage={fieldMsg.mandatory}
          />

          <MantineTextField attribute="firstName" label="First Name" required
                            invalidMessage={fieldMsg.mandatory} />
          <MantineTextField attribute="lastName"  label="Last Name"  required
                            invalidMessage={fieldMsg.mandatory} />

          <MantineDateInput attribute="joiningDate" label="Joining Date" required
                            serverPattern="YYYY-MM-DD"
                            displayPattern="DD MMM YYYY"
                            invalidMessage={fieldMsg.mandatory} />

          <MantineSelect attribute="status" label="Status"
                         defaultValue="ACTIVE"
                         options={[
                           { value: 'ACTIVE',   label: 'Active' },
                           { value: 'INACTIVE', label: 'Inactive' },
                           { value: 'RESIGNED', label: 'Resigned' },
                         ]} />
        </FieldGroupContainer>
      </div>
    </DialogNewForm>
  );
}

View page#

// src/pages/employee/view/EmployeeViewPage.tsx
import { useRef } from 'react';
import { useParams } from 'react-router-dom';
import EmployeeViewForm from './EmployeeViewForm';

export default function EmployeeViewPage() {
  const params = useParams();
  const formRef = useRef<any>(null);

  return (
    <div style={{ padding: 16 }}>
      <EmployeeViewForm
        ref={formRef}
        id={String(params.id)}
        pageName="Employee"
      />
    </div>
  );
}

View form (read-only)#

Display-only widgets from rt-forms-mantine’s form/view/MantineTextView for scalars, MantineLookupView for the FK so the department code + name are rendered without another round-trip.

// src/pages/employee/view/EmployeeViewForm.tsx
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Group, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { PalmyraViewForm, FieldGroupContainer, type IViewForm } from '@palmyralabs/rt-forms';
import {
  MantineTextView, MantineDateView, MantineLookupView, MantineOptionsView,
} from '@palmyralabs/rt-forms-mantine';
import { ServiceEndpoint } from '../../../config/ServiceEndpoints';
import EmployeeEditForm from './EmployeeEditForm';

interface Props { id: string; pageName: string }

const statusOptions = { ACTIVE: 'Active', INACTIVE: 'Inactive', RESIGNED: 'Resigned' };

const EmployeeViewForm = forwardRef(function EmployeeViewForm(props: Props, ref) {
  const navigate = useNavigate();
  const [opened, { open, close }] = useDisclosure(false);
  const formRef = useRef<IViewForm>(null);

  useImperativeHandle(ref, () => ({
    refresh: () => formRef.current?.refresh(),
  }));

  return (
    <>
      <Group justify="space-between" mb="md">
        <Title order={3}>Employee</Title>
        <Group>
          <Button variant="default" onClick={() => navigate('/employees')}>Back</Button>
          <Button onClick={open}>Edit</Button>
        </Group>
      </Group>

      <PalmyraViewForm
        formRef={formRef}
        endPoint={`${ServiceEndpoint.employee.restApi}/{id}`}
        id={props.id}
      >
        <FieldGroupContainer columns={2}>
          <MantineTextView   attribute="loginName"  label="Email" />
          <MantineLookupView attribute="department" label="Department"
                             lookupOptions={{ idAttribute: 'id', labelAttribute: 'name' }} />
          <MantineTextView   attribute="firstName"  label="First Name" />
          <MantineTextView   attribute="lastName"   label="Last Name" />
          <MantineDateView   attribute="joiningDate" label="Joined"
                             serverPattern="YYYY-MM-DD"
                             displayPattern="DD MMM YYYY" />
          <MantineOptionsView attribute="status"    label="Status"
                              options={statusOptions} />
        </FieldGroupContainer>
      </PalmyraViewForm>

      <EmployeeEditForm
        open={opened}
        onClose={close}
        onRefresh={() => formRef.current?.refresh()}
        employeeId={props.id}
        size="lg"
        pageName={props.pageName}
        title="Edit Employee"
      />
    </>
  );
});

export default EmployeeViewForm;

Edit-form dialog#

Mirror of the new-form, with DialogEditForm + id prop:

// src/pages/employee/view/EmployeeEditForm.tsx
import { DialogEditForm } from '@palmyralabs/template-tribble';
import { FieldGroupContainer } from '@palmyralabs/rt-forms';
import {
  MantineTextField, MantineDateInput,
  MantineSelect,    MantineServerLookup,
} from '@palmyralabs/rt-forms-mantine';
import { ServiceEndpoint, LookupEndPoint } from '../../../config/ServiceEndpoints';
import { fieldMsg } from '../../../config/ErrorMsgConfig';

interface Props {
  open:       boolean;
  onClose:    () => void;
  onRefresh:  () => void;
  employeeId: any;
  size?:      string;
  pageName:   string;
  title:      string;
}

export default function EmployeeEditForm(props: Props) {
  const apiEndPoint = ServiceEndpoint.employee.restApi;

  return (
    <DialogEditForm
      endPoint={apiEndPoint}
      open={props.open}
      onClose={props.onClose}
      onRefresh={props.onRefresh}
      id={props.employeeId}
      size={props.size}
      pageName={props.pageName}
      title={props.title}
    >
      <div className="palmyra-form-field-container-wrapper">
        <FieldGroupContainer columns={2}>
          <MantineTextField attribute="loginName" label="Email" required
                            validRule="email"
                            invalidMessage={fieldMsg.mandatory} />

          <MantineServerLookup
            attribute="department"
            label="Department"
            placeholder="Select Department"
            queryOptions={{
              endPoint:       LookupEndPoint.department,
              queryAttribute: 'name',
              labelAttribute: 'name',
              idAttribute:    'id',
              delay:          250,
            }}
            displayAttribute={['code', 'name']}
            required
            invalidMessage={fieldMsg.mandatory}
          />

          <MantineTextField attribute="firstName" label="First Name" required
                            invalidMessage={fieldMsg.mandatory} />
          <MantineTextField attribute="lastName"  label="Last Name"  required
                            invalidMessage={fieldMsg.mandatory} />

          <MantineDateInput attribute="joiningDate" label="Joining Date" required
                            serverPattern="YYYY-MM-DD"
                            displayPattern="DD MMM YYYY"
                            invalidMessage={fieldMsg.mandatory} />

          <MantineSelect attribute="status" label="Status"
                         options={[
                           { value: 'ACTIVE',   label: 'Active' },
                           { value: 'INACTIVE', label: 'Inactive' },
                           { value: 'RESIGNED', label: 'Resigned' },
                         ]} />
        </FieldGroupContainer>
      </div>
    </DialogEditForm>
  );
}

With the grid, new-form, view page, and edit-form wired, the Employee screens are complete. Head to Recap and next steps for an end-to-end wiring summary and pointers on where to take the app from here.