Skip to content

Commit

Permalink
Feat: add task to task occurence for working day
Browse files Browse the repository at this point in the history
  • Loading branch information
saimanoj committed Mar 5, 2025
1 parent 7e2b32d commit 53971e9
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "TaskOccurrence" ADD COLUMN "pageId" TEXT;

-- AddForeignKey
ALTER TABLE "TaskOccurrence" ADD CONSTRAINT "TaskOccurrence_pageId_fkey" FOREIGN KEY ("pageId") REFERENCES "Page"("id") ON DELETE SET NULL ON UPDATE CASCADE;
12 changes: 8 additions & 4 deletions apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,11 @@ model Page {
statusId String? // Status
status Status? @relation(fields: [statusId], references: [id])
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
task Task[]
conversation Conversation[]
workspaceId String
workspace Workspace @relation(fields: [workspaceId], references: [id])
task Task[]
conversation Conversation[]
taskOccurrence TaskOccurrence[]
}

model PersonalAccessToken {
Expand Down Expand Up @@ -371,6 +372,9 @@ model TaskOccurrence {
task Task @relation(fields: [taskId], references: [id])
taskId String
page Page? @relation(fields: [pageId], references: [id])
pageId String?
workspace Workspace @relation(fields: [workspaceId], references: [id])
workspaceId String
Expand Down
16 changes: 1 addition & 15 deletions apps/server/src/modules/content/content.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Database } from '@hocuspocus/extension-database';
import { Hocuspocus, Server } from '@hocuspocus/server';
import { TiptapTransformer } from '@hocuspocus/transformer';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { getSchema } from '@sigma/editor-extensions';
import { PrismaService } from 'nestjs-prisma';
import { prosemirrorJSONToYXmlFragment } from 'y-prosemirror';
Expand All @@ -14,10 +13,7 @@ export class ContentService implements OnModuleInit {
private readonly logger: LoggerService = new LoggerService('ContentGateway');
private server: Hocuspocus;

constructor(
private prisma: PrismaService,
private eventEmitter: EventEmitter2,
) {}
constructor(private prisma: PrismaService) {}

async onModuleInit() {
const loggerScope = this.logger;
Expand Down Expand Up @@ -48,16 +44,6 @@ export class ContentService implements OnModuleInit {
),
},
});

this.eventEmitter.emit('task.update.tasksFromPage', {
pageId: documentName,
tiptapJson: TiptapTransformer.fromYdoc(document).default,
});

this.eventEmitter.emit('page.storeOutlinks', {
pageId: documentName,
tiptapJson: TiptapTransformer.fromYdoc(document).default,
});
},
}),
],
Expand Down
133 changes: 110 additions & 23 deletions apps/server/src/modules/pages/pages.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
convertHtmlToTiptapJson,
convertTiptapJsonToHtml,
Expand All @@ -16,14 +15,17 @@ import {
UpdatePageDto,
enchancePrompt,
MoveTaskToPageDto,
JsonObject,
} from '@sigma/types';
import { parse } from 'date-fns';
import { PrismaService } from 'nestjs-prisma';

import AIRequestsService from 'modules/ai-requests/ai-requests.services';
import { ContentService } from 'modules/content/content.service';
import { TransactionClient } from 'modules/tasks/tasks.utils';

