diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 8752ce7..fbe6c3e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Resolve validator revision env: - DEFT_VALIDATOR_REF: ${{ inputs.deft_ref || vars.DEFT_VALIDATOR_REF || 'd9c1530dec444ac7bc863168b472f12e68cfef6f' }} + DEFT_VALIDATOR_REF: ${{ inputs.deft_ref || vars.DEFT_VALIDATOR_REF || 'cd487526b7cde82882a6edcbcfbe3659d2a78125' }} run: | if ! printf '%s' "$DEFT_VALIDATOR_REF" | grep -Eq '^[0-9a-f]{40}$'; then echo 'The Deft validator revision must be a full 40-character commit SHA.' >&2 @@ -42,7 +42,7 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: ${{ vars.DEFT_VALIDATOR_REPOSITORY || 'Maneek21/Deft' }} - ref: ${{ inputs.deft_ref || vars.DEFT_VALIDATOR_REF || 'd9c1530dec444ac7bc863168b472f12e68cfef6f' }} + ref: ${{ inputs.deft_ref || vars.DEFT_VALIDATOR_REF || 'cd487526b7cde82882a6edcbcfbe3659d2a78125' }} path: deft persist-credentials: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a6c97..22dddac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the Contacts module are documented here. Versions follow [Semantic Versioning](https://semver.org/). +## 1.8.0 — 2026-09-19 + +### Added + +- Complete declarative CRM workflows for contacts, companies, deals, + activities, outreach drafts, and project-linked follow-up tasks. +- Additional views, filters, relations, validation, and operator-facing + metadata used by the integrated Contacts CRM app. + +### Compatibility + +- The update is additive and preserves the existing Contacts 1.1 collections + and fields. + ## 1.1.0 — 2026-08-18 ### Added diff --git a/PUBLISHING.md b/PUBLISHING.md index b320131..e4cf52d 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -2,9 +2,9 @@ ## One-time repository publication -The initial workflow is pinned to Deft validator commit -`d9c1530dec444ac7bc863168b472f12e68cfef6f`, the first published core commit containing the finalized 1.1 -module validator. Future pins must remain full 40-character commit SHAs; do not +The workflow is pinned to Deft validator commit +`cd487526b7cde82882a6edcbcfbe3659d2a78125`, which contains the contract used by Contacts 1.8. +Future pins must remain full 40-character commit SHAs; do not substitute a branch, tag, `master`, or an older validator. The workflow remains intentionally configurable through a diff --git a/README.md b/README.md index 9ee0b35..a4c9ae9 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ core. The installable product is one declarative file: [`deft.module.json`](./deft.module.json). It contains no JavaScript, SQL, remote installer, credentials, or executable payload. -Current release: `1.1.0` +Current release: `1.8.0` Canonical manifest digest: -`sha256:431d879dd511261346c2d38b1c6341f61eb7878ea4690ca836425b3a0c2c266d` +`sha256:495075181043c6cb95ad4080ea894faa6c1b798b3eb2db97571d0b53ff0cbdaf` ## What it adds @@ -79,7 +79,7 @@ run either: Pass a different Deft checkout as the first argument when needed. The helpers run `corepack pnpm module:check ` from that checkout and verify -the expected `1.1.0` digest. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the +the expected `1.8.0` digest. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the release rules. ## Fixtures and screenshots diff --git a/deft.module.json b/deft.module.json index ab1a872..50319c7 100644 --- a/deft.module.json +++ b/deft.module.json @@ -9,8 +9,9 @@ "type": "text" }, { + "description": "Historical company text. The Company link identifies the current company. Preserve this text for reference, or clear it after reviewing the linked company.", "key": "company", - "label": "Company", + "label": "Legacy company name", "required": false, "type": "text" }, @@ -75,8 +76,9 @@ "type": "tags" }, { + "description": "Manually entered historical date. Last recorded contact is calculated separately from directly linked completed interactions; editing this field does not log an activity.", "key": "last_contacted_at", - "label": "Last contacted", + "label": "Last contacted (manual)", "required": false, "type": "datetime" }, @@ -88,6 +90,32 @@ } ], "key": "contacts", + "latest_related": [ + { + "date_field": "occurred_at", + "description": "Latest completed call, email or meeting linked directly to this record. Notes, unclassified activities and future dates are excluded.", + "key": "last_recorded_contact", + "label": "Last recorded contact", + "relation_field": "contact_id", + "source_collection": "activities", + "where": [ + { + "field": "kind", + "values": [ + "call", + "email", + "meeting" + ] + }, + { + "field": "outcome", + "values": [ + "completed" + ] + } + ] + } + ], "name": "Contacts", "search": { "fields": [ @@ -99,8 +127,8 @@ "status" ], "subtitle_fields": [ - "company", - "role" + "role", + "email" ], "title_field": "name" }, @@ -144,7 +172,8 @@ "owner", "tags", "last_contacted_at", - "notes" + "notes", + "company" ], "key": "form", "name": "Form", @@ -161,7 +190,8 @@ "owner", "tags", "last_contacted_at", - "notes" + "notes", + "company" ], "key": "detail", "name": "Details", @@ -241,6 +271,32 @@ } ], "key": "companies", + "latest_related": [ + { + "date_field": "occurred_at", + "description": "Latest completed call, email or meeting linked directly to this record. Notes, unclassified activities and future dates are excluded.", + "key": "last_recorded_contact", + "label": "Last recorded contact", + "relation_field": "company_id", + "source_collection": "activities", + "where": [ + { + "field": "kind", + "values": [ + "call", + "email", + "meeting" + ] + }, + { + "field": "outcome", + "values": [ + "completed" + ] + } + ] + } + ], "name": "Companies", "search": { "fields": [ @@ -339,11 +395,69 @@ "type": "relation" }, { + "description": "Amount in the selected currency. Values without a currency remain unclassified.", "key": "value", "label": "Value", "required": false, "type": "number" }, + { + "description": "Choose the unit for this amount. Leave unset if the original currency is unknown.", + "key": "currency", + "label": "Currency", + "options": [ + { + "label": "USD", + "value": "usd" + }, + { + "label": "EUR", + "value": "eur" + }, + { + "label": "GBP", + "value": "gbp" + }, + { + "label": "INR", + "value": "inr" + }, + { + "label": "CAD", + "value": "cad" + }, + { + "label": "AUD", + "value": "aud" + }, + { + "label": "JPY", + "value": "jpy" + }, + { + "label": "CNY", + "value": "cny" + }, + { + "label": "SGD", + "value": "sgd" + }, + { + "label": "AED", + "value": "aed" + }, + { + "label": "CHF", + "value": "chf" + }, + { + "label": "NZD", + "value": "nzd" + } + ], + "required": false, + "type": "single_select" + }, { "default": "lead", "key": "stage", @@ -404,6 +518,32 @@ } ], "key": "deals", + "latest_related": [ + { + "date_field": "occurred_at", + "description": "Latest completed call, email or meeting linked directly to this record. Notes, unclassified activities and future dates are excluded.", + "key": "last_recorded_contact", + "label": "Last recorded contact", + "relation_field": "deal_id", + "source_collection": "activities", + "where": [ + { + "field": "kind", + "values": [ + "call", + "email", + "meeting" + ] + }, + { + "field": "outcome", + "values": [ + "completed" + ] + } + ] + } + ], "name": "Deals", "search": { "fields": [ @@ -425,12 +565,154 @@ "company_id", "primary_contact_id", "value", + "currency", "owner", "close_date" ], "group_by": "stage", "key": "pipeline", "name": "Pipeline", + "quick_filters": [ + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + } + ], + "key": "open", + "name": "Open deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "won", + "lost" + ] + } + ], + "key": "closed", + "name": "Closed deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "owner", + "operator": "is_empty", + "value": true + } + ], + "key": "unassigned", + "name": "Unassigned" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "is_empty", + "value": true + } + ], + "key": "missing_close_date", + "name": "Missing close date" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "past" + } + ], + "key": "overdue_close", + "name": "Overdue close" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "today" + } + ], + "key": "closing_today", + "name": "Closing today" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "next_7_days" + } + ], + "key": "closing_week", + "name": "Closing next 7 days" + } + ], + "summary": { + "unit_field": "currency", + "value_field": "value" + }, "type": "board" }, { @@ -439,10 +721,148 @@ "company_id", "stage", "value", + "currency", "owner" ], "key": "timeline", "name": "Close timeline", + "quick_filters": [ + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + } + ], + "key": "open", + "name": "Open deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "won", + "lost" + ] + } + ], + "key": "closed", + "name": "Closed deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "owner", + "operator": "is_empty", + "value": true + } + ], + "key": "unassigned", + "name": "Unassigned" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "is_empty", + "value": true + } + ], + "key": "missing_close_date", + "name": "Missing close date" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "past" + } + ], + "key": "overdue_close", + "name": "Overdue close" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "today" + } + ], + "key": "closing_today", + "name": "Closing today" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "next_7_days" + } + ], + "key": "closing_week", + "name": "Closing next 7 days" + } + ], "start_field": "close_date", "type": "timeline" }, @@ -452,12 +872,150 @@ "company_id", "primary_contact_id", "value", + "currency", "stage", "owner", "close_date" ], "key": "table", "name": "Table", + "quick_filters": [ + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + } + ], + "key": "open", + "name": "Open deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "won", + "lost" + ] + } + ], + "key": "closed", + "name": "Closed deals" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "owner", + "operator": "is_empty", + "value": true + } + ], + "key": "unassigned", + "name": "Unassigned" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "is_empty", + "value": true + } + ], + "key": "missing_close_date", + "name": "Missing close date" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "past" + } + ], + "key": "overdue_close", + "name": "Overdue close" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "today" + } + ], + "key": "closing_today", + "name": "Closing today" + }, + { + "filters": [ + { + "field": "stage", + "operator": "in", + "value": [ + "lead", + "qualified", + "proposal", + "negotiation" + ] + }, + { + "field": "close_date", + "operator": "date_relative", + "value": "next_7_days" + } + ], + "key": "closing_week", + "name": "Closing next 7 days" + } + ], "type": "table" }, { @@ -466,6 +1024,7 @@ "company_id", "primary_contact_id", "value", + "currency", "stage", "close_date", "owner", @@ -482,6 +1041,7 @@ "company_id", "primary_contact_id", "value", + "currency", "stage", "close_date", "owner", @@ -527,6 +1087,27 @@ "required": false, "type": "single_select" }, + { + "description": "Choose Completed only for an interaction that happened. A past dated completed call, email or meeting updates Last recorded contact. Leaving this blank preserves an unclassified historical activity.", + "key": "outcome", + "label": "Outcome", + "options": [ + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Planned", + "value": "planned" + }, + { + "label": "Cancelled", + "value": "cancelled" + } + ], + "required": false, + "type": "single_select" + }, { "key": "occurred_at", "label": "Occurred at", @@ -578,11 +1159,13 @@ "subject", "kind", "occurred_at", - "notes" + "notes", + "outcome" ], "subtitle_fields": [ "kind", - "occurred_at" + "occurred_at", + "outcome" ], "title_field": "subject" }, @@ -592,6 +1175,7 @@ "fields": [ "subject", "kind", + "outcome", "contact_id", "company_id", "deal_id", @@ -606,6 +1190,7 @@ "fields": [ "subject", "kind", + "outcome", "occurred_at", "contact_id", "company_id", @@ -620,6 +1205,7 @@ "fields": [ "subject", "kind", + "outcome", "occurred_at", "contact_id", "company_id", @@ -635,6 +1221,7 @@ "fields": [ "subject", "kind", + "outcome", "occurred_at", "contact_id", "company_id", @@ -647,6 +1234,144 @@ "type": "detail" } ] + }, + { + "description": "Prepare messages for selected CRM contacts and review them with your team.", + "fields": [ + { + "key": "name", + "label": "Name", + "required": true, + "type": "text" + }, + { + "key": "subject", + "label": "Subject", + "required": true, + "type": "text" + }, + { + "description": "Draft content for review. Saving this record does not send a message.", + "key": "body", + "label": "Message", + "required": false, + "type": "long_text" + }, + { + "description": "Selected CRM contacts. Review their current email addresses before any execution.", + "key": "contacts", + "label": "Recipients", + "multiple": true, + "required": false, + "target_collection": "contacts", + "type": "relation" + }, + { + "default": "draft", + "key": "status", + "label": "Draft status", + "options": [ + { + "label": "Draft", + "value": "draft" + }, + { + "label": "Ready for review", + "value": "ready_for_review" + }, + { + "label": "Paused", + "value": "paused" + } + ], + "required": false, + "type": "single_select" + }, + { + "key": "owner", + "label": "Owner", + "multiple": false, + "required": false, + "type": "member" + }, + { + "key": "audience_notes", + "label": "Audience notes", + "required": false, + "type": "long_text" + } + ], + "key": "outreach", + "name": "Outreach", + "search": { + "fields": [ + "name", + "subject", + "body", + "status", + "audience_notes" + ], + "subtitle_fields": [ + "subject", + "status" + ], + "title_field": "name" + }, + "singular_name": "Outreach draft", + "views": [ + { + "fields": [ + "name", + "subject", + "contacts", + "status", + "owner" + ], + "key": "table", + "name": "All drafts", + "type": "table" + }, + { + "fields": [ + "name", + "subject", + "contacts", + "owner" + ], + "group_by": "status", + "key": "board", + "name": "Review board", + "type": "board" + }, + { + "fields": [ + "name", + "subject", + "body", + "contacts", + "status", + "owner", + "audience_notes" + ], + "key": "form", + "name": "Form", + "type": "form" + }, + { + "fields": [ + "name", + "subject", + "body", + "contacts", + "status", + "owner", + "audience_notes" + ], + "key": "detail", + "name": "Details", + "type": "detail" + } + ] } ], "description": "A native relationship workspace for contacts, companies, deals, and activity.", @@ -659,5 +1384,5 @@ }, "schema_version": "1", "slug": "contacts", - "version": "1.1.0" + "version": "1.8.0" } diff --git a/scripts/validate.ps1 b/scripts/validate.ps1 index 4f3e3e1..3a28116 100644 --- a/scripts/validate.ps1 +++ b/scripts/validate.ps1 @@ -5,7 +5,7 @@ param( ) $ErrorActionPreference = 'Stop' -$ExpectedDigest = 'sha256:431d879dd511261346c2d38b1c6341f61eb7878ea4690ca836425b3a0c2c266d' +$ExpectedDigest = 'sha256:495075181043c6cb95ad4080ea894faa6c1b798b3eb2db97571d0b53ff0cbdaf' $ModuleRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path if ([string]::IsNullOrWhiteSpace($DeftPath)) { @@ -33,4 +33,4 @@ if ($Output -notmatch [regex]::Escape($ExpectedDigest)) { throw "Manifest is valid but its digest changed. Expected $ExpectedDigest. Update the version, changelog, README, and validation helpers intentionally." } -Write-Output "Contacts 1.1.0 matches $ExpectedDigest" +Write-Output "Contacts 1.8.0 matches $ExpectedDigest" diff --git a/scripts/validate.sh b/scripts/validate.sh index cf8fcbc..0cda00a 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh set -eu -expected_digest='sha256:431d879dd511261346c2d38b1c6341f61eb7878ea4690ca836425b3a0c2c266d' +expected_digest='sha256:495075181043c6cb95ad4080ea894faa6c1b798b3eb2db97571d0b53ff0cbdaf' script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) module_root=$(CDPATH= cd -- "$script_dir/.." && pwd) deft_path=${1:-"$(dirname -- "$module_root")/Deft"} @@ -23,4 +23,4 @@ case "$output" in ;; esac -printf '%s\n' "Contacts 1.1.0 matches $expected_digest" +printf '%s\n' "Contacts 1.8.0 matches $expected_digest"