Lewis/contact and currencies - #56
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.
- Updated README to reflect the new name and purpose of the CRM. - Introduced new input types and mutations for managing deal contacts, including attaching, detaching, and setting roles for contacts on deals. - Implemented service methods to handle contact options and role assignments. - Added tests for the new deal contact functionalities to ensure proper behavior. - Updated API documentation to include new endpoints for deal contact management.
There was a problem hiding this comment.
38 issues found across 82 files
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/lib/trpc/cache.ts">
<violation number="1" location="apps/app/lib/trpc/cache.ts:96">
P2: After a contact or company bulk delete, open attach-contact and contact-assignment controls can still offer the deleted record, leading to a stale selection and a `NotFoundException`/invalid assignment. Adding the corresponding `deals.contactOptions` or `companies.options` query key to the removal invalidation set would refresh these controls.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:17">
P3: The new tagline "an open source, CRM designed" has a misplaced comma that breaks the noun phrase — it should be "an open source CRM" (or "an open-source CRM"). The line also repeats "CRM" (Comp AI CRM ... CRM designed) immediately under the "CRM" h1 and is restated by the following "Agentic-first CRM." line, so consider tightening wording.</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 operations render ungrammatical feedback when exactly one record fails (`1 were left alone`); use singular wording for `failed === 1` so the toast remains clear.</violation>
</file>
<file name="packages/ui/src/components/checkbox.tsx">
<violation number="1" location="packages/ui/src/components/checkbox.tsx:25">
P3: Picking the icon from the controlled `checked` prop breaks the uncontrolled case: when a consumer uses `defaultChecked="indeterminate"` (supported by Radix), `props.checked` is `undefined` so the indicator renders a checkmark even though the box shows the indeterminate styling and data-state. Consider driving the icon from the rendered data-state (e.g. a data-[state=indeterminate] selector on a nested icon) so it matches whatever state Radix actually applies.</violation>
</file>
<file name="apps/app/components/crm/record-sheet/record-stack.ts">
<violation number="1" location="apps/app/components/crm/record-sheet/record-stack.ts:36">
P2: A stale or hand-edited `field` URL opens the create form and can create a new field when saved, because any non-empty string is treated as an editor key but a missing lookup is treated as new. Validating the key against the loaded field list (and allowing create mode only for `field=new`) would prevent accidental mutations from invalid links.</violation>
</file>
<file name="apps/agent/agent/tools/archive_field.ts">
<violation number="1" location="apps/agent/agent/tools/archive_field.ts:6">
P2: The new tool is not registered in the repository's telemetry and transcript tool maps, so its calls are bucketed as `other` and the authored-tool coverage tests fail. Adding `archive_field` to `AGENT_TOOLS` and `TOOL_VERBS` alongside this tool would preserve per-tool metrics and human-readable transcript entries.</violation>
</file>
<file name="apps/api/src/agent/agent-trigger.service.ts">
<violation number="1" location="apps/api/src/agent/agent-trigger.service.ts:73">
P1: Backfills for same-named fields on different record types are collapsed into one task, and the worker cannot tell which entity to scan. Include the entity in the method signature and task identity/reason so each `(entity, key)` field gets its own task.</violation>
<violation number="2" location="apps/api/src/agent/agent-trigger.service.ts:86">
P2: Concurrent field creation, updates, or manual backfill requests can enqueue duplicate work for the same field, causing repeated research and breaking the one-task-per-field guarantee. Make the deduplication atomic with a unique task identity/upsert or an appropriate transaction/lock.</violation>
</file>
<file name="apps/api/src/companies/companies.service.ts">
<violation number="1" location="apps/api/src/companies/companies.service.ts:324">
P1: A failed company update can still persist custom-field changes, and a multi-field request can persist only its earlier fields, leaving the record in a partially updated state. Applying the field writes and company update in one transaction would make the update atomic.</violation>
</file>
<file name="packages/ui/src/components/sortable-list.tsx">
<violation number="1" location="packages/ui/src/components/sortable-list.tsx:102">
P2: If a `SortableItem` is rendered inside a form, clicking its drag handle submits the form because this activator has no explicit button type. Setting `type="button"` keeps reordering from triggering form submission.</violation>
<violation number="2" location="packages/ui/src/components/sortable-list.tsx:106">
P2: On touch devices, dragging from this handle can be interpreted as vertical scrolling instead of starting the sort. Adding `touch-none` to the activator prevents native touch panning from cancelling the PointerSensor gesture.</violation>
</file>
<file name="packages/ui/src/components/data-table.tsx">
<violation number="1" location="packages/ui/src/components/data-table.tsx:544">
P2: During a page or filter update, selection can temporarily operate on different rows from those displayed because the table renders `deferredRows` while the selection state tracks immediate `rows`. Align the selection row IDs with the deferred rows or avoid deferring rendered rows when selection is enabled to prevent selecting unseen records during the transition.</violation>
<violation number="2" location="packages/ui/src/components/data-table.tsx:576">
P2: On viewports below `sm`, the new selection cells are hidden, so Contacts, Companies, and Deals users cannot select a row or start their bulk actions on mobile. Keeping the selection column visible, or providing an alternate mobile selection control, would preserve this feature at the DataTable’s mobile breakpoint.</violation>
</file>
<file name="apps/app/components/crm/fields/field-editor.tsx">
<violation number="1" location="apps/app/components/crm/fields/field-editor.tsx:124">
P2: Editing a manual-only field still sends a coverage request even though the component immediately renders nothing; conditionally enable this query with `field.agentFilled` so manual fields do not perform the two unnecessary coverage counts.</violation>
</file>
<file name="apps/agent/agent/lib/fields.ts">
<violation number="1" location="apps/agent/agent/lib/fields.ts:37">
P3: `readFields` is exported but has no repository caller, leaving this new record-reading path dead; either wire it into an agent read flow or remove it to avoid unused behavior that can drift from the shared service.</violation>
<violation number="2" location="apps/agent/agent/lib/fields.ts:79">
P2: Clearing a field on a nonexistent or wrong-type record reports success even though nothing changed; validate that the target record exists for the selected entity before calling `writeValues`.</violation>
<violation number="3" location="apps/agent/agent/lib/fields.ts:118">
P2: A SELECT with a whitespace-only option passes the length check and creates an option that can never be selected; trim and reject blank option labels before creating the definition.</violation>
<violation number="4" location="apps/agent/agent/lib/fields.ts:134">
P1: Creating a manual-only field through `manage_fields` still stores `agentFilled: true`, so the agent can later write to a field the rep explicitly handed back; pass the tool flag through `createField` and persist it, defaulting only when absent.</violation>
<violation number="5" location="apps/agent/agent/lib/fields.ts:148">
P1: Creating a field or changing an agent-filled field's brief through these agent tools never queues the required `field-backfill` task, so existing records are not evaluated with the new field or instructions; mirror the backfill trigger used by the API path after these writes.</violation>
</file>
<file name="apps/agent/agent/tools/set_field_value.ts">
<violation number="1" location="apps/agent/agent/tools/set_field_value.ts:6">
P2: The new tool is not registered in the repository’s tool registries, so telemetry buckets its calls as `other` and the authored-tool transcript/allowlist checks fail. Add `set_field_value` to `AGENT_TOOLS` and give it an explicit transcript verb alongside the tool file.</violation>
</file>
<file name="apps/app/components/crm/fields/fields-list.tsx">
<violation number="1" location="apps/app/components/crm/fields/fields-list.tsx:159">
P2: The Standard fields section promises “reorder and hide only,” but each entry is rendered as static text with no reorder or visibility control, so users cannot perform the advertised customization; wire those actions to persisted standard-field settings or remove the promise from the row copy.</violation>
<violation number="2" location="apps/app/components/crm/fields/fields-list.tsx:173">
P2: A failed fields.list request is presented as “No custom fields yet” with a New field button because only the pending state is handled; add an explicit `query.isError` state so transient or authorization failures are not mistaken for empty data.</violation>
<violation number="3" location="apps/app/components/crm/fields/fields-list.tsx:173">
P2: When every custom field is archived, the sheet shows the empty state and hides the Archived disclosure, leaving no Restore action and preventing users from recovering those fields; render the archived section outside this live-fields-only branch.</violation>
<violation number="4" location="apps/app/components/crm/fields/fields-list.tsx:200">
P2: Reordering live fields after an archive can assign the same `position` to an archived field that retained its old position, so restoring it can produce an unstable order; reorder all definitions together or assign archived/restored fields unique positions.</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 even though field values are serialized as primitives, allowing malformed values to be persisted as strings for TEXT and USER fields. Constrain each value to `string | number | boolean | null` before it reaches the coercion layer.</violation>
</file>
<file name="apps/api/src/deals/deals.service.ts">
<violation number="1" location="apps/api/src/deals/deals.service.ts:275">
P1: Custom-field changes can be committed even when the same deal update fails, leaving the API reporting an unsuccessful update while the field value has changed. Applying fields through the same transaction as the deal update (and inside the existing error translation path) would keep the update atomic and preserve consistent missing-deal errors.</violation>
</file>
<file name="docs/plan/dynamic-fields-build.md">
<violation number="1" location="docs/plan/dynamic-fields-build.md:80">
P2: This plan is a stale build order: virtually every step it tells an agent to create (fields.ts + exports entry, the fields router/service/files.spec.ts, the agent tools, the Prisma models, the fields/field URL params, the md sheet size, Badge, Collapsible, SortableList, the CrmCache.fields entry) is already present in the repo at this PR's base. An agent following it cannot perform the steps and risks re-creating or overwriting the existing, working implementation. Recommend rewriting this as a record of what shipped (or dropping the build steps) so it reflects the actual repo state before it is used as an instruction set.</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:12">
P2: Agents can write the wrong option when a SELECT field contains duplicate labels. Enforce case-insensitive uniqueness for active option labels per field, or reject duplicates in the create/update validation before storing them.</violation>
<violation number="2" location="packages/db/prisma/migrations/20260806140000_dynamic_fields/migration.sql:42">
P2: A field value can exist without exactly one company, contact, or deal owner, and the nullable unique indexes do not prevent duplicates for NULL owners. Add a `num_nonnulls(companyId, contactId, dealId) = 1` constraint (and enforce the selected owner matches the definition entity) so malformed rows cannot enter the table.</violation>
<violation number="3" location="packages/db/prisma/migrations/20260806140000_dynamic_fields/migration.sql:57">
P2: Concurrent field creation can make the displayed field order unstable because duplicate positions are allowed and ties have no secondary ordering. Serialize position allocation in a transaction or use a database-backed ordering strategy with a deterministic tie-breaker.</violation>
<violation number="4" location="packages/db/prisma/migrations/20260806140000_dynamic_fields/migration.sql:102">
P2: A SELECT value can reference an option from a different field because the two foreign keys are independent. Add a composite relationship/constraint tying `fieldValue.optionId` to an option whose `fieldId` equals the value's `fieldId`.</violation>
</file>
<file name="packages/db/prisma/schema.prisma">
<violation number="1" location="packages/db/prisma/schema.prisma:536">
P2: USER custom fields can store arbitrary or deleted user IDs and cannot be resolved or cleaned up with the `User` record because `userId` is only a scalar. Model this as an optional `User` relation with `onDelete: SetNull` (including the reverse relation), or use a text field if it is intentionally free-form.</violation>
<violation number="2" location="packages/db/prisma/schema.prisma:540">
P2: Opening a record or loading custom-field values will scan the entire `fieldValue` table as data grows because the only target indexes are prefixed by `fieldId`, while `valuesFor` and the agent reader filter by `companyId`, `contactId`, or `dealId` alone. Add indexes keyed by each record column.</violation>
</file>
<file name="apps/api/test/bulk.spec.ts">
<violation number="1" location="apps/api/test/bulk.spec.ts:198">
P2: The company created as `Doomed Co` with domain `doomed-${domain}` is never removed by `clean()`: cleanup matches companies by `where: { domain }` (the exact `bulk-spec.test` domain), so this orphan survives any run where the test fails before `companies.bulkDelete`. Because `companies.create` throws `ConflictException` on a duplicate domain, the next run fails in `beforeAll`/test setup with a confusing "already uses the domain" error instead of the real failure. Consider cleaning `domain: { endsWith: domain }` or tracking the created company id for removal.</violation>
</file>
<file name="apps/api/test/fields.spec.ts">
<violation number="1" location="apps/api/test/fields.spec.ts:147">
P3: `reorder` returns the full entity list, so this exact-array assertion only holds when no other COMPANY field definitions exist in the shared DB. If any seeded or leftover COMPANY field is present the test fails for reasons unrelated to this case. Prefer asserting the relative order of the two spec fields (indexes) rather than the full list.</violation>
<violation number="2" location="apps/api/test/fields.spec.ts:158">
P3: The spec couples unrelated test groups through shared server-side state and declaration order: the "field values" tests depend on fields created by earlier "field definitions" tests, and queued/companyId are mutated across tests. Any focus/filter run (bun -t, .only) or reorder silently fails. Suggest seeding required fields in a beforeAll and asserting only what each test creates.</violation>
</file>
<file name="packages/db/src/agent-tasks.ts">
<violation number="1" location="packages/db/src/agent-tasks.ts:36">
P3: The documented queue ordering is now incomplete: `field-backfill` runs at priority 20, between `companyProfile` and `recheck`, but `docs/agent.md` omits it. Updating that priority list would keep operational guidance consistent with the scheduler.</violation>
</file>
<file name="apps/agent/agent/tools/manage_fields.ts">
<violation number="1" location="apps/agent/agent/tools/manage_fields.ts:77">
P2: Updating only `agentFilled` clears the existing brief because the tool maps an omitted brief to `null`, losing the instructions that should guide future filling. Preserve an omitted brief and reserve an explicit clear value for intentionally removing it.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| }); | ||
| } | ||
|
|
||
| async fieldBackfill(key: string, reason: string): Promise<void> { |
There was a problem hiding this comment.
P1: Backfills for same-named fields on different record types are collapsed into one task, and the worker cannot tell which entity to scan. Include the entity in the method signature and task identity/reason so each (entity, key) field gets its own task.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/agent/agent-trigger.service.ts, line 73:
<comment>Backfills for same-named fields on different record types are collapsed into one task, and the worker cannot tell which entity to scan. Include the entity in the method signature and task identity/reason so each `(entity, key)` field gets its own task.</comment>
<file context>
@@ -70,6 +70,44 @@ export class AgentTriggerService {
});
}
+ async fieldBackfill(key: string, reason: string): Promise<void> {
+ try {
+ const pending = await this.db.agentTask.findFirst({
</file context>
|
|
||
| async update(id: string, input: CompanyUpdateInput) { | ||
| if (input.fields) { | ||
| await this.fields.applyValues(this.db, "COMPANY", id, input.fields); |
There was a problem hiding this comment.
P1: A failed company update can still persist custom-field changes, and a multi-field request can persist only its earlier fields, leaving the record in a partially updated state. Applying the field writes and company update in one transaction would make the update atomic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/companies/companies.service.ts, line 324:
<comment>A failed company update can still persist custom-field changes, and a multi-field request can persist only its earlier fields, leaving the record in a partially updated state. Applying the field writes and company update in one transaction would make the update atomic.</comment>
<file context>
@@ -309,6 +320,10 @@ export class CompaniesService {
async update(id: string, input: CompanyUpdateInput) {
+ if (input.fields) {
+ await this.fields.applyValues(this.db, "COMPANY", id, input.fields);
+ }
+
</file context>
| include: WITH_OPTIONS, | ||
| }); | ||
|
|
||
| return serializeField(definition); |
There was a problem hiding this comment.
P1: Creating a field or changing an agent-filled field's brief through these agent tools never queues the required field-backfill task, so existing records are not evaluated with the new field or instructions; mirror the backfill trigger used by the API path after these writes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/fields.ts, line 148:
<comment>Creating a field or changing an agent-filled field's brief through these agent tools never queues the required `field-backfill` task, so existing records are not evaluated with the new field or instructions; mirror the backfill trigger used by the API path after these writes.</comment>
<file context>
@@ -0,0 +1,200 @@
+ include: WITH_OPTIONS,
+ });
+
+ return serializeField(definition);
+}
+
</file context>
| key, | ||
| label: input.label, | ||
| type: input.type, | ||
| agentBrief: input.agentBrief ?? null, |
There was a problem hiding this comment.
P1: Creating a manual-only field through manage_fields still stores agentFilled: true, so the agent can later write to a field the rep explicitly handed back; pass the tool flag through createField and persist it, defaulting only when absent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/fields.ts, line 134:
<comment>Creating a manual-only field through `manage_fields` still stores `agentFilled: true`, so the agent can later write to a field the rep explicitly handed back; pass the tool flag through `createField` and persist it, defaulting only when absent.</comment>
<file context>
@@ -0,0 +1,200 @@
+ key,
+ label: input.label,
+ type: input.type,
+ agentBrief: input.agentBrief ?? null,
+ position: (last?.position ?? -1) + 1,
+ options: usesOptions(input.type)
</file context>
|
|
||
| async update(id: string, input: DealUpdateInput) { | ||
| if (input.fields) { | ||
| await this.fields.applyValues(this.db, "DEAL", id, input.fields); |
There was a problem hiding this comment.
P1: Custom-field changes can be committed even when the same deal update fails, leaving the API reporting an unsuccessful update while the field value has changed. Applying fields through the same transaction as the deal update (and inside the existing error translation path) would keep the update atomic and preserve consistent missing-deal errors.
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 275:
<comment>Custom-field changes can be committed even when the same deal update fails, leaving the API reporting an unsuccessful update while the field value has changed. Applying fields through the same transaction as the deal update (and inside the existing error translation path) would keep the update atomic and preserve consistent missing-deal errors.</comment>
<file context>
@@ -258,6 +271,10 @@ export class DealsService {
async update(id: string, input: DealUpdateInput) {
+ if (input.fields) {
+ await this.fields.applyValues(this.db, "DEAL", id, input.fields);
+ }
+
</file context>
| className="grid place-content-center text-current transition-none [&>svg]:size-3.5" | ||
| > | ||
| <CheckIcon /> | ||
| {props.checked === "indeterminate" ? <MinusIcon /> : <CheckIcon />} |
There was a problem hiding this comment.
P3: Picking the icon from the controlled checked prop breaks the uncontrolled case: when a consumer uses defaultChecked="indeterminate" (supported by Radix), props.checked is undefined so the indicator renders a checkmark even though the box shows the indeterminate styling and data-state. Consider driving the icon from the rendered data-state (e.g. a data-[state=indeterminate] selector on a nested icon) so it matches whatever state Radix actually applies.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/checkbox.tsx, line 25:
<comment>Picking the icon from the controlled `checked` prop breaks the uncontrolled case: when a consumer uses `defaultChecked="indeterminate"` (supported by Radix), `props.checked` is `undefined` so the indicator renders a checkmark even though the box shows the indeterminate styling and data-state. Consider driving the icon from the rendered data-state (e.g. a data-[state=indeterminate] selector on a nested icon) so it matches whatever state Radix actually applies.</comment>
<file context>
@@ -22,7 +22,7 @@ function Checkbox({
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
- <CheckIcon />
+ {props.checked === "indeterminate" ? <MinusIcon /> : <CheckIcon />}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
</file context>
| return definitions.map(serializeField); | ||
| } | ||
|
|
||
| export async function readFields( |
There was a problem hiding this comment.
P3: readFields is exported but has no repository caller, leaving this new record-reading path dead; either wire it into an agent read flow or remove it to avoid unused behavior that can drift from the shared service.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/agent/agent/lib/fields.ts, line 37:
<comment>`readFields` is exported but has no repository caller, leaving this new record-reading path dead; either wire it into an agent read flow or remove it to avoid unused behavior that can drift from the shared service.</comment>
<file context>
@@ -0,0 +1,200 @@
+ return definitions.map(serializeField);
+}
+
+export async function readFields(
+ entity: FieldEntity,
+ recordId: string,
</file context>
| ids: [second.id, first.id], | ||
| }); | ||
|
|
||
| expect(reordered.map((field) => field.key)).toEqual([ |
There was a problem hiding this comment.
P3: reorder returns the full entity list, so this exact-array assertion only holds when no other COMPANY field definitions exist in the shared DB. If any seeded or leftover COMPANY field is present the test fails for reasons unrelated to this case. Prefer asserting the relative order of the two spec fields (indexes) rather than the full list.
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 147:
<comment>`reorder` returns the full entity list, so this exact-array assertion only holds when no other COMPANY field definitions exist in the shared DB. If any seeded or leftover COMPANY field is present the test fails for reasons unrelated to this case. Prefer asserting the relative order of the two spec fields (indexes) rather than the full list.</comment>
<file context>
@@ -0,0 +1,221 @@
+ ids: [second.id, first.id],
+ });
+
+ expect(reordered.map((field) => field.key)).toEqual([
+ "spec_seats",
+ "spec_runs_on",
</file context>
| }); | ||
| }); | ||
|
|
||
| describe("field values", () => { |
There was a problem hiding this comment.
P3: The spec couples unrelated test groups through shared server-side state and declaration order: the "field values" tests depend on fields created by earlier "field definitions" tests, and queued/companyId are mutated across tests. Any focus/filter run (bun -t, .only) or reorder silently fails. Suggest seeding required fields in a beforeAll and asserting only what each test creates.
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 158:
<comment>The spec couples unrelated test groups through shared server-side state and declaration order: the "field values" tests depend on fields created by earlier "field definitions" tests, and queued/companyId are mutated across tests. Any focus/filter run (bun -t, .only) or reorder silently fails. Suggest seeding required fields in a beforeAll and asserting only what each test creates.</comment>
<file context>
@@ -0,0 +1,221 @@
+ });
+});
+
+describe("field values", () => {
+ it("round-trips each storage class", async () => {
+ await fields.create({
</file context>
| identify: 100, | ||
| sweep: 50, | ||
| companyProfile: 40, | ||
| fieldBackfill: 20, |
There was a problem hiding this comment.
P3: The documented queue ordering is now incomplete: field-backfill runs at priority 20, between companyProfile and recheck, but docs/agent.md omits it. Updating that priority list would keep operational guidance consistent with the scheduler.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/src/agent-tasks.ts, line 36:
<comment>The documented queue ordering is now incomplete: `field-backfill` runs at priority 20, between `companyProfile` and `recheck`, but `docs/agent.md` omits it. Updating that priority list would keep operational guidance consistent with the scheduler.</comment>
<file context>
@@ -32,5 +33,6 @@ export const PRIORITY = {
identify: 100,
sweep: 50,
companyProfile: 40,
+ fieldBackfill: 20,
recheck: 0,
} as const;
</file context>
Summary by cubic
Adds dynamic custom fields for companies, contacts, and deals with a full API, UI editor/sheet, and inline editing. Also adds bulk actions, deal–contact management, and support for ZAR.
New Features
fieldsAPI for list/create/update/archive/reorder; records now acceptfieldsin update inputs and return field values.list_fields,manage_fields(create/update brief),set_field_value,archive_field.Migration
@dnd-kit/core,@dnd-kit/sortable,@dnd-kit/modifiers,@dnd-kit/utilities, then rebuild.fieldsincompany.update,contact.update, anddeal.updatewhen writing custom values.Written for commit a1cbf56. Summary will update on new commits.