diff --git a/backend/server.py b/backend/server.py index e2b2266..e12f4dc 100644 --- a/backend/server.py +++ b/backend/server.py @@ -787,6 +787,7 @@ def _upsert_contact_from_fundraising(conn, investor_name, contact, actor_user_id state = str(contact.get('state') or '').strip() country = str(contact.get('country') or '').strip() location_query = str(contact.get('location_query') or '').strip() + linkedin_url = str(contact.get('linkedin_url') or '').strip() if not full_name and not email: return None first_name, last_name = _split_full_name(full_name) @@ -830,20 +831,21 @@ def _upsert_contact_from_fundraising(conn, investor_name, contact, actor_user_id next_state = state or str(existing['state'] or '') next_country = country or str(existing['country'] or '') next_location_query = location_query or str(existing['location_query'] or '') + next_linkedin = linkedin_url or str(existing['linkedin_url'] or '') next_org = org_id or existing['organization_id'] conn.execute(""" UPDATE contacts SET first_name = ?, last_name = ?, email = ?, title = ?, - organization_id = ?, source = ?, contact_type = 'investor', city = ?, state = ?, country = ?, location_query = ?, updated_at = ? + organization_id = ?, source = ?, contact_type = 'investor', city = ?, state = ?, country = ?, location_query = ?, linkedin_url = ?, updated_at = ? WHERE id = ? - """, (next_first, next_last, next_email, next_title, next_org, next_source, next_city, next_state, next_country, next_location_query, now(), existing['id'])) + """, (next_first, next_last, next_email, next_title, next_org, next_source, next_city, next_state, next_country, next_location_query, next_linkedin, now(), existing['id'])) return existing['id'] contact_id = generate_id() conn.execute(""" INSERT INTO contacts ( - id, first_name, last_name, email, title, organization_id, source, contact_type, status, city, state, country, location_query, created_by, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, 'investor', 'active', ?, ?, ?, ?, ?, ?) + id, first_name, last_name, email, title, organization_id, source, contact_type, status, city, state, country, location_query, linkedin_url, created_by, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, 'investor', 'active', ?, ?, ?, ?, ?, ?, ?) """, ( contact_id, first_name or 'Unknown', @@ -856,6 +858,7 @@ def _upsert_contact_from_fundraising(conn, investor_name, contact, actor_user_id state, country, location_query, + linkedin_url, actor_user_id, now() )) diff --git a/backend/test_grid_contact_link.py b/backend/test_grid_contact_link.py index 2503152..1ca2382 100644 --- a/backend/test_grid_contact_link.py +++ b/backend/test_grid_contact_link.py @@ -44,7 +44,8 @@ def main(): ], "rows": [ {"id": "row-test-1", "investor_name": "Testco Capital", - "contacts": [{"name": "Jane Doe", "email": "jane@testco.com", "title": "Partner"}]}, + "contacts": [{"name": "Jane Doe", "email": "jane@testco.com", "title": "Partner", + "linkedin_url": "https://linkedin.com/in/janedoe"}]}, ], } server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), []) @@ -54,9 +55,11 @@ def main(): check(bool(fc and fc["contact_id"]), f"grid contact_id populated by sync (got {dict(fc) if fc else None})") if fc and fc["contact_id"]: - ct = conn.execute("SELECT id, email FROM contacts WHERE id=?", (fc["contact_id"],)).fetchone() + ct = conn.execute("SELECT id, email, linkedin_url FROM contacts WHERE id=?", (fc["contact_id"],)).fetchone() check(bool(ct and ct["email"] == "jane@testco.com"), f"link points to the correct contacts row (got {dict(ct) if ct else None})") + check(bool(ct and ct["linkedin_url"] == "https://linkedin.com/in/janedoe"), + f"LinkedIn entered in the grid persists to the contact (got {ct['linkedin_url'] if ct else None})") # Re-sync is idempotent: still exactly one linked contact for Jane. server.sync_fundraising_relational(conn, server.sanitize_fundraising_grid(grid), []) diff --git a/frontend/index.html b/frontend/index.html index 733148c..7973f0a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6946,9 +6946,9 @@ const contacts = Array.isArray(value) ? [...value] : []; const updateContacts = (next) => updateCell(row.id, col.id, next); return ( -
+
{contacts.map((c, idx) => ( -
+
{ const next = [...contacts]; next[idx] = { ...next[idx], name: e.target.value }; @@ -6982,6 +6982,17 @@ moveByKey(rowIndex, colIndex, 'Enter'); } }} /> + { + const next = [...contacts]; + next[idx] = { ...next[idx], linkedin_url: e.target.value }; + updateContacts(next); + }} onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + setEditing(null); + moveByKey(rowIndex, colIndex, 'Enter'); + } + }} /> i !== idx); updateContacts(next); }}>× -
+
{c.city || c.state || c.country ? `${c.city || '-'}, ${c.state || '-'}, ${c.country || '-'}` : 'No location set'}
))}
- +
diff --git a/start9/0.4/startos/utils.ts b/start9/0.4/startos/utils.ts index bc2ae0d..4c4971b 100644 --- a/start9/0.4/startos/utils.ts +++ b/start9/0.4/startos/utils.ts @@ -18,8 +18,9 @@ export const PACKAGE_TITLE = 'Ten31 Database' // * 0.1.0:50 (Set Anthropic API Key UI action — no terminal needed) // * 0.1.0:51 (entity-resolution fix: people double-count + duplicate queue) // * 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door) -// * Current: 0.1.0:53 (seed v5 thesis into the Architect Workshop) -export const PACKAGE_VERSION = '0.1.0:53' +// * 0.1.0:53 (seed v5 thesis into the Architect Workshop) +// * Current: 0.1.0:54 (unification polish: LinkedIn in grid inline contact editor) +export const PACKAGE_VERSION = '0.1.0:54' export const DATA_MOUNT_PATH = '/data' export const WEB_PORT = 8080 diff --git a/start9/0.4/startos/versions/index.ts b/start9/0.4/startos/versions/index.ts index ef90d35..09b5aba 100644 --- a/start9/0.4/startos/versions/index.ts +++ b/start9/0.4/startos/versions/index.ts @@ -14,8 +14,9 @@ import { v_0_1_0_50 } from './v0.1.0.50' import { v_0_1_0_51 } from './v0.1.0.51' import { v_0_1_0_52 } from './v0.1.0.52' import { v_0_1_0_53 } from './v0.1.0.53' +import { v_0_1_0_54 } from './v0.1.0.54' export const versionGraph = VersionGraph.of({ - current: v_0_1_0_53, - other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52], + current: v_0_1_0_54, + other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52, v_0_1_0_53], }) diff --git a/start9/0.4/startos/versions/v0.1.0.54.ts b/start9/0.4/startos/versions/v0.1.0.54.ts new file mode 100644 index 0000000..31a9f11 --- /dev/null +++ b/start9/0.4/startos/versions/v0.1.0.54.ts @@ -0,0 +1,17 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +// Grid/contacts unification polish: richer inline contact entry. The fundraising +// grid's per-contact editor now includes a LinkedIn field alongside name, email, +// title, and location; it persists to the contact record through the existing +// grid→contacts sync. No schema migration. +export const v_0_1_0_54 = VersionInfo.of({ + version: '0.1.0:54', + releaseNotes: { + en_US: [ + 'The fundraising grid can now capture a LinkedIn URL right in the inline contact', + 'editor (next to name, email, title, and location), saved to the contact record.', + 'Richer entry without leaving the grid.', + ].join(' '), + }, + migrations: { up: async () => {}, down: async () => {} }, +})