Implement fields management features - #55
Conversation
- Added new fields module with CRUD operations for custom fields. - Introduced tools for creating, updating, archiving, and listing fields. - Integrated field management into existing entities (companies, contacts, deals) for enhanced data handling. - Updated contracts and services to support bulk operations for fields. - Enhanced error handling and validation for field operations.
There was a problem hiding this comment.
36 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/agent/agent-trigger.service.ts">
<violation number="1" location="apps/api/src/agent/agent-trigger.service.ts:79">
P2: The dedup guard for field-backfill tasks matches only on the field key and ignores the record type. Field keys are unique per entity, not globally, so a pending backfill for a COMPANY field like "website" will suppress the backfill for a same-key CONTACT field, which then never gets filled. Consider including the entity in the task's reason (and the dedup prefix) so backfills are distinguished across record types.</violation>
</file>
<file name="apps/app/components/crm/fields/fields-entity.ts">
<violation number="1" location="apps/app/components/crm/fields/fields-entity.ts:21">
P3: kindOf is exported but never called anywhere in the repository, making it dead code. It pairs with entityOf (used in fields-sheet.tsx), so either wire up its eventual consumer or drop it until then to keep the fields module surface free of unused exports.</violation>
</file>
<file name="packages/ui/src/components/data-table.tsx">
<violation number="1" location="packages/ui/src/components/data-table.tsx:475">
P2: Bulk selection is unavailable below the `sm` breakpoint because the header, row, and subrow selection cells all use `hidden sm:table-cell`, leaving mobile users with no way to select records or invoke the new bulk actions. Keep the selection column visible on mobile, or provide an equivalent mobile selection control.</violation>
</file>
<file name="apps/app/components/crm/bulk-actions.tsx">
<violation number="1" location="apps/app/components/crm/bulk-actions.tsx:48">
P3: Partial bulk results display incorrect grammar when exactly one item fails (`1 were left alone`); make the failed-record wording singular/plural-aware.</violation>
</file>
<file name="apps/app/components/crm/fields/fields-list.tsx">
<violation number="1" location="apps/app/components/crm/fields/fields-list.tsx:145">
P2: A failed fields-list request is presented as “No custom fields yet,” which can hide existing fields and offers creation while the data is unavailable. Render the query error with a retry path before treating an empty successful response as an empty configuration.</violation>
<violation number="2" location="apps/app/components/crm/fields/fields-list.tsx:173">
P2: When the last live custom field is archived, `live.length` drops to 0 and the empty state renders, but the `ARCHIVED_ROW` disclosure (the only place Restore buttons live) is nested inside the `live.length > 0` branch. The result: once the final field of an entity is archived, archived fields become unreachable from the list UI — the user can never restore them through this panel (only by creating another live field). Move the archived disclosure outside the `live.length === 0` conditional so Restore remains accessible regardless of how many live fields remain.</violation>
</file>
<file name="apps/agent/agent/tools/list_fields.ts">
<violation number="1" location="apps/agent/agent/tools/list_fields.ts:5">
P2: Adding this tool makes `apps/app`'s “every tool has a line of English” test fail because `list_fields` has no entry in `TOOL_VERBS`. Adding the corresponding human-readable verb in `apps/app/lib/agent-transcript.ts` would keep the transcript contract and test passing.</violation>
</file>
<file name="apps/app/components/crm/fields/record-fields.tsx">
<violation number="1" location="apps/app/components/crm/fields/record-fields.tsx:112">
P2: A SELECT field remains interactive with no pending indicator while its update is in flight, so rapid changes can enqueue multiple mutations and leave an earlier choice persisted after responses complete out of order. Extend `InlineSelectField` with a saving/disabled prop and pass `busy` here before allowing another selection.</violation>
<violation number="2" location="apps/app/components/crm/fields/record-fields.tsx:132">
P2: USER custom fields render as a free-form text input and show opaque workspace user IDs, so users cannot select or identify the member assigned to the field. Add a USER-specific picker backed by workspace user options, or explicitly make USER fields read-only instead of sending them through the generic text fallback.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/contacts/contacts-table.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/contacts/contacts-table.tsx:163">
P2: A USER custom field configured to appear in the contacts table displays an opaque workspace-user ID instead of the member's name because this integration routes it through a renderer with no USER-specific display handling. Resolving the ID to a workspace user and rendering the member label would make the new column usable.</violation>
</file>
<file name="apps/api/src/deals/deals.service.ts">
<violation number="1" location="apps/api/src/deals/deals.service.ts:274">
P1: A failed deal update can still change its custom fields because this call is not part of the later `deal.update` transaction, and `writeValues` commits values one at a time. Passing a transaction client and performing the field writes together with the deal update would keep the update atomic.</violation>
</file>
<file name="apps/api/src/contacts/contacts.contracts.ts">
<violation number="1" location="apps/api/src/contacts/contacts.contracts.ts:58">
P2: Empty-string company IDs are accepted by this bulk contract but become a foreign-key database error instead of a client validation error. Requiring a non-empty string when the value is not `null` would preserve `null` as the clear value and reject malformed requests early.</violation>
</file>
<file name="packages/db/prisma/schema.prisma">
<violation number="1" location="packages/db/prisma/schema.prisma:540">
P2: Record detail reads will scan `fieldValue` by company/contact/deal ID because each new unique index puts `fieldId` first; adding standalone indexes for the three record-ID columns would keep custom-field reads bounded as data grows.</violation>
<violation number="2" location="packages/db/prisma/schema.prisma:546">
P3: The `@@index([optionId])` index on `FieldValue` is present in the schema and migration but is not in the design document's model definition in `docs/plan/dynamic-fields.md §3`, which says to copy the models verbatim. While a useful index for FK lookups, any intentional addition to the canonical model should be reflected in the design record so the two sources of truth don't drift.</violation>
</file>
<file name="apps/app/components/crm/inline-field.tsx">
<violation number="1" location="apps/app/components/crm/inline-field.tsx:45">
P3: Prefer `Object.keys(fields).includes(key)` over `key in fields` for consistency with `savingField`. The `in` operator also matches inherited Object.prototype properties, so a custom field named e.g. "toString" or "constructor" would be incorrectly reported as saving.</violation>
</file>
<file name="apps/app/lib/trpc/cache.ts">
<violation number="1" location="apps/app/lib/trpc/cache.ts:116">
P2: An entity-scoped field change invalidates every CRM list and search query because `listKeys()` is unconditional here. Selecting only the corresponding entity list when `entity` is provided would avoid unrelated active tables and quick-switcher requests while retaining the all-entity behavior for `cache.fields()`.</violation>
</file>
<file name="packages/db/prisma/migrations/20260806140000_dynamic_fields/migration.sql">
<violation number="1" location="packages/db/prisma/migrations/20260806140000_dynamic_fields/migration.sql:50">
P2: `USER` custom fields can contain nonexistent or deleted user IDs, so records can display stale/unresolvable assignments. A foreign key to `user(id)` with the appropriate delete behavior, or equivalent existence validation, would preserve the meaning of this field type.</violation>
</file>
<file name="packages/db/src/fields.ts">
<violation number="1" location="packages/db/src/fields.ts:64">
P2: Removing a select option makes existing selected values unreadable: serialization drops the archived option, but `readValue` still returns its ID, so record and table renderers cannot find a label. Preserve the selected archived option for reads while rejecting it in `coerceValue`, or resolve its label separately.</violation>
<violation number="2" location="packages/db/src/fields.ts:125">
P2: `coerceValue` falls through to `new Date(raw)` for any non-ISO date string (anything that doesn't match `/^\d{4}-\d{2}-\d{2}$/`). The behavior of `Date.parse` / `new Date(string)` for non-standard formats like `"2027/03/31"`, `"03-31-2027"`, or `"31/03/2027"` is explicitly implementation-dependent per the ECMAScript spec and differs across Node, Bun, and browser runtimes. A value that parses cleanly in development could silently give a wrong date (or `Invalid Date`) in production. The `DATE` field type should only accept ISO 8601 date strings (`YYYY-MM-DD`) and reject anything that doesn't match, rather than hoping the runtime agrees with whatever format arrives.</violation>
<violation number="3" location="packages/db/src/fields.ts:228">
P1: Bulk record updates can leave custom-field changes committed even when a later field or the parent record update fails. Coerce and validate the complete batch before writing, then run field writes in the same transaction as the entity update.</violation>
</file>
<file name="apps/app/components/crm/fields/field-editor.tsx">
<violation number="1" location="apps/app/components/crm/fields/field-editor.tsx:129">
P2: After the "Fill the rest" backfill succeeds, nothing invalidates or refetches the coverage query, so the "Filled on X of Y" counters stay stale for the rest of the open session (they only refresh on remount/window-focus). Backfill is asynchronous on the server (`backfill()` returns `{ queued: true }` and the agent does the work in the background), so the number legitimately lags — but the UI should at least be given a chance to update after the mutation completes rather than never being invalidated. Consider calling `cache.fields()` (or navigating the coverage query key) in the backfill `onSuccess` alongside the toast.</violation>
<violation number="2" location="apps/app/components/crm/fields/field-editor.tsx:155">
P2: Fully covered fields still offer an active `Fill the rest` action, and clicking it queues an agent backfill despite there being no records left to fill. Disable the button when `filled >= total` so the action and queue state match the coverage shown.</violation>
<violation number="3" location="apps/app/components/crm/fields/field-editor.tsx:263">
P2: Screen-reader users cannot identify the agent-fill switch, type picker, or individual option inputs because their visible text is not associated with the controls. Associating each control with its visible label, or supplying an equivalent accessible name, would make the editor operable without sight.</violation>
</file>
<file name="apps/api/src/fields/fields.contracts.ts">
<violation number="1" location="apps/api/src/fields/fields.contracts.ts:64">
P2: Record updates accept objects and arrays and can persist them as misleading scalar text such as `[object Object]`. Restrict field values to the supported scalar union before the definition-specific coercion runs.</violation>
</file>
<file name="docs/plan/dynamic-fields-build.md">
<violation number="1" location="docs/plan/dynamic-fields-build.md:175">
P3: The nuqs snippet contains inline code comments, but AGENTS.md ("Never add code comments. Not to new code, not to code you edit.") and §9 ("Do not add code comments") both forbid them. Since §0 tells the agent to follow AGENTS.md and the snippet is copied into `record-stack.ts`, the plan instructs an action that conflicts with its own rules — drop the trailing comments from the pasted lines.</violation>
<violation number="2" location="docs/plan/dynamic-fields-build.md:176">
P3: The `field` URL param reuses `"new"` as the create sentinel while §4 derives field keys from the label via `fieldKeyFromLabel`. A custom field labeled e.g. "New" would slug to key `new`, so its editor screen could never open — `field=new` always routes to the create form instead. Reserve a non-colliding sentinel (e.g. `field=__new__` or a separate `mode` param) or exclude the reserved key from auto-derivation.</violation>
</file>
<file name="apps/api/src/contacts/contacts.service.ts">
<violation number="1" location="apps/api/src/contacts/contacts.service.ts:379">
P1: Contact updates can leave custom field values behind when the standard contact update fails. Applying values through the root database before the existing transaction makes both multi-field validation and contact changes non-atomic; move `applyValues` inside that transaction and pass it `tx`.</violation>
<violation number="2" location="apps/api/src/contacts/contacts.service.ts:435">
P2: Bulk owner assignment returns a server error for an empty-string owner ID instead of treating it as unassigned or rejecting it cleanly: `requireOwner` skips validation, then this line writes `ownerId: ""` and violates the user foreign key. Normalizing the value to `null` here would match the single-record update behavior.</violation>
</file>
<file name="apps/api/test/fields.spec.ts">
<violation number="1" location="apps/api/test/fields.spec.ts:9">
P3: The suite is order-dependent and shares mutable state (companyId, `queued`, and field values) across tests, so a failure or reorder in an earlier case cascades into confusing downstream failures and individual tests can't run in isolation. Consider isolating state per test (e.g. fresh company per `it`) or making the shared fixture explicit so the dependency is visible; a non-shared approach makes the suite more robust and self-contained for future edits.</violation>
</file>
<file name="packages/ui/src/hooks/use-table-selection.ts">
<violation number="1" location="packages/ui/src/hooks/use-table-selection.ts:22">
P2: The selection state is inconsistent between what is persisted and what is surfaced to consumers: `picked` retains selections across page changes (so `clear()` and `has()` operate on the full set), but `ids`, `count`, `allSelected`, and `someSelected` are all derived by filtering `picked` against the *current* `rowIds`. As a result, after a user selects rows on page 1 and moves to page 2, `count` drops to 0, the selection toolbar disappears, and the bulk-actions `ids` passed to mutations (e.g. `selection.ids` in the company/deal/contact bulk-action components) only cover the currently rendered page — even though the page-1 rows silently remain in `picked`. This makes it impossible to run a bulk operation over a multi-page selection and lets the reported count diverge from what the user actually selected. Consider either exposing the full `picked` set (and scoping only the header's `allSelected` to the current page) or explicitly dropping cross-page persistence so `ids`/`count` always reflect exactly what a bulk action will operate on.</violation>
</file>
<file name="apps/app/app/(app)/[slug]/companies/companies-bulk-actions.tsx">
<violation number="1" location="apps/app/app/(app)/[slug]/companies/companies-bulk-actions.tsx:78">
P3: The two new bulk-action files for companies and contacts are near-identical, and the deals file repeats the same bulk scaffolding (owner mutation, delete dialog, pending flag, onSuccess pointing at the cache). Since the common pieces (BulkActionsMenu, BulkOwnerMenu, BulkDeleteDialog, reportBulk) already live in components/crm/bulk-actions.tsx, the repeated per-entity wiring is now duplicated in three places. Any fix to the delete/enrich/owner flow will have to be applied three times, and the near-copy files drift easily. Consider extracting a small per-kind helper (e.g. a hook that wires bulkDelete + confirming/BulkDeleteDialog + pending given a cache kind) to keep the three menus focused on their one or two distinct actions.</violation>
</file>
<file name="apps/api/src/fields/fields.service.ts">
<violation number="1" location="apps/api/src/fields/fields.service.ts:144">
P2: update can produce an unusable SELECT field. `create` requires at least one option for a SELECT type (`input.options.length === 0` throws), but `update` only validates when `data.options` is explicitly present. If a caller switches an existing TEXT/NUMBER field to `type: "SELECT"` without passing options, the update is accepted and leaves a select field with zero options — which then rejects every value write via coerceValue ("...has no option"). Align the update guard with create so switching to SELECT without any option fails, e.g. treat a missing/empty `options` array when the merged type is SELECT as an error.</violation>
<violation number="2" location="apps/api/src/fields/fields.service.ts:233">
P2: archive/restore/delete convert every Prisma failure into a 404. The `.catch(() => { throw new NotFoundException(...) })` swallows all errors, not just "record not found" (P2025), so a transient DB/connection or constraint error is reported to the client as "That field does not exist." This hides real backend failures and makes them indistinguishable from a genuine missing record. Consider checking for the specific P2025 error code (or Prisma's not-found result) and letting other errors propagate so they reach logs/monitoring.</violation>
</file>
<file name="apps/api/test/bulk.spec.ts">
<violation number="1" location="apps/api/test/bulk.spec.ts:53">
P3: The doomed company is created with domain `doomed-${domain}`, which clean()'s exact-domain filter never matches, so if that test fails before bulkDelete removes it, an orphan company (and its deals/activities) persists and accumulates across runs. Suggest deleting companies by domain endsWith or listing ids from `doomed-${domain}` too.</violation>
</file>
<file name="packages/ui/src/components/sortable-list.tsx">
<violation number="1" location="packages/ui/src/components/sortable-list.tsx:54">
P2: After a drag, the list snaps back to the prop order while the reorder mutation is pending because this component does not retain the new order locally. Keeping local ordered IDs and syncing them when the `ids` prop changes would preserve the dragged order during the request.</violation>
<violation number="2" location="packages/ui/src/components/sortable-list.tsx:102">
P2: The drag handle can submit a containing form instead of only initiating a drag because it is a native button without `type="button"`. Setting the handle type explicitly prevents this reusable component from triggering unrelated form submission.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| .map((definition) => [definition.key, definition]), | ||
| ); | ||
|
|
||
| for (const [key, input] of Object.entries(values)) { |
There was a problem hiding this comment.
P1: Bulk record updates can leave custom-field changes committed even when a later field or the parent record update fails. Coerce and validate the complete batch before writing, then run field writes in the same transaction as the entity update.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/fields.ts, line 228:
<comment>Bulk record updates can leave custom-field changes committed even when a later field or the parent record update fails. Coerce and validate the complete batch before writing, then run field writes in the same transaction as the entity update.</comment>
<file context>
@@ -0,0 +1,253 @@
+ .map((definition) => [definition.key, definition]),
+ );
+
+ for (const [key, input] of Object.entries(values)) {
+ const definition = byKey.get(key);
+
</file context>
| const suffix = process.env.TEST_RUN_ID ?? "fields-spec"; | ||
| const domain = `fields-${suffix}.test`; | ||
|
|
||
| const queued: { key: string; reason: string }[] = []; |
There was a problem hiding this comment.
P3: The suite is order-dependent and shares mutable state (companyId, queued, and field values) across tests, so a failure or reorder in an earlier case cascades into confusing downstream failures and individual tests can't run in isolation. Consider isolating state per test (e.g. fresh company per it) or making the shared fixture explicit so the dependency is visible; a non-shared approach makes the suite more robust and self-contained for future edits.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/test/fields.spec.ts, line 9:
<comment>The suite is order-dependent and shares mutable state (companyId, `queued`, and field values) across tests, so a failure or reorder in an earlier case cascades into confusing downstream failures and individual tests can't run in isolation. Consider isolating state per test (e.g. fresh company per `it`) or making the shared fixture explicit so the dependency is visible; a non-shared approach makes the suite more robust and self-contained for future edits.</comment>
<file context>
@@ -0,0 +1,221 @@
+const suffix = process.env.TEST_RUN_ID ?? "fields-spec";
+const domain = `fields-${suffix}.test`;
+
+const queued: { key: string; reason: string }[] = [];
+
+const agent = {
</file context>
| }), | ||
| ); | ||
|
|
||
| const pending = assignOwner.isPending || enrich.isPending || remove.isPending; |
There was a problem hiding this comment.
P3: The two new bulk-action files for companies and contacts are near-identical, and the deals file repeats the same bulk scaffolding (owner mutation, delete dialog, pending flag, onSuccess pointing at the cache). Since the common pieces (BulkActionsMenu, BulkOwnerMenu, BulkDeleteDialog, reportBulk) already live in components/crm/bulk-actions.tsx, the repeated per-entity wiring is now duplicated in three places. Any fix to the delete/enrich/owner flow will have to be applied three times, and the near-copy files drift easily. Consider extracting a small per-kind helper (e.g. a hook that wires bulkDelete + confirming/BulkDeleteDialog + pending given a cache kind) to keep the three menus focused on their one or two distinct actions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/companies/companies-bulk-actions.tsx, line 78:
<comment>The two new bulk-action files for companies and contacts are near-identical, and the deals file repeats the same bulk scaffolding (owner mutation, delete dialog, pending flag, onSuccess pointing at the cache). Since the common pieces (BulkActionsMenu, BulkOwnerMenu, BulkDeleteDialog, reportBulk) already live in components/crm/bulk-actions.tsx, the repeated per-entity wiring is now duplicated in three places. Any fix to the delete/enrich/owner flow will have to be applied three times, and the near-copy files drift easily. Consider extracting a small per-kind helper (e.g. a hook that wires bulkDelete + confirming/BulkDeleteDialog + pending given a cache kind) to keep the three menus focused on their one or two distinct actions.</comment>
<file context>
@@ -0,0 +1,115 @@
+ }),
+ );
+
+ const pending = assignOwner.isPending || enrich.isPending || remove.isPending;
+
+ return (
</file context>
| @@index([fieldId, text]) | ||
| @@index([fieldId, number]) | ||
| @@index([fieldId, date]) | ||
| @@index([optionId]) |
There was a problem hiding this comment.
P3: The @@index([optionId]) index on FieldValue is present in the schema and migration but is not in the design document's model definition in docs/plan/dynamic-fields.md §3, which says to copy the models verbatim. While a useful index for FK lookups, any intentional addition to the canonical model should be reflected in the design record so the two sources of truth don't drift.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/prisma/schema.prisma, line 546:
<comment>The `@@index([optionId])` index on `FieldValue` is present in the schema and migration but is not in the design document's model definition in `docs/plan/dynamic-fields.md §3`, which says to copy the models verbatim. While a useful index for FK lookups, any intentional addition to the canonical model should be reflected in the design record so the two sources of truth don't drift.</comment>
<file context>
@@ -452,6 +454,99 @@ model DealContact {
+ @@index([fieldId, text])
+ @@index([fieldId, number])
+ @@index([fieldId, date])
+ @@index([optionId])
+ @@map("fieldValue")
+}
</file context>
- Updated AgentTriggerService to include entity in field backfill method for improved logging and task management. - Refactored CompaniesService, ContactsService, and DealsService to utilize transactions for field updates, ensuring consistency and error handling. - Modified field handling in FieldsService to support new field value types and improved error translation. - Enhanced test cases to cover new field management features and ensure robust functionality. - Updated UI components to reflect changes in field management and improve user experience.
There was a problem hiding this comment.
2 issues found across 30 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/components/crm/fields/field-columns.tsx">
<violation number="1" location="apps/app/components/crm/fields/field-columns.tsx:63">
P2: Select custom-field columns now display their stored option IDs instead of the configured labels because the renderer's option lookup was replaced by the user map. Preserve the SELECT option lookup while adding the USER lookup.</violation>
</file>
<file name="apps/api/src/deals/deals.service.ts">
<violation number="1" location="apps/api/src/deals/deals.service.ts:319">
P2: Deal field updates are not fully transaction-consistent because `applyValues(tx, ...)` reads definitions through the base database client rather than the transaction client. Passing `tx` through the definition lookup would prevent concurrent field metadata changes from racing validation and value persistence.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| hideBelow: "lg" as const, | ||
| cell: (row: Row) => ( | ||
| <span className="truncate"> | ||
| {render(field.type, row.fields[field.key] ?? null, byId)} |
There was a problem hiding this comment.
P2: Select custom-field columns now display their stored option IDs instead of the configured labels because the renderer's option lookup was replaced by the user map. Preserve the SELECT option lookup while adding the USER lookup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/components/crm/fields/field-columns.tsx, line 63:
<comment>Select custom-field columns now display their stored option IDs instead of the configured labels because the renderer's option lookup was replaced by the user map. Preserve the SELECT option lookup while adding the USER lookup.</comment>
<file context>
@@ -53,9 +60,9 @@ export function useFieldColumns<Row extends WithFields>(
cell: (row: Row) => (
<span className="truncate">
- {render(field.type, row.fields[field.key] ?? null, field.options)}
+ {render(field.type, row.fields[field.key] ?? null, byId)}
</span>
),
</file context>
| where: { id }, | ||
| data, | ||
| select: { id: true, name: true }, | ||
| return await this.db.$transaction(async (tx) => { |
There was a problem hiding this comment.
P2: Deal field updates are not fully transaction-consistent because applyValues(tx, ...) reads definitions through the base database client rather than the transaction client. Passing tx through the definition lookup would prevent concurrent field metadata changes from racing validation and value persistence.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/deals/deals.service.ts, line 319:
<comment>Deal field updates are not fully transaction-consistent because `applyValues(tx, ...)` reads definitions through the base database client rather than the transaction client. Passing `tx` through the definition lookup would prevent concurrent field metadata changes from racing validation and value persistence.</comment>
<file context>
@@ -320,10 +316,16 @@ export class DealsService {
- where: { id },
- data,
- select: { id: true, name: true },
+ return await this.db.$transaction(async (tx) => {
+ if (input.fields) {
+ await this.fields.applyValues(tx, "DEAL", id, input.fields);
</file context>
# Conflicts: # apps/api/src/agent/agent-trigger.service.ts # apps/api/src/companies/companies.service.ts # apps/api/src/contacts/contacts.service.ts # apps/api/src/deals/deals.contracts.ts # apps/api/src/deals/deals.router.ts # apps/api/src/deals/deals.service.ts # apps/api/src/fields/fields.contracts.ts # apps/api/src/fields/fields.service.ts # apps/api/src/generated/server.ts # apps/api/test/bulk.spec.ts # apps/api/test/fields.spec.ts # apps/app/components/crm/bulk-actions.tsx # apps/app/components/crm/fields/field-columns.tsx # apps/app/components/crm/fields/field-editor.tsx # apps/app/components/crm/fields/fields-copy.ts # apps/app/components/crm/fields/fields-list.tsx # apps/app/components/crm/fields/record-fields.tsx # apps/app/components/crm/inline-field.tsx # apps/app/components/crm/record-sheet/company-sheet.tsx # apps/app/components/crm/record-sheet/contact-sheet.tsx # apps/app/components/crm/record-sheet/deal-sheet.tsx # apps/app/lib/trpc/cache.ts # docs/plan/dynamic-fields-build.md # docs/plan/dynamic-fields.md # packages/db/prisma/schema.prisma # packages/db/src/fields-shape.ts # packages/db/src/fields.ts # packages/db/test/fields.spec.ts # packages/ui/src/components/data-table.tsx # packages/ui/src/components/sortable-list.tsx # packages/ui/src/hooks/use-table-selection.ts
Summary by cubic
Adds dynamic custom fields across companies, contacts, and deals with stricter validation, transactional record updates, and an improved UI. Select backfills are now queued per entity, tables show select labels, and inline editing supports the
USERfield.New Features
recordFieldValuesinput; ISO 8601 date parsing;USERfield validates workspace members; tables return select labels; archive/restore/delete translate not-found errors; record updates writefieldsinside a transaction; backfill usesAgentTriggerService.fieldBackfill(entity, key, reason).USERselect with loading; table columns renderUSERvia Owner cells; smoother drag reordering and selection persistence.fields(entity)invalidates record lists and detail queries; addedfieldCoveragecache.newkey added; rationale for new indexes anduserIdFK; expanded tests for dates, user validation, retired select options, and transactional rollbacks.Migration
fieldValuerecord indexes and auserIdFK; includes cleanup of invaliduserIdvalues.@dnd-kit/core,@dnd-kit/sortable,@dnd-kit/modifiers,@dnd-kit/utilitiesand rebuild.Written for commit 68c5711. Summary will update on new commits.