Docyrus

EnumOptionEditor

Optimistic editor for a field's selectable options — inline rename, colour and icon pickers, active switch, advanced flags and long-press drag reordering, plus a bottom-sheet launcher and a Docyrus-connected wrapper.

iOSAndroid
Preview EnumOptionEditor on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-enum-option-editor
Required Packages(6 packages)
pnpm add @tanstack/react-query @docyrus/api-client @docyrus/app-utils react-native-gesture-handler react-native-reanimated tailwind-variants

The module exports four building blocks, API-aligned with the web @docyrus/ui enum option editor:

ExportKindUse it for
EnumOptionEditorComponentPure, backend-agnostic inline editor (settings screens, tests)
EnumOptionEditorDialogComponentThe same editor inside a large bottom sheet (web: Dialog) with a trigger
DocyrusEnumOptionEditorComponentThe sheet wired to the Docyrus enum admin API for one field
useDocyrusEnumEditorPermissionHookWhether the signed-in user may manage options (ADMIN / ARCHITECT role)

Usage

Pure editor

import { useState } from 'react';

import {
  EnumOptionEditor,
  type EnumEditorOption
} from '@/components/docyrus-native/enum-option-editor';

const INITIAL: EnumEditorOption[] = [
  { id: 's-1', name: 'Open', color: 'green-500', sort_order: 10, origin: 'inherited' },
  { id: 's-2', name: 'Doing', color: 'orange-500', sort_order: 20, origin: 'inherited', isCustomized: true },
  { id: 's-3', name: 'On hold', color: '#8b5cf6', icon: 'fal pause', sort_order: 30, origin: 'custom' }
];

export function StatusOptions() {
  const [options, setOptions] = useState(INITIAL);

  return (
    <EnumOptionEditor
      options={options}
      onCreateOption={input => ({ id: `n-${Date.now()}`, origin: 'custom', ...input })}
      onUpdateOption={(id, patch) => setOptions(prev => prev.map(o => (o.id === id ? { ...o, ...patch } : o)))}
      onDeleteOption={id => setOptions(prev => prev.filter(o => o.id !== id))}
      onReorder={ids => setOptions(prev => ids.map(id => prev.find(o => o.id === id)!))} />
  );
}

Bottom-sheet launcher

import { EnumOptionEditorDialog } from '@/components/docyrus-native/enum-option-editor';

<EnumOptionEditorDialog
  options={options}
  title="Manage “Status” options"
  onUpdateOption={updateOption}
  trigger={({ onPress }) => <Button variant="ghost" onPress={onPress}>Manage options</Button>} />

Docyrus-connected (permission-gated)

import {
  DocyrusEnumOptionEditor,
  useDocyrusEnumEditorPermission
} from '@/components/docyrus-native/enum-option-editor';

export function ManageStatusOptions({ client, field, dataSource }) {
  const { canManage } = useDocyrusEnumEditorPermission(client);

  if (!canManage) return null;

  return (
    <DocyrusEnumOptionEditor
      client={client}
      appId={dataSource.tenant_app_id}
      dataSourceId={dataSource.id}
      fieldId={field.id}
      fieldName={field.name}
      onChanged={() => refetchSchema()} />
  );
}

Interaction model

GestureResult
Tap the icon / colour tileOpens the icon sheet (Font Awesome / Huge Icons, featured + search) or the colour sheet (Tailwind families at tone 500, stored as blue-500, plus a free token / hex input)
Edit the nameCommitted on blur or the keyboard's Done key — not per keystroke. Duplicate names show an inline error
Active switchRetires / re-activates the option (active)
"⋯"Action sheet: More settings (slug, description, is_final_option, force_description, force_followup_date, active), Move up / Move down, Delete (custom) or Revert to default (inherited, enabled only when customized)
Long-press the grip + dragReorders the list; neighbours slide out of the way and onReorder(orderedIds) fires on drop

Every change is applied optimistically; if the matching callback rejects, the row rolls back and shows the error message.

API Reference

EnumOptionEditor

PropTypeDefaultDescription
optionsEnumEditorOption[]—Required. Initial options, rendered by sort_order (nulls last) then name. The editor reseeds when the incoming options change
onCreateOption(input: EnumOptionInput) => Promise<EnumEditorOption> | EnumEditorOption—Persist a new option; must resolve to the created option. Omitted → a local tmp_<n> row is added
onUpdateOption(id: string, patch: Partial<EnumOptionInput>) => Promise<void> | void—Persist an edit (name on blur / submit, pickers, switches, advanced fields)
onDeleteOption(id: string) => Promise<void> | void—Permanently delete a custom option
onRevertOption(id: string) => Promise<void> | void—Drop the workspace override of an inherited option
onReorder(orderedIds: string[]) => Promise<void> | void—Persist the new order after a drag or Move up / down
readOnlybooleanfalseRender everything read-only (e.g. options managed via a shared enum set)
allowCreatebooleantrueShow the add-option row
allowDeletebooleantrueOffer Delete / Revert in the row menu
allowReorderbooleantrueEnable the drag grip and the Move up / Move down menu items
noticeTextstring—Info banner (Alert) above the list
emptyTextstring'No options yet.'Empty-state copy
classNamestring—Container className