import {
getOutlinksTaskId,
getTaskExtensionInPage,
removeTaskInExtension,
updateTaskExtensionInPage,
Expand Down Expand Up @@ -279,55 +281,140 @@ export class PagesService {
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async storeOutlinks(tiptapJson: any, pageId: string) {
async storeOutlinks(pageId: string) {
const page = await this.prisma.page.findUnique({ where: { id: pageId } });
const tiptapJson = JSON.parse(page.description);

// Parse outlinks from string to JSON
const pageOutlinks = page.outlinks ? page.outlinks : [];

// Extract taskIds from current outlinks
const currentOutlinkTaskIds = getOutlinksTaskId(pageOutlinks);
const currentTaskExtensionIds = getOutlinksTaskId(pageOutlinks, true);

const outlinks: Outlink[] = [];
const newOutlinkTaskIds: string[] = [];
const taskExtensionOutlinkIndices = new Set<number>();

// Recursive function to traverse the JSON and find task nodes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const findTasks = (node: any, path: number[] = []) => {
// Single-pass traversal to collect outlinks and mark those in taskExtension
const traverseDocument = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node: any,
path: number[] = [],
isInTaskExtension = false,
) => {
// Check if we're entering a taskExtension node
const inTaskExtension =
isInTaskExtension || node.type === 'tasksExtension';

// If this is a task node, create an outlink
if (node.type === 'task' && node.attrs?.id) {
const outlinkIndex = outlinks.length;

// Add the outlink
outlinks.push({
type: OutlinkType.Task,
id: node.attrs.id,
position: {
path: [...path],
},
// Only set taskExtension:true if this task is directly inside a taskExtension node
taskExtension: inTaskExtension,
});
newOutlinkTaskIds.push(node.attrs.id);

// If in taskExtension, mark this outlink's index
if (inTaskExtension) {
taskExtensionOutlinkIndices.add(outlinkIndex);
}
}

// Recursively process content
if (node.content && Array.isArray(node.content)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
node.content.forEach((child: any, index: number) => {
findTasks(child, [...path, index]);
traverseDocument(child, [...path, index], inTaskExtension);
});
}
};

// Start traversing from the root
// Process if content exists
if (tiptapJson.content) {
// Single pass traversal
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tiptapJson.content.forEach((node: any, index: number) => {
findTasks(node, [index]);
traverseDocument(node, [index], false);
});
}

// Update the page with the new outlinks
await this.prisma.page.update({
where: { id: pageId },
data: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outlinks: outlinks as any,
},
});
// Extract task IDs from taskExtension outlinks
const taskExtensionTaskIds = new Set(
Array.from(taskExtensionOutlinkIndices).map(
(index) => outlinks[index].id,
),
);

const taskIdsAreEqual =
currentOutlinkTaskIds.length === newOutlinkTaskIds.length &&
currentOutlinkTaskIds.every((id) => newOutlinkTaskIds.includes(id));

if (!taskIdsAreEqual) {
// Update the page with the new outlinks
await this.prisma.page.update({
where: { id: pageId },
data: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outlinks: outlinks as any,
},
});

if (page.type === 'Daily') {
// Compare current task IDs with task extension task IDs
const removedTaskIds = currentTaskExtensionIds.filter(
(taskId) => !taskExtensionTaskIds.has(taskId),
);
const addedTaskIds = Array.from(taskExtensionTaskIds).filter(
(taskId) => !currentTaskExtensionIds.includes(taskId),
);

if (removedTaskIds.length) {
await this.prisma.taskOccurrence.updateMany({
where: {
taskId: { in: removedTaskIds },
},
data: { deleted: new Date().toISOString() },
});
}

if (addedTaskIds.length) {
const pageDate = parse(page.title, 'dd-MM-yyyy', new Date());

const startTime = new Date(pageDate);
startTime.setHours(0, 0, 0, 0);

const endTime = new Date(pageDate);
endTime.setHours(23, 59, 59, 999);

await this.prisma.taskOccurrence.createMany({
data: addedTaskIds.map((taskId) => ({
taskId,
pageId,
workspaceId: page.workspaceId,
startTime,
endTime,
})),
});
}
}
}

return { outlinks, taskExtensionTaskIds: Array.from(taskExtensionTaskIds) };
}

