Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add miro app card #21

Merged
merged 3 commits into from
Oct 12, 2023
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
8 changes: 8 additions & 0 deletions .changeset/perfect-bulldogs-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@dandori/core": patch
"@dandori/libs": patch
"@dandori/cli": patch
"@dandori/ui": patch
---

Add app miro card
24 changes: 14 additions & 10 deletions packages/cli/src/libs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,14 @@ import { Command } from "commander";
import chalk from "chalk";
import generateDandoriTasks, { DandoriTask } from "@dandori/core";
import { readFile } from "fs/promises";
import { generateDandoriFilePath } from "@dandori/libs";
import { generateDandoriFilePath, logger } from "@dandori/libs";

export class DandoriBaseCli {
private program: Command;
private inputFile: string = "";

constructor() {
this.program = new Command(packageJson.name)
.version(packageJson.version)
.argument("<input-file>")
.usage(`${chalk.green("<input-file>")} [options]`)
.option("-e, --env-file <env-file>")
.action((iFile) => {
this.inputFile = iFile;
});
this.program = this.buildCommand();
}

async generateDandoriTasks(): Promise<DandoriTask[]> {
Expand All @@ -29,9 +22,20 @@ export class DandoriBaseCli {
envFilePath: envFile,
});
if (!tasks) {
console.error("Failed to generate dandori tasks.");
logger.error("Failed to generate dandori tasks.");
process.exit(1);
}
return tasks;
}

protected buildCommand(): Command {
return new Command(packageJson.name)
.version(packageJson.version)
.argument("<input-file>")
.usage(`${chalk.green("<input-file>")} [options]`)
.option("-e, --env-file <env-file>")
.action((iFile) => {
this.inputFile = iFile;
});
}
}
11 changes: 10 additions & 1 deletion packages/cli/src/miro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
import { DandoriBaseCli } from "./libs";
import { generateDandoriMiroCards } from "@dandori/ui";

const cli = new DandoriBaseCli();
class DandoriMiroCli extends DandoriBaseCli {
override buildCommand() {
return super
.buildCommand()
.option("-a, --app-card")
.option("-b, --board-id <board-id>");
}
}

const cli = new DandoriMiroCli();
const tasks = await cli.generateDandoriTasks();
await generateDandoriMiroCards(tasks);
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"dependencies": {
"@dandori/libs": "workspace:*",
"dotenv": "^16.3.1",
"openai": "^4.11.1"
"openai": "^4.12.1"
}
}
53 changes: 32 additions & 21 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { configDotenv } from "dotenv";
import OpenAI from "openai";
import { generateDandoriFilePath, logger } from "@dandori/libs";
import {
generateDandoriFilePath,
logger,
runPromisesSequentially,
} from "@dandori/libs";
import { ChatCompletion } from "openai/src/resources/chat/completions";

export type ChatGPTFunctionCallModel = "gpt-3.5-turbo-0613" | "gpt-4-0613";

