Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/free-ducks-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@plotday/tool-outlook-calendar": patch
"@plotday/tool-google-calendar": patch
"@plotday/tool-linear": patch
"@plotday/tool-asana": patch
"@plotday/tool-slack": patch
"@plotday/tool-jira": patch
---

Fixed: Set author and assignee
11 changes: 11 additions & 0 deletions .changeset/quiet-ties-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@plotday/tool-outlook-calendar": patch
"@plotday/tool-google-calendar": patch
"@plotday/tool-linear": patch
"@plotday/tool-asana": patch
"@plotday/tool-slack": patch
"@plotday/tool-jira": patch
"@plotday/twister": patch
---

Added: created_at for item's original creation time in the source system
52 changes: 52 additions & 0 deletions tools/asana/src/asana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ActivityType,
type NewActivityWithNotes,
} from "@plotday/twister";
import type { Actor, ActorId, NewContact } from "@plotday/twister/plot";
import type {
Project,
ProjectAuth,
Expand Down Expand Up @@ -252,6 +253,14 @@ export class Asana extends Tool<Asana> implements ProjectTool {
"completed_at",
"created_at",
"modified_at",
"assignee",
"assignee.email",
"assignee.name",
"assignee.photo",
"created_by",
"created_by.email",
"created_by.name",
"created_by.photo",
].join(","),
};

Expand Down Expand Up @@ -317,6 +326,39 @@ export class Asana extends Tool<Asana> implements ProjectTool {
task: any,
projectId: string
): Promise<NewActivityWithNotes> {
const createdBy = task.created_by;
const assignee = task.assignee;

// Create contacts for created_by and assignee
const contacts: NewContact[] = [];
if (createdBy?.email) {
contacts.push({
email: createdBy.email,
name: createdBy.name,
avatar: createdBy.photo?.image_128x128,
});
}
if (assignee?.email && assignee.email !== createdBy?.email) {
contacts.push({
email: assignee.email,
name: assignee.name,
avatar: assignee.photo?.image_128x128,
});
}

let authorActor: Actor | undefined;
let assigneeActor: Actor | undefined;

if (contacts.length > 0) {
const actors = await this.tools.plot.addContacts(contacts);
if (createdBy?.email) {
authorActor = actors.find((a) => a.email === createdBy.email);
}
if (assignee?.email) {
assigneeActor = actors.find((a) => a.email === assignee.email);
}
}

// Build notes array: description
const notes: Array<{ content: string }> = [];

Expand All @@ -333,6 +375,8 @@ export class Asana extends Tool<Asana> implements ProjectTool {
type: ActivityType.Action,
title: task.name,
source: `asana:task:${projectId}:${task.gid}`,
author: authorActor,
assignee: assigneeActor,
doneAt: task.completed ? new Date(task.completed_at || Date.now()) : null,
notes,
};
Expand Down Expand Up @@ -489,6 +533,14 @@ export class Asana extends Tool<Asana> implements ProjectTool {
"completed_at",
"created_at",
"modified_at",
"assignee",
"assignee.email",
"assignee.name",
"assignee.photo",
"created_by",
"created_by.email",
"created_by.name",
"created_by.photo",
].join(","),
});

Expand Down
16 changes: 15 additions & 1 deletion tools/google-calendar/src/google-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,9 +428,22 @@ export class GoogleCalendar
continue;
}

// Extract and create contacts from attendees
// Extract and create contacts from organizer and attendees
let actorIds: ActorId[] = [];
let validAttendees: typeof event.attendees = [];
let authorActor = undefined;

// Create contact for organizer (author)
if (event.organizer?.email) {
const organizerContact: NewContact = {
email: event.organizer.email,
name: event.organizer.displayName,
};
const [author] = await this.tools.plot.addContacts([organizerContact]);
authorActor = author;
}

