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 / Area | FE days | BE days | QA days | Total |
|---|---|---|---|---|
| Phase 1 — UI (mocked) | 3.5 | — | 1 | 4.5 |
| Phase 2 — API integration | 2 | — | 0.5 | 2.5 |
| Grand total | 5.5 | — | 1.5 | 7 |
Confidence: medium. Key assumptions: resource-lookup composable path unverified (OQ-3 — agent must
grep -r "node-resources/lookup" modules/ai-agent/);ActionIntegrationForm.vueadaptive renderer's existinghtml.elementswitch 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:
- 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. - Render
is_additional_field: truerows through the existinghtml.elementswitch; array fields (html.element: 'select multiple') useActionMultiSelectFieldconstrained toresource_types[0].items; AI mode shows a disabled control + placeholder label. - Save additional-field entries to
parameters.argumentswith theis_additional_field: trueshape from RFC §2.A. - Guard scope boundary: Custom group must be absent in
update deal/update ticketforms.
Implementation Plan
| Action | File | What changes |
|---|---|---|
| extend | modules/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 |
| create | modules/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 thechatbot-ferepository before writing any code.
Implementation steps
-
Explore the codebase — Open
modules/ai-agent/components/forms/action/ActionIntegrationForm.vue[unverified]. Read lines:95–350(approximate per RFC §2.0) to learn thehtml.elementswitch structure;:126–140for the existingActionMultiSelectFieldmount;:117–121for thedepends_onpattern. Rungrep -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. -
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 →
ActionMultiSelectFieldmounted withresource_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_nameforis_rl: truedropdown) update deal/update ticketforms → Custom group absent (snapshot); no regression on standard fields
Run
[VERIFY: test command from package.json]— confirm all new tests fail. -
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., oneselect, oneselect multiple, oneinput textdescriptor) - Template section for the Custom group below Standard fields — shell only, no logic yet
- Reactive refs:
-
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 viavi.mock/jest.mock. PopulatecustomFieldDescriptorsfromresponse.data[]. -
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.elementswitch handles rendering — no new branches needed; additional fields useis_additional_field: truein the Let AI / Set manually row toggle select multiplerows: mountActionMultiSelectFieldwith:options="descriptor.resource_types[0].items"— same pattern as dealtagsat:126-140- AI mode: when
use_ai: true, passdisabledprop + placeholder text to the control - Save handler: include
is_additional_field: true,id: descriptor.value,type: descriptor.property.type,use_ai,value, andcached_result_nameforis_rl: truefields - Scope guard: check action type before showing Custom group; add snapshot test for
update deal/update ticketform renders
- Loading: skeleton/spinner in Custom group while
-
Go green — Run
[VERIFY: test command]until all spec tests pass. -
Quality gate — Run
[VERIFY: lint + typecheck + build commands from package.json]. Confirm no newv-htmlusage touchesdisplay_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
ActionMultiSelectFieldconstrained toresource_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 ticketsnapshot 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
| Discipline | Days |
|---|---|
| Frontend | 3 |
| Backend | — |
| QA | 1 |
| Total | 4 |
Assumptions: existing
html.elementswitch requires no structural refactor — additional fields slot in as new property descriptors;ActionMultiSelectFieldreused 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
| Action | File | What 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.* |
| extend | modules/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
-
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). -
Write failing tests (red) — Add to the spec from Task 1.1: assert
ai_agent_action_custom_field_addedis called with{ action_type, field_name, field_type, is_array, mode }when a custom field is added. Assertai_agent_action_custom_field_lookup_failedis 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. -
Add i18n keys — Open the locale file. Add under the
ai_agentnamespace:"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"} -
Replace hardcoded strings — In
ActionIntegrationForm.vue, replace every Custom-group template string with its$t('ai_agent.custom_fields.*')equivalent. -
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.namefield_type, // descriptor.property.typeis_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 starterror_code, // HTTP status or 'timeout'}) -
Go green — Run test command until all pass.
-
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_addedfires with correct payload when builder adds a custom field -
ai_agent_action_custom_field_lookup_failedfires 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
| Discipline | Days |
|---|---|
| Frontend | 0.5 |
| Backend | — |
| QA | 0 |
| Total | 0.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/lookupresponses 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
| Action | File | What changes |
|---|---|---|
| extend | modules/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 |
| extend | modules/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
-
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 acceptedresource_keyvalues? Note the exact call signature used for existing keys (e.g.pipeline,stage). -
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. -
Upgrade tests to HTTP interception — In the spec file, remove
vi.mock('[composable]')stubs. Add MSW handlers (or axios-mock-adapter) forPOST /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_keyfor each action type. Run test command — confirm new integration assertions fail. - Handler 1:
-
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. -
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.
-
Quality gate —
[VERIFY: lint + typecheck + build].
Acceptance criteria
-
POST /v1/node-resources/lookupcalled with{ resource_key: 'additional_field_deal' }forqontak_crm_deal_create -
POST /v1/node-resources/lookupcalled with{ resource_key: 'additional_field_ticket' }forqontak_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
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0.5 |
| Total | 1.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/lookupreturning data foradditional_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
| Action | File | What changes |
|---|---|---|
| create | e2e/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
-
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. -
Scaffold spec — Create the spec file at the appropriate path. Define two
describeblocks:"custom fields — flag ON"(uses internal test org with flag enabled)"custom fields — flag OFF"(uses an org without the flag)
-
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 -
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) -
Go green — Run
[VERIFY: E2E command from package.json]against staging. Fix selector / timing issues; run 3× to confirm no flakiness. -
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
| Discipline | Days |
|---|---|
| Frontend | 1 |
| Backend | — |
| QA | 0 |
| Total | 1 |
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_fieldsflag 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
| Story | Reason |
|---|---|
| QACF-S03 | BE-only — no FE code; all ACs covered by BE RFC Chunk 4 (merge_additional_field_arguments) |
| QACF-S04 | No-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 |