---
name: mystique-patterns
scope: frontend
description: "Practical patterns: query construction, fragment mechanics, troubleshooting"
load: on-demand
relevant-skills: [fluent-mystique-builder, fluent-mystique-validate, fluent-mystique-analyze, fluent-ui-test]
---

# Mystique Patterns and Troubleshooting

Practical patterns, workflows, and troubleshooting guide for Fluent Commerce Mystique manifest development.

---

## 1. GraphQL Query Construction Workflow

When building queries for manifest data sources, follow this strict workflow to avoid hallucinated fields that cause silent failures.

### Step 1: Discover available fields

Use the Fluent MCP GraphQL tools:
- `graphql.listRoots` -- find available root query nodes (e.g., `orders`, `orderById`, `fulfilments`)
- `graphql.generateFull` -- generate a complete sample query from a given root with all available fields

### Step 2: Remove fields you do not need

Start from the generated query and trim it down. Keep only the fields your components actually reference.

### Step 3: Validate the query

Use `graphql.validate` and/or `graphql.execute` to test. Never run mutations without explicit user permission.

### Step 4: NEVER add fields that are not in the generated query

Hallucinated fields cause silent failures -- no error is thrown, the field simply returns `null` or the entire query silently breaks. If a field does not appear in `generateFull` output, it does not exist.

### The `me` query pattern

User actions that need to know who performed the action require the `me` query:

```graphql
query {
  me {
    username
    primaryRetailer {
      id
    }
    roles {
      edges {
        node {
          role {
            name
          }
        }
      }
    }
  }
}
```

This is typically added to the page's data sources when the page includes user action buttons.

---

## 2. Fragment Loading and Override Mechanics

### Plugin file precedence

The OMS app loads its main manifest from the plugin static file at:
```
/_plugins/oms/manifests/fc.mystique.manifest.oms.json
```

This file is NOT the same as the `fc.mystique.manifest.oms` setting. The setting exists (RETAILER context) but is **ignored** for route resolution when the plugin file exists. Only routes defined in the plugin file are loaded.

### Fragment resolution order

The main manifest references fragments. Each fragment resolves from either:

1. **Plugin static files** (OOTB fragments): `/_plugins/{plugin}/manifests/{settingName}.json`
2. **Settings API** (custom/modified fragments): GraphQL settings query matching the fragment name

### Available OMS fragments

| Fragment Name                                        | Source (default) |
|------------------------------------------------------|-----------------|
| `fc.mystique.manifest.oms.fragment.dashboard`        | Plugin file      |
| `fc.mystique.manifest.oms.fragment.ordermanagement`  | Settings API     |
| `fc.mystique.manifest.oms.fragment.mixedbaskets`     | Settings API     |
| `fc.mystique.manifest.oms.fragment.availability`     | Plugin file      |
| `fc.mystique.manifest.oms.fragment.products`         | Settings API     |
| `fc.mystique.manifest.oms.fragment.inventory`        | Settings API     |
| `fc.mystique.manifest.oms.fragment.unifiedinventory` | Settings API     |
| `fc.mystique.manifest.oms.fragment.stores`           | Settings API     |
| `fc.mystique.manifest.oms.fragment.insights`         | Plugin file      |
| `fc.mystique.manifest.oms.fragment.admin`            | Settings API     |

### How to customize a fragment

1. **Download the baseline** from the platform CDN:
   ```
   https://fluent.apps.fluentcommerce.com/_plugins/oms/manifests/fc.mystique.manifest.oms.fragment.ordermanagement.json
   ```

2. **Check if a setting override already exists** by querying:
   ```graphql
   query {
     settings(name: "fc.mystique.manifest.oms.fragment.ordermanagement") {
       edges { node { id name lobValue } }
     }
   }
   ```

