Skip to content

Commit ef45c5f

Browse files
Copilothotlong
andcommitted
Add example handler implementations for app-todo and app-crm actions
Provides actual function bodies for previously dangling target string references: completeTask, startTask, cloneTask, massCompleteTasks, deleteCompletedTasks, exportTasksToCSV (todo); convertLead, addToCampaign, cloneRecord, massUpdateStage, escalateCase, closeCase, markAsPrimaryContact, exportToCSV (crm). Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent cb515ff commit ef45c5f

7 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Case Action Handlers
5+
*
6+
* Handler implementations for actions defined in case.actions.ts.
7+
*
8+
* @example Registration:
9+
* ```ts
10+
* engine.registerAction('case', 'escalateCase', escalateCase);
11+
* engine.registerAction('case', 'closeCase', closeCase);
12+
* ```
13+
*/
14+
15+
interface ActionContext {
16+
record: Record<string, unknown>;
17+
user: { id: string; name: string };
18+
engine: {
19+
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
20+
};
21+
params?: Record<string, unknown>;
22+
}
23+
24+
/** Escalate a case to the escalation team */
25+
export async function escalateCase(ctx: ActionContext): Promise<void> {
26+
const { record, engine, user, params } = ctx;
27+
await engine.update('case', record._id as string, {
28+
is_escalated: true,
29+
escalation_reason: params?.reason as string,
30+
escalated_by: user.id,
31+
escalated_at: new Date().toISOString(),
32+
priority: 'urgent',
33+
});
34+
}
35+
36+
/** Close a case with a resolution */
37+
export async function closeCase(ctx: ActionContext): Promise<void> {
38+
const { record, engine, user, params } = ctx;
39+
await engine.update('case', record._id as string, {
40+
is_closed: true,
41+
resolution: params?.resolution as string,
42+
closed_by: user.id,
43+
closed_at: new Date().toISOString(),
44+
status: 'closed',
45+
});
46+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Contact Action Handlers
5+
*
6+
* Handler implementations for actions defined in contact.actions.ts.
7+
*
8+
* @example Registration:
9+
* ```ts
10+
* engine.registerAction('contact', 'markAsPrimaryContact', markAsPrimaryContact);
11+
* ```
12+
*/
13+
14+
interface ActionContext {
15+
record: Record<string, unknown>;
16+
user: { id: string; name: string };
17+
engine: {
18+
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
19+
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
20+
};
21+
params?: Record<string, unknown>;
22+
}
23+
24+
/** Mark a contact as the primary contact for its account */
25+
export async function markAsPrimaryContact(ctx: ActionContext): Promise<void> {
26+
const { record, engine } = ctx;
27+
const accountId = record.account_id as string;
28+
29+
// Clear existing primary contacts on the same account
30+
const siblings = await engine.find('contact', { account_id: accountId, is_primary: true });
31+
for (const sibling of siblings) {
32+
await engine.update('contact', sibling._id as string, { is_primary: false });
33+
}
34+
35+
// Set current contact as primary
36+
await engine.update('contact', record._id as string, { is_primary: true });
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Global Action Handlers
5+
*
6+
* Handler implementations for cross-domain actions defined in global.actions.ts.
7+
*
8+
* @example Registration:
9+
* ```ts
10+
* engine.registerAction('*', 'exportToCSV', exportToCSV);
11+
* ```
12+
*/
13+
14+
interface ActionContext {
15+
record: Record<string, unknown>;
16+
user: { id: string; name: string };
17+
engine: {
18+
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
19+
};
20+
params?: Record<string, unknown>;
21+
}
22+
23+
/** Export records of a given object to CSV format */
24+
export async function exportToCSV(ctx: ActionContext): Promise<string> {
25+
const { params, engine } = ctx;
26+
const objectName = (params?.objectName ?? 'account') as string;
27+
const records = await engine.find(objectName, {});
28+
if (records.length === 0) return '';
29+
30+
const keys = Object.keys(records[0]);
31+
const header = keys.join(',');
32+
const rows = records.map((r) => keys.map((k) => r[k] ?? '').join(','));
33+
return [header, ...rows].join('\n');
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Action Handler Implementations Barrel
5+
*
6+
* Re-exports all handler functions for registration via engine.registerAction().
7+
*/
8+
export { convertLead, addToCampaign } from './lead.handlers';
9+
export { cloneRecord, massUpdateStage } from './opportunity.handlers';
10+
export { escalateCase, closeCase } from './case.handlers';
11+
export { markAsPrimaryContact } from './contact.handlers';
12+
export { exportToCSV } from './global.handlers';
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Lead Action Handlers
5+
*
6+
* Handler implementations for lead-domain actions defined in lead.actions.ts.
7+
* The `ConvertLeadAction` (type: flow) is handled by the flow engine;
8+
* `CreateCampaignAction` (type: modal) is handled by the UI modal system.
9+
*
10+
* This file provides the server-side logic backing these actions.
11+
*
12+
* @example Registration:
13+
* ```ts
14+
* engine.registerAction('lead', 'convertLead', convertLead);
15+
* ```
16+
*/
17+
18+
interface ActionContext {
19+
record: Record<string, unknown>;
20+
user: { id: string; name: string };
21+
engine: {
22+
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
23+
insert(object: string, data: Record<string, unknown>): Promise<{ _id: string }>;
24+
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
25+
};
26+
params?: Record<string, unknown>;
27+
}
28+
29+
/** Convert a qualified lead into Account, Contact, and Opportunity records */
30+
export async function convertLead(ctx: ActionContext): Promise<{
31+
accountId: string;
32+
contactId: string;
33+
opportunityId: string;
34+
}> {
35+
const { record, engine, user } = ctx;
36+
37+
const account = await engine.insert('account', {
38+
name: record.company as string,
39+
website: record.website,
40+
industry: record.industry,
41+
created_by: user.id,
42+
});
43+
44+
const contact = await engine.insert('contact', {
45+
first_name: record.first_name,
46+
last_name: record.last_name,
47+
email: record.email,
48+
phone: record.phone,
49+
account_id: account._id,
50+
});
51+
52+
const opportunity = await engine.insert('opportunity', {
53+
name: `${record.company} - New Opportunity`,
54+
account_id: account._id,
55+
contact_id: contact._id,
56+
stage: 'prospecting',
57+
amount: record.estimated_value ?? 0,
58+
});
59+
60+
await engine.update('lead', record._id as string, {
61+
is_converted: true,
62+
status: 'converted',
63+
converted_account_id: account._id,
64+
converted_contact_id: contact._id,
65+
converted_opportunity_id: opportunity._id,
66+
});
67+
68+
return {
69+
accountId: account._id,
70+
contactId: contact._id,
71+
opportunityId: opportunity._id,
72+
};
73+
}
74+
75+
/** Add selected leads to a campaign */
76+
export async function addToCampaign(ctx: ActionContext): Promise<void> {
77+
const { params, engine } = ctx;
78+
const campaignId = params?.campaign as string;
79+
const leadIds = (params?.selectedIds ?? []) as string[];
80+
for (const leadId of leadIds) {
81+
await engine.insert('campaign_member', {
82+
campaign_id: campaignId,
83+
lead_id: leadId,
84+
status: 'sent',
85+
});
86+
}
87+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Opportunity Action Handlers
5+
*
6+
* Handler implementations for actions defined in opportunity.actions.ts.
7+
*
8+
* @example Registration:
9+
* ```ts
10+
* engine.registerAction('opportunity', 'cloneRecord', cloneRecord);
11+
* ```
12+
*/
13+
14+
interface ActionContext {
15+
record: Record<string, unknown>;
16+
user: { id: string; name: string };
17+
engine: {
18+
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
19+
insert(object: string, data: Record<string, unknown>): Promise<{ _id: string }>;
20+
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
21+
};
22+
params?: Record<string, unknown>;
23+
}
24+
25+
/** Clone an opportunity record */
26+
export async function cloneRecord(ctx: ActionContext): Promise<{ _id: string }> {
27+
const { record, engine } = ctx;
28+
const { _id, created_at, updated_at, ...fields } = record as Record<string, unknown>;
29+
return engine.insert('opportunity', {
30+
...fields,
31+
name: `Copy of ${fields.name ?? 'Untitled'}`,
32+
stage: 'prospecting',
33+
});
34+
}
35+
36+
/** Mass update opportunity stage for selected records */
37+
export async function massUpdateStage(ctx: ActionContext): Promise<void> {
38+
const { params, engine } = ctx;
39+
const newStage = params?.stage as string;
40+
const ids = (params?.selectedIds ?? []) as string[];
41+
for (const id of ids) {
42+
await engine.update('opportunity', id, { stage: newStage });
43+
}
44+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Task Action Handlers
5+
*
6+
* Example handler implementations for actions defined in task.actions.ts.
7+
* Each handler is registered via `engine.registerAction()` and referenced
8+
* by name through the action's `target` field.
9+
*
10+
* @example Registration (in a plugin or config bootstrap):
11+
* ```ts
12+
* engine.registerAction('task', 'completeTask', completeTask);
13+
* engine.registerAction('task', 'startTask', startTask);
14+
* ```
15+
*/
16+
17+
// ─── Handler Context (simplified for example purposes) ──────────────
18+
interface ActionContext {
19+
/** The record being acted upon */
20+
record: Record<string, unknown>;
21+
/** Current authenticated user */
22+
user: { id: string; name: string };
23+
/** Data engine for CRUD operations */
24+
engine: {
25+
update(object: string, id: string, data: Record<string, unknown>): Promise<void>;
26+
insert(object: string, data: Record<string, unknown>): Promise<{ _id: string }>;
27+
find(object: string, query: Record<string, unknown>): Promise<Array<Record<string, unknown>>>;
28+
delete(object: string, ids: string[]): Promise<void>;
29+
};
30+
/** Action parameters (from user input / params) */
31+
params?: Record<string, unknown>;
32+
}
33+
34+
/** Mark a single task as complete */
35+
export async function completeTask(ctx: ActionContext): Promise<void> {
36+
const { record, engine, user } = ctx;
37+
await engine.update('task', record._id as string, {
38+
status: 'completed',
39+
completed_at: new Date().toISOString(),
40+
completed_by: user.id,
41+
});
42+
}
43+
44+
/** Mark a task as in-progress */
45+
export async function startTask(ctx: ActionContext): Promise<void> {
46+
const { record, engine } = ctx;
47+
await engine.update('task', record._id as string, {
48+
status: 'in_progress',
49+
started_at: new Date().toISOString(),
50+
});
51+
}
52+
53+
/** Clone a task (duplicate with reset status) */
54+
export async function cloneTask(ctx: ActionContext): Promise<{ _id: string }> {
55+
const { record, engine } = ctx;
56+
const { _id, created_at, updated_at, completed_at, completed_by, ...fields } = record as Record<string, unknown>;
57+
return engine.insert('task', {
58+
...fields,
59+
status: 'not_started',
60+
subject: `Copy of ${fields.subject ?? 'Untitled'}`,
61+
});
62+
}
63+
64+
/** Mark all selected tasks as complete (bulk) */
65+
export async function massCompleteTasks(ctx: ActionContext): Promise<void> {
66+
const { params, engine, user } = ctx;
67+
const ids = (params?.selectedIds ?? []) as string[];
68+
const now = new Date().toISOString();
69+
for (const id of ids) {
70+
await engine.update('task', id, {
71+
status: 'completed',
72+
completed_at: now,
73+
completed_by: user.id,
74+
});
75+
}
76+
}
77+
78+
/** Delete all completed tasks */
79+
export async function deleteCompletedTasks(ctx: ActionContext): Promise<void> {
80+
const { engine } = ctx;
81+
const completed = await engine.find('task', { status: 'completed' });
82+
const ids = completed.map((r) => r._id as string);
83+
if (ids.length > 0) {
84+
await engine.delete('task', ids);
85+
}
86+
}
87+
88+
/** Export tasks to CSV format */
89+
export async function exportTasksToCSV(ctx: ActionContext): Promise<string> {
90+
const { engine } = ctx;
91+
const tasks = await engine.find('task', {});
92+
const header = 'subject,status,priority,category,due_date';
93+
const rows = tasks.map((t) =>
94+
[t.subject, t.status, t.priority, t.category, t.due_date ?? ''].join(','),
95+
);
96+
return [header, ...rows].join('\n');
97+
}

0 commit comments

Comments
 (0)