// Create contacts for attendees
if (event.attendees && event.attendees.length > 0) {
// Filter to get only valid attendees (with email, not resources)
validAttendees = event.attendees.filter(
Expand Down Expand Up @@ -557,6 +570,7 @@ export class GoogleCalendar
recurrenceCount: activityData.recurrenceCount || null,
doneAt: null,
title: activityData.title || null,
author: authorActor,
recurrenceRule: activityData.recurrenceRule || null,
recurrenceExdates: activityData.recurrenceExdates || null,
recurrenceDates: activityData.recurrenceDates || null,
Expand Down
67 changes: 45 additions & 22 deletions tools/jira/src/jira.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { Version3Client } from "jira.js";
import {
type ActivityLink,
ActivityType,
ActivityUpdate,
type NewActivityWithNotes,
} from "@plotday/twister";
import type { Actor, ActorId, NewContact } from "@plotday/twister/plot";
import type {
Project,
ProjectAuth,
Expand Down Expand Up @@ -90,11 +92,7 @@ export class Jira extends Tool<Jira> implements ProjectTool {
? R
: []
): Promise<ActivityLink> {
const jiraScopes = [
"read:jira-work",
"write:jira-work",
"read:jira-user",
];
const jiraScopes = ["read:jira-work", "write:jira-work", "read:jira-user"];

// Generate opaque token for authorization
const authToken = crypto.randomUUID();
Expand Down Expand Up @@ -155,19 +153,13 @@ export class Jira extends Tool<Jira> implements ProjectTool {
* Start syncing issues from a Jira project
*/
async startSync<
TCallback extends (
issue: NewActivityWithNotes,
...args: any[]
) => any
TCallback extends (issue: NewActivityWithNotes, ...args: any[]) => any
>(
authToken: string,
projectId: string,
callback: TCallback,
options?: ProjectSyncOptions,
...extraArgs: TCallback extends (
issue: any,
...rest: infer R
) => any
...extraArgs: TCallback extends (issue: any, ...rest: infer R) => any
? R
: []
): Promise<void> {
Expand Down Expand Up @@ -265,6 +257,8 @@ export class Jira extends Tool<Jira> implements ProjectTool {
"description",
"status",
"assignee",
"reporter",
"creator",
"comment",
"created",
"updated",
Expand All @@ -280,10 +274,7 @@ export class Jira extends Tool<Jira> implements ProjectTool {
// Set unread based on sync type (false for initial sync to avoid notification overload)
activityWithNotes.unread = !state.initialSync;
// Execute the callback using the callback token
await this.tools.callbacks.run(
callbackToken,
activityWithNotes
);
await this.tools.callbacks.run(callbackToken, activityWithNotes);
}

// Check if more pages
Expand All @@ -294,7 +285,8 @@ export class Jira extends Tool<Jira> implements ProjectTool {
await this.set(`sync_state_${projectId}`, {
startAt: nextStartAt,
batchNumber: state.batchNumber + 1,
issuesProcessed: state.issuesProcessed + (searchResult.issues?.length || 0),
issuesProcessed:
state.issuesProcessed + (searchResult.issues?.length || 0),
initialSync: state.initialSync,
});

Expand Down Expand Up @@ -322,6 +314,38 @@ export class Jira extends Tool<Jira> implements ProjectTool {
const fields = issue.fields || {};
const status = fields.status?.name;
const comments = fields.comment?.comments || [];
const reporter = fields.reporter || fields.creator;
const assignee = fields.assignee;

// Create contacts for reporter and assignee
const contacts: NewContact[] = [];
if (reporter?.emailAddress) {
contacts.push({
email: reporter.emailAddress,
name: reporter.displayName,
avatar: reporter.avatarUrls?.["48x48"],
});
}
if (assignee?.emailAddress && assignee.emailAddress !== reporter?.emailAddress) {
contacts.push({
email: assignee.emailAddress,
name: assignee.displayName,
avatar: assignee.avatarUrls?.["48x48"],
});
}

let authorActor: Actor | undefined;
let assigneeActor: Actor | undefined;

if (contacts.length > 0) {
const actors = await this.tools.plot.addContacts(contacts);
if (reporter?.emailAddress) {
authorActor = actors.find((a) => a.email === reporter.emailAddress);
}
if (assignee?.emailAddress) {
assigneeActor = actors.find((a) => a.email === assignee.emailAddress);
}
}

// Build notes array: description + comments
const notes: Array<{ content: string }> = [];
Expand Down Expand Up @@ -352,6 +376,8 @@ export class Jira extends Tool<Jira> implements ProjectTool {
type: ActivityType.Action,
title: fields.summary || issue.key,
source: `jira:issue:${projectId}:${issue.key}`,
author: authorActor,
assignee: assigneeActor,
doneAt:
status === "Done" || status === "Closed" || status === "Resolved"
? new Date()
Expand Down Expand Up @@ -396,10 +422,7 @@ export class Jira extends Tool<Jira> implements ProjectTool {
* @param authToken - Authorization token
* @param update - ActivityUpdate with changed fields
*/
async updateIssue(
authToken: string,
update: import("@plotday/twister").ActivityUpdate
): Promise<void> {
async updateIssue(authToken: string, update: ActivityUpdate): Promise<void> {
// Extract Jira issue key from source
const issueKey = update.source?.split(":").pop();
if (!issueKey) {
Expand Down
34 changes: 34 additions & 0 deletions tools/linear/src/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ActivityType,
type NewActivityWithNotes,
} from "@plotday/twister";
import type { Actor, ActorId, NewContact } from "@plotday/twister/plot";
import type {
Project,
ProjectAuth,
Expand Down Expand Up @@ -320,9 +321,40 @@ export class Linear extends Tool<Linear> implements ProjectTool {
projectId: string
): Promise<NewActivityWithNotes> {
const state = await issue.state;
const creator = await issue.creator;
const assignee = await issue.assignee;
const comments = await issue.comments();

// Create contacts for creator and assignee
const contacts: NewContact[] = [];
if (creator?.email) {
contacts.push({
email: creator.email,
name: creator.name,
avatar: creator.avatarUrl,
});
}
if (assignee?.email && assignee.email !== creator?.email) {
contacts.push({
email: assignee.email,
name: assignee.name,
avatar: assignee.avatarUrl,
});
}

let authorActor: Actor | undefined;
let assigneeActor: Actor | undefined;

if (contacts.length > 0) {
const actors = await this.tools.plot.addContacts(contacts);
if (creator?.email) {
authorActor = actors.find((a) => a.email === creator.email);
}
if (assignee?.email) {
assigneeActor = actors.find((a) => a.email === assignee.email);
}
}

// Build notes array: description + comments
const notes: Array<{ content: string }> = [];

Expand All @@ -338,6 +370,8 @@ export class Linear extends Tool<Linear> implements ProjectTool {
type: ActivityType.Action,
title: issue.title,
source: `linear:issue:${projectId}:${issue.id}`,
author: authorActor,
assignee: assigneeActor,
doneAt:
state?.name === "Done" || state?.name === "Completed"
? new Date()
Expand Down
16 changes: 15 additions & 1 deletion tools/outlook-calendar/src/outlook-calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,22 @@ export class OutlookCalendar
continue;
}

// Extract and create contacts from attendees
// Extract and create contacts from organizer and attendees
let actorIds: ActorId[] = [];
let validAttendees: typeof outlookEvent.attendees = [];
let authorActor = undefined;

// Create contact for organizer (author)
if (outlookEvent.organizer?.emailAddress?.address) {
const organizerContact: NewContact = {
email: outlookEvent.organizer.emailAddress.address,
name: outlookEvent.organizer.emailAddress.name,
};
const [author] = await this.tools.plot.addContacts([organizerContact]);
authorActor = author;
}

// Create contacts for attendees
if (outlookEvent.attendees && outlookEvent.attendees.length > 0) {
// Filter to get only valid attendees (with email, not resources)
validAttendees = outlookEvent.attendees.filter(
Expand Down Expand Up @@ -491,6 +504,7 @@ export class OutlookCalendar
// Build NewActivityWithNotes from the transformed activity
const activityWithNotes: NewActivityWithNotes = {
...activity,
author: authorActor,
meta: activity.meta,
tags: tags && Object.keys(tags).length > 0 ? tags : activity.tags,
notes,
Expand Down
Loading