3. **If no setting exists**, create one:
   ```graphql
   mutation {
     createSetting(input: {
       name: "fc.mystique.manifest.oms.fragment.ordermanagement"
       context: "ACCOUNT"
       contextId: 0
       valueType: "JSON"
       lobValue: { "manifestVersion": "2.0", "routes": [] }
     }) { id }
   }
   ```

4. **If a setting exists**, update it:
   ```graphql
   mutation {
     updateSetting(input: {
       id: 242
       lobValue: { "manifestVersion": "2.0", "routes": [] }
     }) { id }
   }
   ```

### Key constraint

You CANNOT add new fragment references to the main manifest plugin file. To add custom sections to the OMS app, add them to an existing fragment that IS loaded from settings.

---

## 3. Data Source Patterns

### Pattern 1: Page query data (PREFERRED)

Define the query at the route level. All components on the page share this data.

```json
{
  "path": "/orders/:id",
  "component": "fc.page",
  "props": {
    "title": "Order {{orderById.ref}}"
  },
  "descendants": [
    {
      "component": "fc.card.attribute",
      "dataSource": "orderById",
      "props": {
        "attributes": [
          { "label": "Ref", "value": "{{ref}}" }
        ]
      }
    }
  ],
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { ref status } }",
    "variables": { "id": "{{params.id}}" }
  },
}
```

### Pattern 2: Multiple data sources

When a page needs data from multiple queries, use `dataSources` (plural).

```json
{
  "dataSources": {
    "orders": {
      "query": "orders",
      "variables": { "retailerId": "{{activeContext.id}}" }
    },
    "products": {
      "query": "products",
      "variables": { "catalogueRef": "MASTER" }
    }
  }
}
```

Components access via their `dataSource` prop:
```json
{
  "component": "fc.card.attribute",
  "dataSource": "orders.edges.0.node",
  "props": { "attributes": [] }
}
```

### Pattern 3: Mid-tree data provider (fc.provider.graphql)

When a subtree of components needs data from a different entity than the page query, wrap them in `fc.provider.graphql`. This is a manifest-only solution — no custom code needed.

```json
{
  "component": "fc.provider.graphql",
  "props": {
    "query": "query($ref: [String]) { inventoryPositions(productRef: $ref) { edges { node { ref qty } } } }",
    "variables": { "ref": "{{product.ref}}" },
    "path": "inventoryPositions"
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "inventoryPositions",
      "props": {
        "attributes": [
          { "label": "Location", "value": "{{node.ref}}" },
          { "label": "Qty", "value": "{{node.qty}}" }
        ]
      }
    }
  ]
}
```

### Pattern 4: Component-level useQuery (LAST RESORT)

Only use in custom SDK components when page query data is insufficient. This adds an extra GraphQL request per component instance.

```typescript
const [result] = useQuery(inventoryQuery, { productRef: data.ref });
```

**Anti-pattern warning:** Never use `useQuery()` inside a component rendered per-row in `fc.list` or `fc.repeater` — this fires a separate API call PER ROW with no shared cache. Use `fc.provider.graphql` or expand the page query instead.

### Pattern 5: Context variables (no query needed)

Access current user and context without any query:

```json
{
  "retailerId": "{{activeContext.id}}",
  "username": "{{activeUser.username}}",
  "locationRef": "{{activeLocation.ref}}"
}
```

### Data scoping with dataSource

The `dataSource` prop on a component instance (outside `props`) scopes all template expressions within that component:

```json
{
  "component": "fc.card.attribute",
  "dataSource": "orderById.customer",
  "props": {
    "attributes": [
      { "label": "Name", "value": "{{firstName}}" }
    ]
  }
}
```

Here `{{firstName}}` resolves to `orderById.customer.firstName`.

---

## 4. Route Configuration

### Path parameters

```json
{
  "path": "/orders/:id",
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status } }",
    "variables": { "id": "{{params.id}}" }
  }
}
```

### Compound parameters

