Skip to main content

Task Breakdown — CRM Actions Custom Fields (FE)

Generated from crm-actions-custom-fields-fe.md · 2026-07-16
Slicing: horizontal (Phase 1: UI mocked → Phase 2: API integration) · Scope: full (blocked tasks included)
Repo: chatbot-fe — no local checkout; all file paths are [unverified — check repo] unless stated otherwise.


Effort Summary

Phase / AreaFE daysBE daysQA daysTotal
Phase 1 — UI (mocked)3.514.5
Phase 2 — API integration20.52.5
Grand total5.51.57

Confidence: medium. Key assumptions: resource-lookup composable path unverified (OQ-3 — agent must grep -r "node-resources/lookup" modules/ai-agent/); ActionIntegrationForm.vue adaptive renderer's existing html.element switch accepts additional-field descriptors without structural refactor; production Figma frames not yet produced (wireframes are the build reference). If OQ-3 reveals the composable needs interface changes, add +0.5 d to Task 2.1.


Phase 1 — UI (APIs mocked)

Task 1.1: [FE] Custom Group Picker + Additional Field Row Handling (QACF-S01, QACF-S02, QACF-S01-NEG)

The builder can open the Create Deal / Create Ticket config drawer, see a "Custom fields from CRM" group in the Add-field picker when the org flag is on, add any additional field (including array-type multi-select chips), and save a complete config — all verified against mocked lookup data.

Status: ✅ Actionable — full UI can be built and unit-tested with mocked responses; no BE deployment needed.

Design reference: In-repo wireframes — Screen 1 (Standard + Custom groups), Screen 2 (ArrayFieldRow — AI / manual), Screen 3 (Deal picker parity), Screen 4 (scope boundary) · DS version: [VERIFY: @mekari/mekaui version from chatbot-fe/package.json] · Design QA: [Design QA — to be assigned]

What to build

Extend ActionIntegrationForm.vue to:

  1. When nodeRegistry.settings.custom_fields_enabled === true, call the resource-lookup composable (mocked for Phase 1) and render a "Custom fields from CRM" group below Standard fields — with loading, error, empty, and success states.
  2. Render is_additional_field: true rows through the existing html.element switch; array fields (html.element: 'select multiple') use ActionMultiSelectField constrained to resource_types[0].items; AI mode shows a disabled control + placeholder label.
  3. Save additional-field entries to parameters.arguments with the is_additional_field: true shape from RFC §2.A.
  4. Guard scope boundary: Custom group must be absent in update deal / update ticket forms.

Implementation Plan

ActionFileWhat changes
extendmodules/ai-agent/components/forms/action/ActionIntegrationForm.vue [unverified — check repo]Custom group section; resource-lookup call (Phase 1: mocked); loading/error/empty states; is_additional_field row handling; array-field ActionMultiSelectField mount; AI-mode disabled state; save-shape logic; scope boundary guard
createmodules/ai-agent/components/forms/action/__tests__/ActionIntegrationForm.customFields.spec.ts [unverified — check repo; verify test-file convention from neighbors]Unit tests for all 6 states (flag off, loading, success, error, empty, scope boundary) + save-shape assertion + array field rendering + AI mode + regression snapshot for update actions

File path rule: Paths sourced from RFC §2.D and §2.0 Repo Reading Guide. Verify actual path, test-file convention (__tests__/, .spec.ts, co-located, etc.), and import alias (~/, @/, relative) from the chatbot-fe repository before writing any code.

