diff --git a/.changeset/perfect-bulldogs-happen.md b/.changeset/perfect-bulldogs-happen.md new file mode 100644 index 0000000..ebc0a1b --- /dev/null +++ b/.changeset/perfect-bulldogs-happen.md @@ -0,0 +1,8 @@ +--- +"@dandori/core": patch +"@dandori/libs": patch +"@dandori/cli": patch +"@dandori/ui": patch +--- + +Add app miro card diff --git a/packages/cli/src/libs.ts b/packages/cli/src/libs.ts index 5bae82f..4d2aee9 100644 --- a/packages/cli/src/libs.ts +++ b/packages/cli/src/libs.ts @@ -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("") - .usage(`${chalk.green("")} [options]`) - .option("-e, --env-file ") - .action((iFile) => { - this.inputFile = iFile; - }); + this.program = this.buildCommand(); } async generateDandoriTasks(): Promise { @@ -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("") + .usage(`${chalk.green("")} [options]`) + .option("-e, --env-file ") + .action((iFile) => { + this.inputFile = iFile; + }); + } } diff --git a/packages/cli/src/miro.ts b/packages/cli/src/miro.ts index 464b8c8..c91067a 100644 --- a/packages/cli/src/miro.ts +++ b/packages/cli/src/miro.ts @@ -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 "); + } +} + +const cli = new DandoriMiroCli(); const tasks = await cli.generateDandoriTasks(); await generateDandoriMiroCards(tasks); diff --git a/packages/core/package.json b/packages/core/package.json index c427112..d8415ec 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,6 +27,6 @@ "dependencies": { "@dandori/libs": "workspace:*", "dotenv": "^16.3.1", - "openai": "^4.11.1" + "openai": "^4.12.1" } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 089a0c9..810d7aa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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"; @@ -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, ], - }); + "Generating tasks", + ); const resFunctionCall = completion.choices[0].message.function_call; if (!resFunctionCall) { diff --git a/packages/libs/src/index.ts b/packages/libs/src/index.ts index be79d75..a9f06fb 100644 --- a/packages/libs/src/index.ts +++ b/packages/libs/src/index.ts @@ -1,2 +1,3 @@ export * from "./generateDandoriFilePath"; export * from "./logger"; +export * from "./runPromisesSequentially"; diff --git a/packages/libs/src/runPromisesSequentially.ts b/packages/libs/src/runPromisesSequentially.ts new file mode 100644 index 0000000..a8e5433 --- /dev/null +++ b/packages/libs/src/runPromisesSequentially.ts @@ -0,0 +1,22 @@ +import { logger } from "./logger"; + +export async function runPromisesSequentially( + runPromises: (() => Promise)[], + runningLogPrefix: string, +): Promise { + 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; +} diff --git a/packages/ui/src/miro.ts b/packages/ui/src/miro.ts index 0518cdc..e962368 100644 --- a/packages/ui/src/miro.ts +++ b/packages/ui/src/miro.ts @@ -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[0]; + isAppCard?: boolean; }; // miro settings @@ -29,19 +30,13 @@ type DandoriTaskWithNextTasks = DandoriTask & { [taskChildrenPropName]?: DandoriTaskWithNextTasks[]; }; -async function runPromisesSequentially(runPromises: (() => Promise)[]) { - 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( + nodes: TreeModel.Node[], + callback: NodeVisitorFunction, +): void { + nodes.forEach((node) => { + node.walk({ strategy: "breadth" }, callback, undefined); + }); } export async function generateDandoriMiroCards( @@ -77,66 +72,68 @@ export async function generateDandoriMiroCards( const taskNodes = nestedTasks.map((nestedTask) => tree.parse(nestedTask)); const runCreateCardPromises: (() => Promise)[] = []; const taskIdCardIdMap = new Map(); - 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)[] = []; - 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!"); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17061c9..1a062c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ importers: specifier: ^16.3.1 version: 16.3.1 openai: - specifier: ^4.11.1 - version: 4.11.1 + specifier: ^4.12.1 + version: 4.12.1 packages/libs: dependencies: @@ -146,8 +146,8 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/runtime@7.23.1: - resolution: {integrity: sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==} + /@babel/runtime@7.23.2: + resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 @@ -156,7 +156,7 @@ packages: /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/config': 2.3.1 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -174,7 +174,7 @@ packages: /@changesets/assemble-release-plan@5.2.4: resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 @@ -202,7 +202,7 @@ packages: resolution: {integrity: sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==} hasBin: true dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/apply-release-plan': 6.1.4 '@changesets/assemble-release-plan': 5.2.4 '@changesets/changelog-git': 0.1.14 @@ -217,7 +217,7 @@ packages: '@changesets/types': 5.2.1 '@changesets/write': 0.2.3 '@manypkg/get-packages': 1.1.3 - '@types/is-ci': 3.0.1 + '@types/is-ci': 3.0.2 '@types/semver': 7.5.3 ansi-colors: 4.1.3 chalk: 2.4.2 @@ -277,7 +277,7 @@ packages: /@changesets/get-release-plan@3.0.17: resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/assemble-release-plan': 5.2.4 '@changesets/config': 2.3.1 '@changesets/pre': 1.0.14 @@ -293,7 +293,7 @@ packages: /@changesets/git@2.0.0: resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -318,7 +318,7 @@ packages: /@changesets/pre@1.0.14: resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -328,7 +328,7 @@ packages: /@changesets/read@0.5.9: resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -349,7 +349,7 @@ packages: /@changesets/write@0.2.3: resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -644,7 +644,7 @@ packages: /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -653,7 +653,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.23.1 + '@babel/runtime': 7.23.2 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -693,8 +693,8 @@ packages: fastq: 1.15.0 dev: true - /@types/is-ci@3.0.1: - resolution: {integrity: sha512-mnb1ngaGQPm6LFZaNdh3xPOoQMkrQb/KBPhPPN2p2Wk8XgeUqWj6xPnvyQ8rvcK/VFritVmQG8tvQuy7g+9/nQ==} + /@types/is-ci@3.0.2: + resolution: {integrity: sha512-9PyP1rgCro6xO3R7zOEoMgx5U9HpLhIg1FFb9p2mWX/x5QI8KMuCWWYtCT1dUQpicp84OsxEAw3iqwIKQY5Pog==} dependencies: ci-info: 3.9.0 dev: true @@ -1628,7 +1628,7 @@ packages: dependencies: debug: 3.2.7 is-core-module: 2.13.0 - resolve: 1.22.6 + resolve: 1.22.8 transitivePeerDependencies: - supports-color dev: true @@ -2889,7 +2889,7 @@ packages: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.6 + resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 dev: true @@ -3004,8 +3004,8 @@ packages: mimic-fn: 4.0.0 dev: true - /openai@4.11.1: - resolution: {integrity: sha512-GU0HQWbejXuVAQlDjxIE8pohqnjptFDIm32aPlNT1H9ucMz1VJJD0DaTJRQsagNaJ97awWjjVLEG7zCM6sm4SA==} + /openai@4.12.1: + resolution: {integrity: sha512-EAoUwm4dtiWvFwBhOCK/VfF8sj1ZU8+aAIJnfT4NyeTfrt1DM/6Gdd6fOZWTjBYryTAqu9Vpb5+9Wu6JMtm/gA==} hasBin: true dependencies: '@types/node': 18.18.4 @@ -3434,8 +3434,8 @@ packages: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} dev: true - /resolve@1.22.6: - resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true dependencies: is-core-module: 2.13.0