```json
{
  "path": "/product/:productRef/:catalogueRef",
  "data": {
    "query": "query($productRef: String!, $catalogueRef: String!) { productByRef(ref: $productRef, catalogue: { ref: $catalogueRef }) { id ref } }",
    "variables": {
      "productRef": "{{params.productRef}}",
      "catalogueRef": "{{params.catalogueRef}}"
    }
  }
}
```

### Query parameters

Filter values from URL query strings are accessed the same way:

```json
{
  "data": {
    "variables": {
      "status": "{{params.filter_status}}",
      "fromDate": "{{params.filter_from}}"
    }
  }
}
```

### Navigation menu structure

```json
{
  "navigation": {
    "items": [
      {
        "label": "i18n:fc.nav.dashboard",
        "path": "/dashboard",
        "icon": { "name": "MdDashboard" }
      },
      {
        "label": "i18n:fc.nav.orders",
        "path": "/orders",
        "icon": { "name": "MdShoppingCart" },
        "children": [
          { "label": "i18n:fc.nav.orders.all", "path": "/orders" },
          { "label": "i18n:fc.nav.orders.create", "path": "/orders/create" }
        ]
      }
    ]
  }
}
```

### Back buttons and breadcrumbs

```json
{
  "component": "fc.page",
  "props": {
    "title": "Order {{order.ref}}",
    "backButton": {
      "label": "Back to Orders",
      "path": "#/orders"
    }
  }
}
```

### Preserving URL params in links

Use triple braces to avoid HTML escaping:

```json
{
  "link": "#/inventorySearch/product/{{node.productRef}}?{{{currentUrlParams}}}"
}
```

---

## 5. Manifest Setting Rules (Non-Negotiable)

These rules apply to ALL manifest settings -- root manifests AND fragments.

| Rule                        | Value                  | Notes                                                          |
|-----------------------------|------------------------|----------------------------------------------------------------|
| `valueType`                 | `"JSON"`               | NEVER `"LOB"` or `"STRING"`. Legacy LOB settings exist but must not be replicated. |
| `context`                   | `"ACCOUNT"`            | NEVER `"RETAILER"`. Legacy RETAILER settings are anomalies.   |
| `contextId`                 | `0`                    | Always zero for account-level settings.                        |
| Body location               | `lobValue`             | NEVER `value` (255 char limit, silently truncates large JSON). |
| `lobValue` format           | Raw JSON object        | NEVER `JSON.stringify()` -- double-encoding causes "Manifest contains an invalid route" and sidebar sections disappear silently. |
| Deploy method (recommended) | `fluent module install --include settings --force` | Package in `assets/settings/manifests.json` (relative to module root). |
| Deploy method (alternative) | GraphQL `createSetting` / `updateSetting` mutations | Use same JSON/ACCOUNT/0 values. |
| NEVER use                   | MCP `setting.upsert` / `setting.upsert` | These create LOB type which Mystique cannot parse. |

### Naming convention

| Setting Type     | Pattern                                                          | Example                                                 |
|------------------|------------------------------------------------------------------|---------------------------------------------------------|
| Main manifest    | `fc.mystique.manifest.<app>`                                     | `fc.mystique.manifest.oms`                              |
| Fragment         | `fc.mystique.manifest.<app>.fragment.<name>`                     | `fc.mystique.manifest.oms.fragment.ordermanagement`     |

### Mandatory backup before modification

Before modifying any existing manifest setting:

1. Query the existing setting value via GraphQL
2. Save to `accounts/<PROFILE>/manifests/backups/<setting-name>_<YYYY-MM-DD_HHmmss>.json`
3. Include full metadata: id, name, context, contextId, valueType
4. For new settings, note "NEW -- no backup needed"

### Rollback

Read the backup file, extract the `lobValue`, and re-deploy using the same method.

---

## 6. Settings vs Manifest Decision Matrix