Implementation steps

  1. Explore the codebase — Open modules/ai-agent/components/forms/action/ActionIntegrationForm.vue [unverified]. Read lines :95–350 (approximate per RFC §2.0) to learn the html.element switch structure; :126–140 for the existing ActionMultiSelectField mount; :117–121 for the depends_on pattern. Run grep -r "node-resources/lookup" modules/ai-agent/ to find the resource-lookup composable (OQ-3). Note its import alias and call signature. Open one neighboring spec file to confirm the test-file naming convention.

  2. Write failing tests (red) — Create the spec file at the path identified above. Cover:

    • custom_fields_enabled: false → no Custom group (snapshot)
    • custom_fields_enabled: true, loading state → spinner in Custom group; Standard group visible
    • lookup success → Custom group renders items from mock fixture
    • lookup empty → "No custom fields in your CRM." message
    • lookup error → error message + retry button; Standard group usable; retry re-calls composable
    • builder adds array field → ActionMultiSelectField mounted with resource_types[0].items; free-value entry not possible
    • AI mode for array field → control disabled; label matches ai_agent.custom_fields.ai_array_placeholder
    • save config → parameters.arguments.<field_name> contains { is_additional_field: true, id, type, use_ai, value } (+ cached_result_name for is_rl: true dropdown)
    • update deal / update ticket forms → Custom group absent (snapshot); no regression on standard fields

    Run [VERIFY: test command from package.json] — confirm all new tests fail.

  3. Scaffold — In ActionIntegrationForm.vue, add:

    • Reactive refs: customFieldDescriptors: Ref<PropertyDescriptor[]>, customFieldsLoading, customFieldsError
    • Computed showCustomGroup: nodeRegistry.settings?.custom_fields_enabled === true && actionType is deal_create or ticket_create
    • A fetchCustomFields() stub returning a mock array matching BE RFC §5.3 shape (e.g., one select, one select multiple, one input text descriptor)
    • Template section for the Custom group below Standard fields — shell only, no logic yet
  4. Wire state — Import the resource-lookup composable from the path found in step 1. Replace the stub with a real composable call using resource_key: 'additional_field_deal' or 'additional_field_ticket' (keyed by action type) — composable stays mocked at the spec layer via vi.mock / jest.mock. Populate customFieldDescriptors from response.data[].

  5. Implement behavior:

    • Loading: skeleton/spinner in Custom group while customFieldsLoading
    • Error: error message + retry button; retry calls fetchCustomFields(); Standard group unaffected
    • Empty: "No custom fields in your CRM." when customFieldDescriptors.length === 0
    • Field rows: for each descriptor, the existing html.element switch handles rendering — no new branches needed; additional fields use is_additional_field: true in the Let AI / Set manually row toggle
    • select multiple rows: mount ActionMultiSelectField with :options="descriptor.resource_types[0].items" — same pattern as deal tags at :126-140
    • AI mode: when use_ai: true, pass disabled prop + placeholder text to the control
    • Save handler: include is_additional_field: true, id: descriptor.value, type: descriptor.property.type, use_ai, value, and cached_result_name for is_rl: true fields
    • Scope guard: check action type before showing Custom group; add snapshot test for update deal / update ticket form renders
  6. Go green — Run [VERIFY: test command] until all spec tests pass.

  7. Quality gate — Run [VERIFY: lint + typecheck + build commands from package.json]. Confirm no new v-html usage touches display_name (XSS guard per RFC §3 Security).

Acceptance criteria

  • Custom group renders when nodeRegistry.settings.custom_fields_enabled === true; absent when flag is false/absent
  • Loading → spinner/skeleton in Custom group only; Standard group immediately usable
  • Success → Custom group shows items with property.display_name + type badge; array-type items show "Array" badge
  • Empty → "No custom fields in your CRM." in Custom group
  • Error → error message + retry button; retry re-fetches; Standard group unaffected
  • Array field renders ActionMultiSelectField constrained to resource_types[0].items; free-value entry not possible (QACF-S02/ERR-1)
  • AI mode: control disabled, placeholder reads "AI will select one or more values" (QACF-S02/AC-2)
  • Saved parameters.arguments.<field_name> = { is_additional_field: true, id, type, use_ai, value[, cached_result_name] } (QACF-S01/AC-3)
  • update deal / update ticket snapshot has no Custom group element (QACF-S01-NEG/NEG-1)
  • Standard-field config on any action type unchanged — no regression (QACF-S01-NEG)
  • All unit tests pass

Test strategy

Spec mocks the resource-lookup composable (vi.mock / jest.mock) to return a 3-item fixture: one select (is_rl: true), one select multiple (array), one input text. Asserts Custom group aria-label="Custom fields from CRM" present; ActionMultiSelectField mounted with correct :options; disabled + placeholder for AI mode. A separate describe stubs the composable to reject and asserts the error state. A snapshot describe mounts the update-deal form and asserts no Custom group node.

Effort estimate

DisciplineDays
Frontend3
Backend
QA1
Total4

Assumptions: existing html.element switch requires no structural refactor — additional fields slot in as new property descriptors; ActionMultiSelectField reused as-is; resource-lookup composable call signature matches existing usage. If OQ-3 reveals a non-standard interface: +0.5 d FE.

Run to verify

# Verify exact commands from chatbot-fe/package.json first
[VERIFY_TEST_CMD] -- modules/ai-agent/components/forms/action/ActionIntegrationForm
[VERIFY_LINT_CMD] && [VERIFY_TYPECHECK_CMD] && [VERIFY_BUILD_CMD]

Depends on

  • Nothing external — Phase 1 uses mocked lookup responses

Task 1.2: [FE] i18n Keys + Analytics Events (QACF-S01, QACF-S02)

All Custom-group user-facing strings are localized via the project's i18n system, and product analytics receive custom-field interaction events with the correct payload.

