diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75d4c3e..7d1fa22 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,3 +19,9 @@ jobs: uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - uses: paambaati/codeclimate-action@v5.0.0 + continue-on-error: true + env: + CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} + with: + coverageLocations: ./coverage/*.xml:clover diff --git a/README.md b/README.md index e3e35ba..d91f97c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# Trello Kanban Board [![test](https://github.com/mayank1513/vscode-extension-trello-kanban-board/actions/workflows/test.yml/badge.svg)](https://github.com/mayank1513/vscode-extension-trello-kanban-board/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/mayank1513/vscode-extension-trello-kanban-board/graph/badge.svg)](https://codecov.io/gh/mayank1513/vscode-extension-trello-kanban-board) +# Trello Kanban Board + +[![test](https://github.com/mayank1513/vscode-extension-trello-kanban-board/actions/workflows/test.yml/badge.svg)](https://github.com/mayank1513/vscode-extension-trello-kanban-board/actions/workflows/test.yml) [![Maintainability](https://api.codeclimate.com/v1/badges/ac44e4371dd3a274285e/maintainability)](https://codeclimate.com/github/mayank1513/vscode-extension-trello-kanban-board/maintainability) [![codecov](https://codecov.io/gh/mayank1513/vscode-extension-trello-kanban-board/graph/badge.svg)](https://codecov.io/gh/mayank1513/vscode-extension-trello-kanban-board) > Organize your work/ideas with Trello like Kanban board! @@ -14,11 +16,9 @@ ✅ Intuitive drag and drop UI -## Installation - -Click here to install this extension. +✅ Awailable for web as well - https://vscode-extension-trello-kanban-board.vercel.app/ -OR +## Installation Visit [MarketPlace](https://marketplace.visualstudio.com/items?itemName=mayank1513.trello-kanban-task-board) diff --git a/extension/panel.ts b/extension/panel.ts index df2db20..92a7ef7 100644 --- a/extension/panel.ts +++ b/extension/panel.ts @@ -1,4 +1,4 @@ -import { Disposable, ExtensionContext, Uri, ViewColumn, WebviewPanel, window } from "vscode"; +import { Disposable, ExtensionContext, Memento, Uri, ViewColumn, Webview, WebviewPanel, window } from "vscode"; import { ScopeType, prefix } from "./constants"; import { MessageType } from "./interface"; @@ -32,11 +32,26 @@ export class Panel { private _setupWebView() { const { extensionUri, globalState, workspaceState } = this._context; const { webview } = this._panel; + + webview.html = this._getHTML(webview, extensionUri); + + this._panel.onDidDispose(this._dispose, null, this._disposables); + + // message listeners + const scope = this._scope; + const memento = scope === "Global" ? globalState : workspaceState; + webview.onDidReceiveMessage( + (message: MessageType) => this._messageHandler(message, memento, prefix, scope, webview), + undefined, + this._disposables + ); + } + + private _getHTML(webview: Webview, extensionUri: Uri) { const cssUri = webview.asWebviewUri(Uri.joinPath(extensionUri, "assets", "index.css")); const jsUri = webview.asWebviewUri(Uri.joinPath(extensionUri, "assets", "index.js")); const iconUri = webview.asWebviewUri(Uri.joinPath(extensionUri, "logo.png")); - - webview.html = ` + return ` @@ -51,54 +66,42 @@ export class Panel { `; + } - this._panel.onDidDispose( - () => { - this._panel.dispose(); - // Dispose of all disposables (i.e. commands) for the current webview panel - // using while loop as disposables might be added while disposing other disposables in the array - while (this._disposables.length) { - const disposable = this._disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - }, - null, - this._disposables - ); + private _dispose() { + this._panel.dispose(); + // Dispose of all disposables (i.e. commands) for the current webview panel + // using while loop as disposables might be added while disposing other disposables in the array + while (this._disposables.length) { + const disposable = this._disposables.pop(); + if (disposable) { + disposable.dispose(); + } + } + } - // message listeners - const scope = this._scope; - const momento = scope === "Global" ? globalState : workspaceState; - const key = prefix; - webview.onDidReceiveMessage( - (message: MessageType) => { - const { action, data, text } = message; - switch (action) { - case "load": - { - const data = momento.get(key) || { scope }; - webview.postMessage({ action: "load", data: { ...data, scope } } as MessageType); - } - break; - case "save": - momento.update(key, data); - break; - case "success": - case "info": - window.showInformationMessage(text || ""); - break; - case "warn": - window.showWarningMessage(text || ""); - break; - case "error": - window.showErrorMessage(text || ""); - break; + private _messageHandler(message: MessageType, memento: Memento, key: string, scope: string, webview: Webview) { + const { action, data, text } = message; + switch (action) { + case "load": + { + const data = memento.get(key) || { scope }; + webview.postMessage({ action: "load", data: { ...data, scope } } as MessageType); } - }, - undefined, - this._disposables - ); + break; + case "save": + memento.update(key, data); + break; + case "success": + case "info": + window.showInformationMessage(text || ""); + break; + case "warn": + window.showWarningMessage(text || ""); + break; + case "error": + window.showErrorMessage(text || ""); + break; + } } } diff --git a/package.json b/package.json index 91ada7d..5ff9a50 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "trello-kanban-task-board", "private": true, - "version": "0.0.0", + "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", @@ -13,37 +13,37 @@ "deploy": "npm run build && npm run build:extension && cd dist && vsce publish --yarn" }, "dependencies": { - "@mayank1513/fork-me": "^1.1.2", - "@paralleldrive/cuid2": "^2.2.2", + "@mayank1513/fork-me": "^2.0.0", + "nanoid": "^5.0.4", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", - "react-markdown": "^9.0.0", + "react-markdown": "^9.0.1", "react-toastify": "^9.1.3" }, "devDependencies": { - "@testing-library/jest-dom": "^6.1.3", - "@testing-library/react": "^14.0.0", - "@types/node": "^20.8.3", - "@types/react": "^18.2.25", - "@types/react-beautiful-dnd": "^13.1.5", - "@types/react-dom": "^18.2.11", - "@types/vscode": "^1.83.0", - "@types/vscode-webview": "^1.57.2", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "@vitejs/plugin-react": "^4.1.0", - "@vitest/coverage-v8": "^0.34.6", - "@vscode/vsce": "^2.21.1", - "eslint": "^8.51.0", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", + "@types/node": "^20.10.4", + "@types/react": "^18.2.45", + "@types/react-beautiful-dnd": "^13.1.7", + "@types/react-dom": "^18.2.17", + "@types/vscode": "^1.85.0", + "@types/vscode-webview": "^1.57.4", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.0.4", + "@vscode/vsce": "^2.22.0", + "eslint": "^8.55.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "jsdom": "^22.1.0", - "sass": "^1.69.0", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", - "vite": "^4.4.11", - "vite-tsconfig-paths": "^4.2.1", - "vitest": "^0.34.6" + "eslint-plugin-react-refresh": "^0.4.5", + "jsdom": "^23.0.1", + "sass": "^1.69.5", + "ts-node": "^10.9.2", + "typescript": "^5.3.3", + "vite": "^5.0.8", + "vite-tsconfig-paths": "^4.2.2", + "vitest": "^1.0.4" } } diff --git a/src/components/board.module.scss b/src/components/board.module.scss index 5d273d3..8853faf 100644 --- a/src/components/board.module.scss +++ b/src/components/board.module.scss @@ -17,6 +17,10 @@ } } +.main { + display: flex; +} + .theme1 { background: linear-gradient(-45deg, blue, red); } diff --git a/src/components/board.tsx b/src/components/board.tsx index 493e0bf..cef9488 100644 --- a/src/components/board.tsx +++ b/src/components/board.tsx @@ -4,61 +4,76 @@ import ColumnList from "./column-list"; import { DragDropContext, DropResult } from "react-beautiful-dnd"; import { ForkMe } from "@mayank1513/fork-me/server"; import "@mayank1513/fork-me/server/index.css"; +import { BoardType } from "@/interface"; +import DrawerToggle from "./drawer-toggle"; +import { useState } from "react"; +import Drawer from "./drawer"; export default function Board() { - const { setState, state } = useGlobalState(); + const { state, setState } = useGlobalState(); + const [open, setOpen] = useState(false); - const onDragEnd = (result: DropResult) => { - const { source, destination, draggableId } = result; - if (!destination) return; - - if (destination.droppableId === source.droppableId && destination.index === source.index) return; - - /** column id always starts with "column-" */ - const columns = [...state.columns]; - if (draggableId.startsWith("column-")) { - const column = columns.splice(source.index, 1)[0]; - columns.splice(destination.index, 0, column); - setState({ ...state, columns: columns }); - } else { - const sourceCol = columns.find((c) => c.id === source.droppableId); - let destinationCol; - if (destination.droppableId === "columns") { - destinationCol = columns[Math.min(destination.index, columns.length - 1)]; - } else { - destinationCol = columns.find((c) => c.id === destination.droppableId); - } - const taskId = sourceCol?.tasksIds.splice(source.index, 1)[0]; - const tasks = { ...state.tasks }; - if (taskId) { - tasks[taskId].columnId = destination.droppableId; - destinationCol?.tasksIds.splice(destination.index, 0, taskId); - setState({ ...state, columns }); - } - } - }; + const onDragEnd = (result: DropResult) => handleDragEnd(result, state, setState); return (
+ setOpen(!open)} isOpen={open} />