| Factor                       | Use Settings                                  | Use Manifest                                   |
|------------------------------|-----------------------------------------------|------------------------------------------------|
| Varies by retailer/location  | Yes                                           | No                                             |
| Changes frequently           | Yes                                           | No                                             |
| Managed by non-developers    | Yes                                           | No                                             |
| Stable UI structure          | No                                            | Yes                                            |
| Common across tenants        | No                                            | Yes                                            |
| Developer-managed            | No                                            | Yes                                            |

**Settings** are best for: API keys, feature flags, retailer-specific options, dropdown option lists, Google Maps keys, i18n language dictionaries.

**Manifest** is best for: page layouts, navigation structure, component configuration, data source queries, route definitions.

---

## 7. Common Troubleshooting

### Template not resolving

**Symptom:** Raw template text like `order.ref` appears instead of the value.

**Cause:** Missing curly braces.

```json
// WRONG
{ "value": "order.ref" }

// CORRECT
{ "value": "{{order.ref}}" }
```

### Condition returning string "false"

**Symptom:** Condition check does not work; element always shows or always hides.

**Cause:** Using a string literal instead of a template expression.

```json
// WRONG
{ "condition": "false" }

// CORRECT
{ "condition": "{{and field1 field2}}" }
```

### Link not rendering

**Symptom:** Text shows but is not clickable.

**Cause:** The `condition` is falsy or missing, or the linked entity ID is null.

```json
{
  "value": "{{customer.name}}",
  "link": "#/customer/{{customer.id}}",
  "condition": "{{and customer.id}}"
}
```

Both `link` and a truthy `condition` are required for the link to render.

### Icon not showing

**Symptom:** No icon appears beside the text.

**Cause:** Missing library prefix on icon name.

```json
// WRONG
{ "icon": { "name": "CheckCircle" } }

// CORRECT - Material Design prefix
{ "icon": { "name": "MdCheckCircle" } }

// CORRECT - Font Awesome prefix
{ "icon": { "name": "FaCheck" } }
```

### Date query with localized digits

**Symptom:** GraphQL query returns no results or wrong results when using date variables. Backend receives Arabic/Hindi digits instead of ASCII.

**Cause:** Missing `_locale='en'` and UTC flag.

```json
// WRONG
{ "createdFrom": "{{dateFormat (dateSubtract day=1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'}}" }

// CORRECT
{ "createdFrom": "{{dateFormat (dateSubtract day=1) 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' true _locale='en'}}" }
```

### Invalid GraphQL fields

**Symptom:** Query returns null for certain fields, or the entire query silently fails.

**Cause:** Fields were manually added that do not exist in the schema.

**Fix:** Use `graphql.generateFull` to discover valid fields. Never add fields not in the generated output.

### Missing `me` query for user actions

**Symptom:** User action buttons do not appear on the page, or actions fire without user context.

**Cause:** The page data source does not include the `me` query.

**Fix:** Add the `me` query to the page's data sources:
```json
{
  "dataSources": {
    "order": { "query": "orderById", "variables": { "id": "{{params.id}}" } },
    "me": { "query": "me { username primaryRetailer { id } roles { edges { node { role { name } } } } }" }
  }
}
```

Also ensure:
- The workflow actually has user actions defined at the entity's current status
- The user action includes a `modules` array that includes the current app (for OMS App use legacy alias `"adminconsole"`, for Store App use legacy alias `"servicepoint"`)
- `fc.page.props.actions` is enabled (`true` or a `PageActionConfig`)

### Attribute access patterns

**Symptom:** Attribute value is empty or undefined.

**Cause:** Trying to index into the attributes array instead of using `byName`.

```json
// WRONG
"{{order.attributes[0].value}}"

// CORRECT
"{{order.attributes.byName.vipStatus}}"
```

### Actions not showing on page

**Checklist:**
1. Is `actions` enabled on `fc.page` (`true` or a `PageActionConfig`)?
2. Does the page query return the entity (order, fulfilment, etc.) as the first result?
3. Does the workflow define user actions at the entity's current status?
4. Does the user action's `modules` array include the current app?
5. Does the user action have `confirm: true` set in its context?
6. Is the `me` query included in the page's data sources?