Status: ✅ Actionable — depends only on Task 1.1 (component must exist before analytics can be wired).

Design reference: n/a — no new visual surface; string values from RFC §3 Performance Requirement; event schemas from RFC §2.A Event payloads.

What to build

Add five i18n locale keys and wire two analytics tracking calls into ActionIntegrationForm.vue.

Implementation Plan

ActionFileWhat changes
extend[VERIFY: i18n locale file — e.g. src/locales/en.json or i18n/en.ts in chatbot-fe] [unverified — check repo]Add 5 keys under ai_agent.custom_fields.*
extendmodules/ai-agent/components/forms/action/ActionIntegrationForm.vue [unverified — check repo]Replace hardcoded Custom-group strings with $t(...) calls; add ai_agent_action_custom_field_added and ai_agent_action_custom_field_lookup_failed analytics events

Implementation steps

  1. Explore the codebase — Find any existing component in modules/ai-agent/ that fires an analytics event (grep -r "track\|analytics\|$emit.*track" modules/ai-agent/). Note the composable or utility name and call signature. Find the i18n locale file by searching for an existing AI agent key (grep -r "\"ai_agent\"" locales/ src/ i18n/). Confirm the nesting style (dot notation vs nested objects).

  2. Write failing tests (red) — Add to the spec from Task 1.1: assert ai_agent_action_custom_field_added is called with { action_type, field_name, field_type, is_array, mode } when a custom field is added. Assert ai_agent_action_custom_field_lookup_failed is called with { action_type, latency_ms, error_code } on lookup rejection. Assert no raw English string "Custom fields from CRM" appears as a bare template literal in the rendered component. Run test command — confirm new assertions fail.

  3. Add i18n keys — Open the locale file. Add under the ai_agent namespace:

    "custom_fields": {
    "group_label": "Custom fields from CRM",
    "empty": "No custom fields in your CRM.",
    "error": "Failed to load custom fields. Retry?",
    "loading": "Loading custom fields…",
    "ai_array_placeholder": "AI will select one or more values"
    }
  4. Replace hardcoded strings — In ActionIntegrationForm.vue, replace every Custom-group template string with its $t('ai_agent.custom_fields.*') equivalent.

  5. Wire analytics — Import the analytics composable/utility found in step 1. In the add-field handler, fire:

    track('ai_agent_action_custom_field_added', {
    action_type, // 'qontak_crm_deal_create' | 'qontak_crm_ticket_create'
    field_name, // descriptor.name
    field_type, // descriptor.property.type
    is_array, // descriptor.property.type === 'array'
    mode, // 'ai' | 'manual'
    })

    In the lookup error handler, fire:

    track('ai_agent_action_custom_field_lookup_failed', {
    action_type,
    latency_ms, // elapsed ms since fetch start
    error_code, // HTTP status or 'timeout'
    })
  6. Go green — Run test command until all pass.

  7. Quality gate[VERIFY: build command]. Confirm no missing i18n key warnings in build output.

Acceptance criteria

  • All five keys present in the locale file under ai_agent.custom_fields.*
  • No hardcoded English strings for Custom-group messages in the component template
  • ai_agent_action_custom_field_added fires with correct payload when builder adds a custom field
  • ai_agent_action_custom_field_lookup_failed fires with correct payload on lookup failure
  • Unit tests (analytics spy assertions) pass
  • Build produces no i18n-key-missing warnings

Test strategy

Spy on the analytics utility in the Task 1.1 spec. Add a describe('analytics') block: trigger the add-field handler and assert the spy was called with the exact payload shape. Trigger a lookup rejection and assert lookup_failed event with error_code matching the stubbed error.

Effort estimate

DisciplineDays
Frontend0.5
Backend
QA0
Total0.5

Assumptions: i18n locale file uses nested object structure (not flat dot-key strings); analytics composable call signature matches an existing usage in modules/ai-agent/.

Run to verify

[VERIFY_TEST_CMD] -- modules/ai-agent/components/forms/action/ActionIntegrationForm
[VERIFY_BUILD_CMD]

Depends on

  • [Task 1.1] — component structure must be in place before analytics calls can be wired

Phase 2 — API Integration

Task 2.1: [FE] Wire Real Lookup Endpoint (QACF-S01, QACF-S02)

The Custom group is powered by real POST /v1/node-resources/lookup responses from the chatbot API, with full error handling and HTTP-layer integration tests.

Status: ⚠️ Partially blocked — Phase 1 mock implementation is complete and testable. Only the live-API smoke-test and integration tests require BE Chunks 1–5 (additional_field_deal / additional_field_ticket resource keys) deployed to staging. The composable wiring and allow-list update can be done any time after Task 1.1.