Trello Kanban Board: {state.scope}


- - {state.scope === "Browser" && ( - <> - - - Install VSCode Extension - - - )} +
+ + +
+ {state.scope === "Browser" && }
); } + +function handleDragEnd(result: DropResult, state: BoardType, setState: (state: BoardType) => void) { + const { source, destination, draggableId } = result; + if (!destination) return; + + if (destination.droppableId === source.droppableId && destination.index === source.index) return; + + /** column id always starts with "column-" */ + const columns = [...state.columns]; + if (draggableId.startsWith("column-")) { + const column = columns.splice(source.index, 1)[0]; + columns.splice(destination.index, 0, column); + setState({ ...state, columns: columns }); + } else { + const sourceCol = columns.find((c) => c.id === source.droppableId); + let destinationCol; + if (destination.droppableId === "columns") { + destinationCol = columns[Math.min(destination.index, columns.length - 1)]; + } else { + destinationCol = columns.find((c) => c.id === destination.droppableId); + } + const taskId = sourceCol?.tasksIds.splice(source.index, 1)[0]; + const tasks = { ...state.tasks }; + if (taskId) { + tasks[taskId].columnId = destination.droppableId; + destinationCol?.tasksIds.splice(destination.index, 0, taskId); + setState({ ...state, columns }); + } + } +} + +function BrowserOnlyUI() { + return ( + <> + + + Install VSCode Extension + + + ); +} diff --git a/src/components/column-list/column-list.module.scss b/src/components/column-list/column-list.module.scss index 91be971..4897014 100644 --- a/src/components/column-list/column-list.module.scss +++ b/src/components/column-list/column-list.module.scss @@ -37,10 +37,10 @@ opacity: 0.5; } .addTask { - margin: 5px; + margin: 0 5px; width: calc(100% - 10px); cursor: pointer; - padding: 5px; + padding: 3px 5px; } .addColumn { margin: 10px; @@ -52,7 +52,8 @@ .taskList { overflow-x: hidden; overflow-y: auto; - max-height: calc(100vh - 180px); + max-height: calc(100vh - 200px); + padding: 0; } .close { display: none; diff --git a/src/components/column-list/column.tsx b/src/components/column-list/column.tsx index 1b09ec5..e2b926f 100644 --- a/src/components/column-list/column.tsx +++ b/src/components/column-list/column.tsx @@ -3,21 +3,37 @@ import styles from "./column-list.module.scss"; import { Draggable, Droppable } from "react-beautiful-dnd"; import Task from "components/task"; import { useGlobalState } from "utils/context"; -import { createId } from "@paralleldrive/cuid2"; +import { nanoid } from "nanoid"; import ColumnHeader from "./column-header"; import { vscode } from "utils/vscode"; +import { useRef } from "react"; export default function Column({ column, index }: { column: ColumnType; index: number }) { const { state, setState } = useGlobalState(); + const listRef = useRef(null); const addTask = () => { const newTask: TaskType = { - id: createId(), + id: nanoid(), description: "", columnId: column.id, }; column.tasksIds = [...column.tasksIds, newTask.id]; setState({ ...state, tasks: { ...state.tasks, [newTask.id]: newTask }, columns: [...state.columns] }); vscode.toast(`New task created in ${column?.title} column!`, "success"); + setTimeout(() => { + // listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" }); + const newTaskElement = listRef.current?.children[listRef.current.children.length - 1] as HTMLLabelElement; + newTaskElement?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }); + newTaskElement?.click(); + setTimeout(() => { + newTaskElement?.getElementsByTagName("textarea")[0].focus(); + newTaskElement?.getElementsByTagName("textarea")[0].click(); + setTimeout( + () => newTaskElement?.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" }), + 100 + ); + }, 50); + }, 250); }; return ( @@ -33,12 +49,12 @@ export default function Column({ column, index }: { column: ColumnType; index: n

-
+
    {column.tasksIds.map((taskId, index) => ( ))} {provided1.placeholder} -
+ diff --git a/src/components/column-list/index.tsx b/src/components/column-list/index.tsx index e99f185..36a51a0 100644 --- a/src/components/column-list/index.tsx +++ b/src/components/column-list/index.tsx @@ -3,12 +3,12 @@ import { Droppable } from "react-beautiful-dnd"; import Column from "./column"; import { ColumnType } from "@/interface"; import { useGlobalState } from "utils/context"; -import { createId } from "@paralleldrive/cuid2"; +import { nanoid } from "nanoid"; export default function ColumnList({ columns }: { columns: ColumnType[] }) { const { state, setState } = useGlobalState(); const addColumn = () => - setState({ ...state, columns: [...state.columns, { id: "column-" + createId(), title: "", tasksIds: [] }] }); + setState({ ...state, columns: [...state.columns, { id: "column-" + nanoid(), title: "", tasksIds: [] }] }); return ( {(provided) => ( diff --git a/src/components/drawer-toggle/drawer-toggle.module.scss b/src/components/drawer-toggle/drawer-toggle.module.scss new file mode 100644 index 0000000..004ab14 --- /dev/null +++ b/src/components/drawer-toggle/drawer-toggle.module.scss @@ -0,0 +1,43 @@ +$tr: 5px; +.toggle { + height: 32px; + width: 45px; + display: inline-flex; + flex-flow: column nowrap; + justify-content: space-evenly; + padding: 0 5px; + margin: 10px 5px; + cursor: pointer; + transition: all 0.3s; + float: left; + * { + height: 4px; + background: #dff; + border-radius: 50%; + border: 1px solid #5555; + } + :first-child, + :last-child { + transform: none; + margin: 0 7px; + } + :nth-child(2) { + opacity: 1; + } +} + +.open { + transform: rotate(-180deg); + * { + background: red; + &:first-child { + transform: rotate(45deg) translate($tr, $tr); + } + &:last-child { + transform: rotate(-45deg) translate($tr, -$tr); + } + &:nth-child(2) { + opacity: 0; + } + } +} diff --git a/src/components/drawer-toggle/index.tsx b/src/components/drawer-toggle/index.tsx new file mode 100644 index 0000000..af4aa78 --- /dev/null +++ b/src/components/drawer-toggle/index.tsx @@ -0,0 +1,17 @@ +"use client"; +import styles from "./drawer-toggle.module.scss"; + +interface DrawerToggleProps { + isOpen: boolean; + toggle: () => void; +} + +export default function DrawerToggle({ toggle, isOpen }: DrawerToggleProps) { + return ( +
+
+
+
+
+ ); +} diff --git a/src/components/drawer/drawer.module.scss b/src/components/drawer/drawer.module.scss new file mode 100644 index 0000000..46a6705 --- /dev/null +++ b/src/components/drawer/drawer.module.scss @@ -0,0 +1,23 @@ +.drawer { + transition: all 0.3s; + width: 0; + opacity: 0; + box-shadow: 0 5px 5px gray; + ul { + padding: 0; + list-style-type: none; + } + a { + padding: 20px; + display: block; + color: inherit; + font-weight: 500; + &:hover { + box-shadow: 0 0 5px gray; + } + } +} +.open { + width: 320px; + opacity: 1; +} diff --git a/src/components/drawer/index.tsx b/src/components/drawer/index.tsx new file mode 100644 index 0000000..2286679 --- /dev/null +++ b/src/components/drawer/index.tsx @@ -0,0 +1,28 @@ +import styles from "./drawer.module.scss"; + +const links = [ + { + text: "🌟 Rate me on VSCode Marketplace", + href: "https://marketplace.visualstudio.com/items?itemName=mayank1513.trello-kanban-task-board", + }, + { text: "🌏 Web Version", href: "https://vscode-extension-trello-kanban-board.vercel.app/" }, + { text: "🌟 Star/Fork me on GitHub", href: "https://github.com/mayank1513/vscode-extension-trello-kanban-board" }, + { text: "📖 Learn", href: "https://mayank-chaudhari.vercel.app/courses" }, + { text: "🤝 Get in touch", href: "https://mayank-chaudhari.vercel.app/" }, +]; + +export default function Drawer({ open }: { open: boolean }) { + return ( + + ); +} diff --git a/src/components/task/index.tsx b/src/components/task/index.tsx index 1973e63..39d7aa5 100644 --- a/src/components/task/index.tsx +++ b/src/components/task/index.tsx @@ -51,7 +51,7 @@ export default function Task({ task, index }: { task: TaskType; index: number }) ref={provided.innerRef} {...provided.dragHandleProps} {...provided.draggableProps} - className={styles.task}> + className={[styles.task, isEditing ? styles.active : ""].join(" ")}>