### Custom field type not rendering in user action form

**Symptom:** The action appears, but a custom form field is missing or renders incorrectly.

**Common causes:**

1. Workflow `attributes[].type` does not exactly match a `FieldRegistry.register()` key.
2. The plugin bundle that registers the field is not loaded in the **root** manifest's `plugins[]` (IMPORTANT: `plugins[]` goes ONLY in `fc.mystique.manifest.<app>`, NEVER in fragment settings).
3. The field contract is edited in manifest instead of workflow (manifest does not redefine form schema).

**Fix:**

- Confirm exact type string match between workflow and `FieldRegistry`.
- Confirm plugin loading and that `version.json` is reachable.
- Move field schema changes (`type`, `label`, `defaultValue`, `source`, `mandatory`) into workflow `userActions[].attributes[]`.

### OMS DOM Structure and Styling Reference

Extracted from live OMS app (v2026). Use for Tier 1 DOM injection mocks and SDK component styling.

**Font:** `Lato, sans-serif` (NOT Roboto — Lato is loaded by the OMS app theme).

**Card section DOM hierarchy:**
```
MuiGrid-root css-127zmmi (display:flex, padding:8px)     ← grid wrapper
  └── MuiPaper-root MuiCard-root (transparent bg, no border/shadow)  ← card
        └── div.css-refld5 (padding:16px)                 ← inner container
              ├── h3.MuiTypography-root.MuiTypography-h3  ← section heading
              └── div.MuiGrid-root.jss88 (repeated)       ← value rows
```

**Computed styles (match these exactly for native blending):**

| Element | Property | Value |
|---------|----------|-------|
| Card wrapper | background | `transparent` / `rgba(0,0,0,0)` |
| Card wrapper | border / box-shadow | none |
| Inner div | padding | `16px` |
| Grid wrapper | padding | `8px` |
| Heading (h3) | font | `Lato, sans-serif`, 16px, weight 700 |
| Heading (h3) | text-transform | `uppercase` |
| Heading (h3) | color | `rgba(0,0,0,0.87)` |
| Label text | font-size | `12px` |
| Label text | color | `rgb(153,153,153)` / `#999` |
| Label text | font-weight | `400` |
| Value rows | display | `flex` |
| Value rows | padding | `8px 0px 0px` |

**JSS class names** (dynamically generated, may change across versions):
- Card: `jss94 jss96`
- Value rows: `jss88`
- Grid: `css-127zmmi`

These JSS names are version-specific. Always target by MUI class (`MuiPaper-root`, `MuiTypography-h3`) or structural position (h3 → parentElement → grandparent), not by `jssXXX` numbers.

**Injection best practice:** Find sections by heading text (e.g., `querySelectorAll('h3.MuiTypography-h3')` then match `.textContent`), traverse up to grandparent for the grid wrapper, and insert siblings at that level.

### Manifest JSON syntax errors

**Symptom:** Entire page fails to load or shows blank.

**Fix:** Validate JSON syntax before deploying:
```bash
cat manifest.json | jq .
```

Check for common issues:
- Trailing commas (not allowed in JSON)
- Single quotes instead of double quotes
- Missing closing braces/brackets
- Unescaped special characters in strings

---

## 8. Custom Component Rendering Patterns (SDK)

### Card as Root Element (MANDATORY)

Custom `content` category ComponentRegistry components that sit inside `fc.page.section` MUST use `<Card>` from `mystique/components/Card` as their root JSX element. The Card component handles MUI Grid item wrapping internally.

**Why:** `fc.page.section` auto-wraps OOTB components (`fc.card.attribute`, `fc.list`) in `MuiGrid-item` divs, but does NOT wrap custom ComponentRegistry components. Without Card, custom components render at 0px/1px width.