Design reference: n/a — no visual change; this task removes mocks and confirms real HTTP contract.

What to build

Remove the Phase 1 mock and confirm the real resource-lookup composable call using resource_key: 'additional_field_deal' / 'additional_field_ticket'. Add or extend the composable's allow-list if one exists. Upgrade unit tests to HTTP-layer integration tests.

Implementation Plan

ActionFileWhat changes
extendmodules/ai-agent/components/forms/action/ActionIntegrationForm.vue [unverified — check repo]Remove Phase 1 mock stub; confirm composable is called with correct resource_key and organizationId cache key
extend[resource-lookup composable — path from grep] [unverified — check repo]Add additional_field_deal / additional_field_ticket to allow-list if the composable enforces one
extendmodules/ai-agent/components/forms/action/__tests__/ActionIntegrationForm.customFields.spec.ts [unverified — check repo]Replace vi.mock composable stubs with MSW / axios-mock-adapter HTTP interception; add assertions on request body and response parsing

Implementation steps

  1. Explore the codebase — Re-open the resource-lookup composable from Phase 1. Confirm: does it maintain an internal Map<resource_key, data[]> cache? Does it have an explicit allow-list of accepted resource_key values? Note the exact call signature used for existing keys (e.g. pipeline, stage).

  2. Update the composable allow-list — If an allow-list exists, add 'additional_field_deal' and 'additional_field_ticket'. If the composable accepts any string key, no change needed.

  3. Upgrade tests to HTTP interception — In the spec file, remove vi.mock('[composable]') stubs. Add MSW handlers (or axios-mock-adapter) for POST /v1/node-resources/lookup:

    • Handler 1: resource_key === 'additional_field_deal' → 200 with BE RFC §5.3 fixture
    • Handler 2: resource_key === 'additional_field_ticket' → 200 with ticket fixture
    • Handler 3: → 5xx → asserts error state
    • Handler 4: → 404 → asserts "No custom fields" state (field not yet enabled)

    Assert request body contains correct resource_key for each action type. Run test command — confirm new integration assertions fail.

  4. Remove the Phase 1 mock — In ActionIntegrationForm.vue, delete the mock stub. Confirm the real composable call is the only path. Verify cache key convention (${resource_key}:${organizationId}) matches the composable's existing pattern.

  5. Go green — Run test command until all pass. When BE Chunks 1–5 are deployed to staging: smoke-test manually — open Create Deal config for the internal test org, confirm Custom group populates with real CRM field names.

  6. Quality gate[VERIFY: lint + typecheck + build].

Acceptance criteria

  • POST /v1/node-resources/lookup called with { resource_key: 'additional_field_deal' } for qontak_crm_deal_create
  • POST /v1/node-resources/lookup called with { resource_key: 'additional_field_ticket' } for qontak_crm_ticket_create
  • Response data[] items populate the Custom group (field names match CRM definitions)
  • 404 → Custom group shows "No custom fields in your CRM."
  • 5xx / timeout → Custom group shows error + retry; Standard group unaffected
  • Resource-lookup composable accepts the two new resource keys without errors
  • HTTP-layer integration tests pass
  • Staging smoke-test: real CRM additional fields appear in Custom group for internal test org — (pending BE Chunks 1–5 on staging)

Test strategy

MSW / axios-mock-adapter intercepts POST /v1/node-resources/lookup at the HTTP level. Key assertions: correct resource_key in request body; data[0].property.is_additional_field === true drives the additional-field row branch; a 5xx rejection triggers the ai_agent_action_custom_field_lookup_failed analytics event (spy shared with Task 1.2).

Effort estimate

DisciplineDays
Frontend1
Backend
QA0.5
Total1.5

Assumptions: resource-lookup composable is open-ended (no allow-list); if an allow-list exists and requires interface refactor: +0.5 d FE. BE Chunks 1–5 on staging needed for smoke-test only — integration tests run fully mocked.

Run to verify

[VERIFY_TEST_CMD] -- modules/ai-agent/components/forms/action/ActionIntegrationForm
[VERIFY_LINT_CMD] && [VERIFY_BUILD_CMD]

Depends on

  • [Task 1.1] — UI must be complete
  • [Task 1.2] — analytics spy shared in spec
  • External: BE Chunks 1–5 deployed to staging — POST /v1/node-resources/lookup returning data for additional_field_deal / additional_field_ticket (for smoke-test step 5 only; not needed for unit/integration tests)

Task 2.2: [FE] E2E Spec — Custom Fields Config Flow (QACF-S01, QACF-S02)