Expand Down Expand Up @@ -101,30 +106,36 @@ export default async function generateDandoriTasks(
});
const model: ChatGPTFunctionCallModel =
options?.chatGPTModel ?? defaultChatGPTFunctionCallModel;
const completion = await openai.chat.completions.create({
messages: [{ role: "user", content: source }],
model,
function_call: { name: functionCallName },
functions: [
{
name: functionCallName,
description: `Get the tasks flow which will be used like Gantt chart.`,
parameters: {
type: "object",
properties: {
tasks: {
type: "array",
items: {
const [completion] = await runPromisesSequentially(
[
() =>
openai.chat.completions.create({
messages: [{ role: "user", content: source }],
model,
function_call: { name: functionCallName },
functions: [
{
name: functionCallName,
description: `Get the tasks flow which will be used like Gantt chart.`,
parameters: {
type: "object",
required: requiredProperties,
properties: functionCallTaskProperties,
properties: {
tasks: {
type: "array",
items: {
type: "object",
required: requiredProperties,
properties: functionCallTaskProperties,
},
},
},
},
},
},
},
},
],
}) as unknown as Promise<ChatCompletion>,
],
});
"Generating tasks",
);

const resFunctionCall = completion.choices[0].message.function_call;
if (!resFunctionCall) {
Expand Down
1 change: 1 addition & 0 deletions packages/libs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./generateDandoriFilePath";
export * from "./logger";
export * from "./runPromisesSequentially";
22 changes: 22 additions & 0 deletions packages/libs/src/runPromisesSequentially.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { logger } from "./logger";

export async function runPromisesSequentially<T>(
runPromises: (() => Promise<T>)[],
runningLogPrefix: string,
): Promise<T[]> {
let currentRunPromiseCount = 0;
const maxRunPromiseCount = runPromises.length;
const timeId = setInterval(() => {
logger.info(
`${runningLogPrefix}... (${currentRunPromiseCount}/${maxRunPromiseCount})`,
);
}, 5000);
const results: T[] = [];
for (const runPromise of runPromises) {
results.push(await runPromise());
currentRunPromiseCount++;
}
clearInterval(timeId);
logger.info(`${runningLogPrefix}... Done!`);
return results;
}
137 changes: 67 additions & 70 deletions packages/ui/src/miro.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { MiroApi } from "@mirohq/miro-api";
import { DandoriTask } from "@dandori/core";
import FlatToNested from "flat-to-nested";
import TreeModel from "tree-model";
import { logger } from "@dandori/libs";
import TreeModel, { NodeVisitorFunction } from "tree-model";
import { logger, runPromisesSequentially } from "@dandori/libs";

export type GenerateDandoriMiroCardsOptions = {
miroAccessToken?: string;
boardId?: Parameters<MiroApi["getBoard"]>[0];
isAppCard?: boolean;
};

// miro settings
Expand All @@ -29,19 +30,13 @@ type DandoriTaskWithNextTasks = DandoriTask & {
[taskChildrenPropName]?: DandoriTaskWithNextTasks[];
};

async function runPromisesSequentially(runPromises: (() => Promise<void>)[]) {
let currentRunPromiseCount = 0;
const maxRunPromiseCount = runPromises.length;
const timeId = setInterval(() => {
logger.info(
`task running... (${currentRunPromiseCount}/${maxRunPromiseCount})`,
);
}, 5000);
for (const runPromise of runPromises) {
await runPromise();
currentRunPromiseCount++;
}
clearInterval(timeId);
function iterateBreadthNodes<T>(
nodes: TreeModel.Node<T>[],
callback: NodeVisitorFunction<T>,
): void {
nodes.forEach((node) => {
node.walk({ strategy: "breadth" }, callback, undefined);
});
}

export async function generateDandoriMiroCards(
Expand Down Expand Up @@ -77,66 +72,68 @@ export async function generateDandoriMiroCards(
const taskNodes = nestedTasks.map((nestedTask) => tree.parse(nestedTask));
const runCreateCardPromises: (() => Promise<void>)[] = [];
const taskIdCardIdMap = new Map<string, string>();
taskNodes.forEach((taskNode) => {
taskNode.walk(
{ strategy: "breadth" },
(node) => {
const task = node.model as DandoriTaskWithNextTasks;
runCreateCardPromises.push(async () => {
const card = await miroBoard.createCardItem({
data: {
title: task.name,
description: task.description,
dueDate: task.deadline ? new Date(task.deadline) : undefined,
},
geometry: {
width: defaultCardWidth,
height: defaultCardHeight,
},
position: {
x:
(defaultCardMarginX + defaultCardWidth) *
(node.getPath().length - 1),
y: (defaultCardMarginY + defaultCardHeight) * node.getIndex(),
},
});
taskIdCardIdMap.set(task.id, card.id);
});
return true;
iterateBreadthNodes(taskNodes, (node) => {
const task = node.model as DandoriTaskWithNextTasks;
const baseCardParams = {
data: {
title: task.name,
description: task.description,
},
geometry: {
width: defaultCardWidth,
height: defaultCardHeight,
},
position: {
x:
(defaultCardMarginX + defaultCardWidth) * (node.getPath().length - 1),
y: (defaultCardMarginY + defaultCardHeight) * node.getIndex(),
},
undefined,
);
};
const createCard = options?.isAppCard
? () => miroBoard.createAppCardItem(baseCardParams)
: () => {
const mergedDataParams = {
...baseCardParams.data,
dueDate: task.deadline ? new Date(task.deadline) : undefined,
};
return miroBoard.createCardItem({
...baseCardParams,
data: mergedDataParams,
});
};
runCreateCardPromises.push(async () => {
const card = await createCard();
taskIdCardIdMap.set(task.id, card.id);
});
return true;
});
await runPromisesSequentially(runCreateCardPromises);
await runPromisesSequentially(runCreateCardPromises, "Creating cards");

const runCreateConnectorPromises: (() => Promise<void>)[] = [];
taskNodes.forEach((taskNode) => {
taskNode.walk(
{ strategy: "breadth" },
(node) => {
const task = node.model as DandoriTaskWithNextTasks;
const nextTasks = task[taskChildrenPropName];
if (!nextTasks?.length) {
return true;
}
const startCardId = taskIdCardIdMap.get(task.id);
nextTasks.forEach((nextTask) => {
runCreateConnectorPromises.push(async () => {
await miroBoard.createConnector({
startItem: {
id: startCardId,
},
endItem: {
id: taskIdCardIdMap.get(nextTask.id),
},
});
});
iterateBreadthNodes(taskNodes, (node) => {
const task = node.model as DandoriTaskWithNextTasks;
const nextTasks = task[taskChildrenPropName];
if (!nextTasks?.length) {
return true;
}
const startCardId = taskIdCardIdMap.get(task.id);
nextTasks.forEach((nextTask) => {
runCreateConnectorPromises.push(async () => {
await miroBoard.createConnector({
startItem: {
id: startCardId,
},
endItem: {
id: taskIdCardIdMap.get(nextTask.id),
},
});
return true;
},
undefined,
);
});
});
return true;
});
await runPromisesSequentially(runCreateConnectorPromises);
await runPromisesSequentially(
runCreateConnectorPromises,
"Creating connectors",
);
logger.info("Create miro cards and connectors successfully!");
}
Loading