**Standard pattern:**
```tsx
import { Card, CardWidth } from 'mystique/components/Card';
import { CardContent } from 'mystique/components/CardContent';

interface Props extends StdProps {
  title?: string;
  width?: CardWidth;
}

const MyComponent: React.FC<Props> = ({ data, title, width }) => (
  <Card title={title || 'Default Title'} width={width}>
    <CardContent>{/* padded content */}</CardContent>
  </Card>
);
```

**Edge-to-edge content (maps, tables):** Use Card without CardContent.

**Manifest configuration:**
```json
{
  "component": "ns.content.my-component",
  "width": "full",
  "props": { "title": "My Title" }
}
```

`width` at top level is a standard manifest property passed as a prop. `title` goes inside `props`.

### Category-specific root patterns

| Category | Root element | Grid handling |
|----------|-------------|---------------|
| `content` | `<Card title={title} width={width}>` | Card handles Grid item internally |
| `layout` | `<Grid item>` + `<Grid container>` + `<Children />` | Component manages grid directly |
| `page` | Any root (e.g., `<div>`) | Full page replacement, no grid context |
| `filter` | Any root | Typically inline, no grid context |

### CardWidth values

`'quarter'` (25%), `'third'` (33%), `'half'` (50%), `'two-thirds'` (67%), `'full'` (100%), or numeric 1-12 (grid columns).

### OOTB component composition (FieldRegistry → ComponentRegistry.get)

FieldRegistry fields can retrieve and render OOTB platform components via `ComponentRegistry.get()`:

```typescript
const QuantityList = ComponentRegistry.get('fc.quantity.list') as FC<QuantityListProps>;
return <QuantityList {...listProps} quantitySelectorProps={fieldProps} scannerProps={barcodeProps} />;
```

This is the pattern used by the official Returns plugin — a custom field that wraps the platform QuantityList with custom state, settings-driven config, and validation.

### List row expansion

Lists support expandable rows via `row.expansion` config. Register a content component, then reference it as expansion:

```json
{
  "component": "fc.list",
  "props": {
    "row": {
      "expansion": {
        "component": "acme.content.item-detail",
        "dataSource": "node",
        "props": { "colSpan": 6 }
      }
    }
  }
}
```

The expansion component receives the row's `node` data. Pass parent state/dispatch via props for bi-directional communication.

### Settings-driven field configuration

A single Fluent setting can control an entire field's behavior (query, columns, validation, scanner config). Pattern:

1. Define config interface with optional overrides for query, attributes, validation, scanner
2. `useSettings({ fieldConfig: 'fc.mystique.fields.myfield' })` to fetch
3. `recursiveValueDecorator(config, 'string', prefixedTranslate)` to auto-translate i18n strings
4. Apply config to drive rendering: `config?.query || defaultQuery`, `config?.attributes || defaultColumns`

This enables per-retailer customization without code changes — retailers update a Fluent setting to change field behavior. Used by the official Returns plugin.

---

## 9. Complete Page Patterns

### Order list page with filters

```json
{
  "path": "/orders",
  "component": "fc.page",
  "props": {
    "title": "i18n:fc.om.orders.title"
  },
  "descendants": [
    {
      "component": "fc.list",
      "dataSource": "orders",
      "props": {
        "attributes": [
          {
            "label": "i18n:fc.om.orders.column.ref",
            "value": "{{node.ref}}",
            "link": "#/order/{{node.id}}"
          },
          {
            "label": "i18n:fc.om.orders.column.customer",
            "value": "{{node.customer.firstName}} {{node.customer.lastName}}"
          },
          {
            "label": "i18n:fc.om.orders.column.status",
            "value": "{{i18n 'fc.om.order.status.' node.status _fallback=node.status}}"
          }
        ],
        "filters": [
          {
            "field": "status",
            "type": "select",
            "label": "i18n:fc.om.orders.filter.status"
          }
        ]
      }
    }
  ],
  "data": {
    "query": "query($retailerId: [Int!], $status: [String]) { orders(retailerId: $retailerId, status: $status) { edges { node { id ref status customer { firstName lastName } } } } }",
    "variables": {
      "retailerId": "{{activeContext.id}}",
      "status": "{{params.filter_status}}"
    }
  }
}
```