The full builder flow — open Create Deal/Ticket config, Custom group loads, add an array field, save, reload → field persists — is covered by an automated end-to-end test.

Status: 🚫 Blocked — requires FE changes deployed and BE Chunks 1–5 deployed to staging and ai_agent_action_custom_fields flag enabled for the E2E test org.

Design reference: n/a — E2E spec only.

What to build

A new E2E spec covering the happy path (flag ON) and the flag-off guard: open Create Deal config, confirm Custom group loads, add an array custom field, save, reload, assert field persists.

Implementation Plan

ActionFileWhat changes
createe2e/ai-agent-custom-fields.spec.ts [unverified — check repo; E2E directory may be cypress/, playwright/, tests/e2e/, etc.]Happy path + flag-off guard for custom fields config flow

Implementation steps

  1. Explore the codebase — Find one existing E2E spec in chatbot-fe (find . -name "*.spec.ts" -path "*/e2e/*" -o -name "*.cy.ts") to learn: the runner (Playwright / Cypress), how tests authenticate, how they navigate to the AI Agent config drawer, and how fixture orgs are seeded.

  2. Scaffold spec — Create the spec file at the appropriate path. Define two describe blocks:

    • "custom fields — flag ON" (uses internal test org with flag enabled)
    • "custom fields — flag OFF" (uses an org without the flag)
  3. Implement happy-path test:

    - Log in as bot manager for internal test org
    - Navigate to AI Agent config → open a Create Deal action
    - Assert: "Custom fields from CRM" group heading visible
    - Click an array-type custom field from the Custom group
    - Assert: field row appears with use_ai: true default
    - Switch to "Set manually"
    - Assert: multi-select chip input appears
    - Save config
    - Reload the page
    - Open Create Deal config again
    - Assert: the added field row is still present with correct field name
  4. Implement flag-off guard test:

    - Log in as bot manager for org without flag
    - Navigate to AI Agent config → open a Create Deal action
    - Assert: "Custom fields from CRM" group heading absent
    - Assert: standard fields present (no regression)
  5. Go green — Run [VERIFY: E2E command from package.json] against staging. Fix selector / timing issues; run 3× to confirm no flakiness.

  6. Quality gate[VERIFY: E2E lint/typecheck if applicable].

Acceptance criteria

  • Happy path E2E: Custom group loads → array field added → config saved → field name persists on reload
  • Flag-off guard E2E: no Custom group heading; standard fields unaffected
  • E2E passes 3 consecutive runs without flakiness in CI

Test strategy

E2E authenticates as a test bot manager for the internal test org (flag ON). Assertions are DOM-observable: group heading presence, chip input mount, and field name in the config on reload. Flag-off guard uses a separate test user / org seed. No request mocking — runs against staging with real BE data.

Effort estimate

DisciplineDays
Frontend1
Backend
QA0
Total1

Assumptions: E2E runner and auth helper follow an existing pattern in chatbot-fe; no new test-org seed infrastructure needed; staging with flag-enabled org is available before E2E authoring begins.

Run to verify

[VERIFY_E2E_CMD] e2e/ai-agent-custom-fields.spec.ts

Depends on

  • [Task 2.1] — real endpoint must be wired
  • External: ai_agent_action_custom_fields flag enabled for E2E test org; BE Chunks 1–5 fully deployed to staging

Ordering rationale

  • Task 1.1 first — it is the entire core deliverable; every other task depends on or augments it. Can start immediately.
  • Task 1.2 after 1.1 — i18n/analytics augment the same component; easier once the component's string and event sites are settled.
  • Task 2.1 after 1.1 + 1.2 — replaces Phase 1 mocks with real HTTP calls; the composable allow-list change can be authored before BE staging is ready, but the smoke-test step needs BE Chunks deployed. Key external dependency to push on: BE Chunk 3 (lookup endpoint with new resource keys) — that unblocks 2.1's smoke-test independently of the flag/seed work.
  • Task 2.2 last — E2E requires both FE (Tasks 1.1–2.1) deployed and BE staging + flag ON; nothing to unblock this until 2.1 is green and staging is ready.
  • Critical path: BE Chunks 1–5 on staging is the single external gate. Push Chunk 3 first (new resource keys on lookup endpoint) to unblock Task 2.1 integration testing as early as possible.

Skipped stories

StoryReason
QACF-S03BE-only — no FE code; all ACs covered by BE RFC Chunk 4 (merge_additional_field_arguments)
QACF-S04No-op — CRM additional fields are org-level (BE RFC Decision 3.7); no pipeline-change pruning to implement; QACF-S04/AC-1 and AC-2 are met trivially without any code