@OnEvent('page.storeOutlinks')
async handleStoreOutlinks(payload: {
pageId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tiptapJson: any;
}) {
await this.storeOutlinks(payload.tiptapJson, payload.pageId);
async handleHooks(payload: { pageId: string; changedData: JsonObject }) {
if (payload.changedData.description) {
await this.storeOutlinks(payload.pageId);
}
}

async moveTaskToPage(moveTaskToPageData: MoveTaskToPageDto) {
Expand Down
25 changes: 24 additions & 1 deletion apps/server/src/modules/pages/pages.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Page, Task } from '@sigma/types';
import { JsonValue, Page, Task } from '@sigma/types';

export function getTaskExtensionInPage(page: Page) {
const description = page.description;
Expand Down Expand Up @@ -212,3 +212,26 @@ export function removeTaskInExtension(

return taskExtensions;
}

export function getOutlinksTaskId(
pageOutlinks: JsonValue,
taskExtensionOnly: boolean = false,
) {
// Extract taskIds from current outlinks
if (!pageOutlinks || !Array.isArray(pageOutlinks)) {
return [];
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (
pageOutlinks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((outlink: any) => {
// If taskExtensionOnly is true, only include tasks that are in task extensions
// Otherwise include all tasks
return !taskExtensionOnly || outlink.taskExtension === true;
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((outlink: any) => outlink.id)
);
}
2 changes: 2 additions & 0 deletions apps/server/src/modules/replication/replication.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ export const tablesToSendMessagesFor = new Map([
[ModelNameEnum.ConversationHistory, true],
[ModelNameEnum.List, true],
]);

export const tableHooks = new Map([[ModelNameEnum.Page, true]]);
3 changes: 2 additions & 1 deletion apps/server/src/modules/replication/replication.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

import { PagesModule } from 'modules/pages/pages.module';
import { SyncModule } from 'modules/sync/sync.module';
import SyncActionsService from 'modules/sync-actions/sync-actions.service';

import ReplicationService from './replication.service';

@Module({
imports: [SyncModule],
imports: [SyncModule, PagesModule],
controllers: [],
providers: [ReplicationService, ConfigService, SyncActionsService],
exports: [],
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/modules/replication/replication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
import { v4 as uuidv4 } from 'uuid';

import { LoggerService } from 'modules/logger/logger.service';
import { PagesService } from 'modules/pages/pages.service';
import { SyncGateway } from 'modules/sync/sync.gateway';
import SyncActionsService from 'modules/sync-actions/sync-actions.service';

import {
logChangeType,
logType,
tableHooks,
tablesToSendMessagesFor,
} from './replication.interface';

Expand All @@ -32,6 +34,7 @@ export default class ReplicationService {
private configService: ConfigService,
private syncGateway: SyncGateway,
private syncActionsService: SyncActionsService,
private pagesService: PagesService,
) {
this.client = new Client({
user: configService.get('POSTGRES_USER'),
Expand Down Expand Up @@ -226,6 +229,12 @@ export default class ReplicationService {
.to(recipientId)
.emit('message', JSON.stringify(syncActionData));
}

if (tableHooks.has(modelName)) {
const changedData = this.getChangedData(change);

this.pagesService.handleHooks({ pageId: modelId, changedData });
}
});
} else {
this.logger.info({ message: 'No change data in log' });
Expand Down
19 changes: 0 additions & 19 deletions apps/server/src/modules/tasks-hook/tasks-hook.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Outlink, OutlinkType, Task, TaskHookContext } from '@sigma/types';
import { tasks } from '@trigger.dev/sdk/v3';
import { format } from 'date-fns/format';
import { PrismaService } from 'nestjs-prisma';
import { beautifyTask } from 'triggers/task/beautify-task';
import { generateSummaryTask } from 'triggers/task/generate-summary';
Expand Down Expand Up @@ -35,7 +34,6 @@ export class TaskHooksService {
// Only trigger when task is CUD from the API without transaction
if (!tx) {
await Promise.all([
this.handleDueDate(task, context),
this.handleTitleChange(task, context),
this.handleDeleteTask(task, context),
this.handleScheduleTask(task, context, tx),
Expand Down Expand Up @@ -76,23 +74,6 @@ export class TaskHooksService {
}
}

private async handleDueDate(task: Task, context: TaskHookContext) {
switch (context.action) {
case 'delete':
if (task.dueDate) {
const formattedDate = format(task.dueDate, 'dd-MM-yyyy');
await this.pagesService.removeTaskFromPageByTitle(
formattedDate,
task.id,
);
}
return { message: 'Handled duedate delete' };

default:
return { message: `Unhandled duedate case ${context.action}` };
}
}

async handleTitleChange(task: Task, context: TaskHookContext) {
if (
context.previousTask &&
Expand Down
Loading

0 comments on commit 53971e9

Please sign in to comment.