### Detail page with user actions and grid layout

```json
{
  "path": "/order/:id",
  "component": "fc.page",
  "props": {
    "title": "Order {{order.ref}}",
    "actions": true
  },
  "descendants": [
    {
      "component": "fc.grid",
      "descendants": [
        {
          "component": "fc.card.attribute",
          "dataSource": "orderById",
          "props": {
            "title": "Order Details",
            "width": "two-thirds",
            "attributes": [
              { "label": "Reference", "value": "{{ref}}" },
              { "label": "Status", "value": "{{status}}" },
              { "label": "Created", "value": "{{dateFormat createdOn}}" }
            ]
          }
        },
        {
          "component": "fc.card.attribute",
          "dataSource": "orderById.customer",
          "props": {
            "title": "Customer",
            "width": "third",
            "attributes": [
              {
                "label": "Name",
                "value": "{{firstName}} {{lastName}}",
                "link": "#/customer/{{id}}",
                "condition": "{{and id}}"
              },
              { "label": "Email", "value": "{{email}}" }
            ]
          }
        }
      ]
    },
    {
      "component": "fc.list",
      "dataSource": "orderById.items",
      "props": {
        "title": "Order Items",
        "attributes": [
          { "label": "SKU", "value": "{{node.product.ref}}" },
          { "label": "Quantity", "value": "{{node.quantity}}" },
          { "label": "Price", "value": "{{currency node.totalPrice node.currency}}" }
        ]
      }
    }
  ],
  "data": {
    "query": "query($id: ID!) { orderById(id: $id) { id ref status createdOn customer { id firstName lastName email } items { edges { node { quantity totalPrice currency product { ref } } } } } }",
    "variables": { "id": "{{params.id}}" }
  }
}
```

### Dashboard with metric tiles

```json
{
  "path": "/dashboard",
  "component": "fc.page",
  "props": {
    "title": "i18n:fc.dashboard.title"
  },
  "descendants": [
    {
      "component": "fc.grid",
      "descendants": [
        {
          "component": "fc.card.metric",
          "props": {
            "title": "Orders Today",
            "width": "quarter",
            "value": "{{ordersToday.edges.length}}",
            "icon": { "name": "MdShoppingCart" },
            "highlight": "primary"
          }
        },
        {
          "component": "fc.card.metric",
          "props": {
            "title": "Revenue Today",
            "width": "quarter",
            "value": "{{currency revenueToday 'AUD'}}",
            "icon": { "name": "MdAttachMoney" },
            "highlight": "success"
          }
        }
      ]
    }
  ],
  "dataSources": {
    "ordersToday": {
      "query": "orders",
      "variables": {
        "retailerId": "{{activeContext.id}}",
        "createdFrom": "{{dateFormat (dateSubtract day=0) 'YYYY-MM-DD[T]00:00:00.000[Z]' true _locale='en'}}"
      }
    }
  }
}
```

### Custom plugin component integration

```json
{
  "plugins": [
    {
      "type": "url",
      "src": "https://cdn.example.com/my-plugin/v1.0.0/"
    }
  ],
  "routes": [
    {
      "path": "/custom",
      "component": "fc.page",
      "props": {
        "title": "Custom View"
      },
      "descendants": [
        {
          "component": "company.MyCustomComponent",
          "dataSource": "orders",
          "props": {
            "showChart": true,
            "chartType": "bar"
          }
        }
      ],
      "data": {
        "query": "query($retailerId: [Int!]) { orders(retailerId: $retailerId) { edges { node { id ref status } } } }",
        "variables": { "retailerId": "{{activeContext.id}}" }
      }
    }
  ]
}
```

---