EnumOptionEditorDialog

Accepts every EnumOptionEditor prop plus:

PropTypeDefaultDescription
triggerReactNode | ((opts: { onPress: () => void }) => ReactNode) | null"Manage options" buttonCustom trigger. A node is wrapped in a Pressable; a function receives onPress; null renders no trigger (fully controlled)
openboolean—Controlled open state
onOpenChange(open: boolean) => void—Open state callback
titlestring'Manage options'Sheet title
descriptionstring'Add, edit, reorder and retire the selectable options for this field.'Sheet message
disabledboolean—Disable the default trigger

DocyrusEnumOptionEditor

PropTypeDefaultDescription
clientRestApiClient—Required. Authenticated Docyrus REST client
appIdstring—Required. App id (tenant_app_id) or slug — the {appId} path segment
dataSourceIdstring—Required. Data-source id
fieldIdstring—Required. Field id whose options are managed
fieldNamestring—Used as the sheet title when title is omitted
triggerReactNode | ((opts: { onPress: () => void }) => ReactNode) | null"Manage options" buttonSame as EnumOptionEditorDialog
openboolean—Controlled open state
onOpenChange(open: boolean) => void—Open state callback
onChanged() => void—Called after every successful write (refresh the owning form's schema)
disabledboolean—Disable the default trigger
titlestringfieldNameSheet title

Endpoints (all under /v1/dev/apps/{appId}/data-sources/{dataSourceId}/fields/{fieldId}/enums): GET (options + enumSetId), POST create, PATCH update / reorder (sortOrder = (i + 1) * 10), DELETE (enumIds), DELETE …/{id}/customization revert. Ownership (custom vs inherited) comes from GET /v1/dev/data-sources/enums?fieldId=…. A field on a shared enum set opens read-only with a notice. Options are fetched only while the sheet is open (query key ['docyrus-enum-options', dataSourceId, fieldId]); every write invalidates it and the shared inventory's data sources (getSharedDocyrusInventory(client).invalidateDataSources()). Requires the Architect.ReadWrite.All scope plus the ADMIN or ARCHITECT role.

useDocyrusEnumEditorPermission

useDocyrusEnumEditorPermission(
  client: RestApiClient | undefined,
  options?: UseDocyrusEnumEditorPermissionOptions
): { canManage: boolean; isLoading: boolean }
OptionTypeDefaultDescription
enabledbooleantrueGate the profile fetch
adminRoleIdsstring[][ADMIN_ROLE_ID, ARCHITECT_ROLE_ID]Role ids allowed to manage options
meEndpointstring'/v1/users/me'Profile endpoint (query key ['docyrus-me', meEndpoint], 5-minute stale time)

Constants: ADMIN_ROLE_ID = 'e37ea658-8cfa-11ed-a098-7703de2a84b1', ARCHITECT_ROLE_ID = '61672102-b5c8-11ee-b08a-23d1fbdaaa6a'.

Components

ComponentDescription
EnumOptionEditorInline optimistic option list editor
EnumOptionEditorDialogBottom-sheet launcher for the editor
DocyrusEnumOptionEditorLauncher wired to the Docyrus enum admin API

Type Exports

TypeDescription
EnumEditorOptionEnumOption + origin?: EnumOptionOrigin + isCustomized?: boolean
EnumOptionOrigin'custom' | 'inherited'
EnumOptionInputEditable attributes: name, slug?, color?, icon?, description?, sort_order?, active?, is_final_option?, force_description?, force_followup_date?, parent?
EnumOptionEditorPropsProps for EnumOptionEditor
EnumOptionEditorDialogPropsProps for EnumOptionEditorDialog
EnumOptionEditorTriggerNative trigger type (ReactNode | (({ onPress }) => ReactNode))
DocyrusEnumOptionEditorPropsProps for DocyrusEnumOptionEditor
UseDocyrusEnumEditorPermissionOptionsOptions for useDocyrusEnumEditorPermission

Differences from web

  • EnumOptionEditorDialog / DocyrusEnumOptionEditor render a large bottom sheet instead of a Dialog; trigger additionally accepts ({ onPress }) => ReactNode.
  • Row actions live in a "⋯" action sheet; Move up / Move down are added as an accessible alternative to drag.
  • The Active switch is shown on every row (web keeps it in the advanced popover; native has it in both places).

Translation keys

Copy is read through useUiTranslation() with the web keys: ui.enumEditor.* (manageOptions, title, dialogDescription, color, colorPlaceholder, icon, iconFontAwesome, iconHugeIcons, iconSearch, noIcons, clear, slug, slugAuto, description, active, activeHint, finalOption, finalOptionHint, forceDescription, forceFollowup, moreSettings, reorder, revert, delete, inherited, overridden, empty, addPlaceholder, add, duplicateName, saveFailed, deleteFailed, revertFailed, createFailed, sharedSetNotice) plus native-only ui.enumEditor.name, ui.enumEditor.reorderHint, ui.enumEditor.moveUp, ui.enumEditor.moveDown, and the shared ui.common.done / ui.common.loading.

On this page