## 10. i18n Conventions (from OOTB Reference Plugins)

### Key naming convention

OOTB plugins follow a hierarchical dot-separated key pattern:

```
<namespace>.<app>.[ui.]<domain>.<path>.<type>
```

| Segment | Examples | Notes |
|---------|----------|-------|
| `namespace` | `fc` (OOTB), `acme` (custom) | Custom plugins MUST NOT use `fc` |
| `app` | `oms`, `store` | Target application |
| `ui` (optional) | `ui` | Used in Store plugin, omitted in OMS |
| `domain` | `orders`, `pick`, `returns` | Business domain |
| `path` | `list`, `detail.summary`, `item` | Component/section path |
| `type` | `title`, `label`, `column`, `button`, `placeholder` | UI element type |

**Examples from OOTB:**
- `fc.oms.orders.list.title` — OMS orders list page title
- `fc.store.ui.pick.item.scanned.label` — Store pick item scanned label
- `fc.oms.order.status.CREATED` — Status display value

### Usage patterns

| Context | Syntax | Example |
|---------|--------|---------|
| Manifest static labels | `i18n:` prefix | `"label": "i18n:fc.oms.nav.orders"` |
| Manifest dynamic values | `{{i18n}}` helper | `"value": "{{i18n 'fc.oms.order.status.' node.status _fallback=node.status}}"` |
| SDK component code | `translateOr()` | `translateOr(['fc.oms.orders.title', 'Orders'])` |
| SDK bold text | `{{bold}}` helper | `"{{bold (i18n 'fc.store.ui.item.label')}}"` |

### Translation file structure

OOTB plugins use **flat JSON** files (no nesting), one file per locale:

```
i18n/locales/en/translation.json
i18n/locales/fr/translation.json
```

```json
{
  "fc.oms.orders.list.title": "Orders",
  "fc.oms.orders.list.column.ref": "Reference",
  "fc.oms.orders.list.column.status": "Status",
  "fc.oms.order.detail.title": "Order {{ref}}"
}
```

Webpack `CopyWebpackPlugin` copies `i18n/locales` to `dist/i18n` for runtime loading by the host app.

---

## 11. Settings Value Format Reference

### Manifest settings (non-negotiable)

All manifest settings (`fc.mystique.manifest.*`) use: `valueType: JSON`, `context: ACCOUNT`, `contextId: 0`, body in `lobValue`.

### Business settings (dropdowns, config)

Two value format patterns exist in OOTB:

**Pattern 1 — `active` wrapper (workflow user action dropdowns):**
Required when the setting is referenced via `source: "settings.<KEY>"` in workflow user action attributes.

```json
{
  "active": [
    { "name": "Damaged", "value": "DAMAGED" },
    { "name": "Wrong Item", "value": "WRONG_ITEM" }
  ]
}
```

Note: Uses `name` key (not `label`) for the display text. The `active` wrapper is mandatory — plain arrays silently fail.

**Pattern 2 — Direct array (SDK component dropdowns):**
Used when SDK components read settings via `useSettings()` / `getSettings()`.

```json
[
  { "label": "Damaged", "value": "DAMAGED" },
  { "label": "Wrong Item", "value": "WRONG_ITEM" }
]
```

Note: Uses `label` key (not `name`). No `active` wrapper needed.

### Business settings context

Unlike manifest settings, business settings legitimately use RETAILER context when values differ per retailer:

| Setting type | Context | Example |
|---|---|---|
| Manifest settings | Always `ACCOUNT`/`0` | `fc.mystique.manifest.oms` |
| Account-wide business settings | `ACCOUNT`/`0` | Feature flags, global config |
| Retailer-specific business settings | `RETAILER`/`<id>` | `RETURN_REASON`, `CANCELLATION_REASON` |

### Naming convention

Business settings use `SCREAMING_SNAKE_CASE`: `RETURN_REASON`, `CANCELLATION_REASON`, `PACK_SETTINGS`.
