kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(columns): add column management

Andrej 189236cd c2020de7

+3908 -108
+30
apps/api/drizzle/0012_mixed_thor_girl.sql
··· 1 + CREATE TABLE "column" ( 2 + "id" text PRIMARY KEY NOT NULL, 3 + "project_id" text NOT NULL, 4 + "name" text NOT NULL, 5 + "slug" text NOT NULL, 6 + "position" integer DEFAULT 0 NOT NULL, 7 + "icon" text, 8 + "color" text, 9 + "is_final" boolean DEFAULT false NOT NULL, 10 + "created_at" timestamp DEFAULT now() NOT NULL, 11 + "updated_at" timestamp DEFAULT now() NOT NULL 12 + ); 13 + --> statement-breakpoint 14 + CREATE TABLE "workflow_rule" ( 15 + "id" text PRIMARY KEY NOT NULL, 16 + "project_id" text NOT NULL, 17 + "integration_type" text NOT NULL, 18 + "event_type" text NOT NULL, 19 + "column_id" text NOT NULL, 20 + "created_at" timestamp DEFAULT now() NOT NULL, 21 + "updated_at" timestamp DEFAULT now() NOT NULL 22 + ); 23 + --> statement-breakpoint 24 + ALTER TABLE "task" ADD COLUMN "column_id" text;--> statement-breakpoint 25 + ALTER TABLE "column" ADD CONSTRAINT "column_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint 26 + ALTER TABLE "workflow_rule" ADD CONSTRAINT "workflow_rule_project_id_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."project"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint 27 + ALTER TABLE "workflow_rule" ADD CONSTRAINT "workflow_rule_column_id_column_id_fk" FOREIGN KEY ("column_id") REFERENCES "public"."column"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint 28 + CREATE INDEX "column_projectId_idx" ON "column" USING btree ("project_id");--> statement-breakpoint 29 + CREATE INDEX "workflow_rule_projectId_idx" ON "workflow_rule" USING btree ("project_id");--> statement-breakpoint 30 + ALTER TABLE "task" ADD CONSTRAINT "task_column_id_column_id_fk" FOREIGN KEY ("column_id") REFERENCES "public"."column"("id") ON DELETE set null ON UPDATE cascade;
+2040
apps/api/drizzle/meta/0012_snapshot.json
··· 1 + { 2 + "id": "a8fd55db-33b9-40a0-892e-a11e36505fc0", 3 + "prevId": "95fa9105-bddc-4049-b23d-2c413b803c23", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.account": { 8 + "name": "account", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "text", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "account_id": { 18 + "name": "account_id", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true 22 + }, 23 + "provider_id": { 24 + "name": "provider_id", 25 + "type": "text", 26 + "primaryKey": false, 27 + "notNull": true 28 + }, 29 + "user_id": { 30 + "name": "user_id", 31 + "type": "text", 32 + "primaryKey": false, 33 + "notNull": true 34 + }, 35 + "access_token": { 36 + "name": "access_token", 37 + "type": "text", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "refresh_token": { 42 + "name": "refresh_token", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": false 46 + }, 47 + "id_token": { 48 + "name": "id_token", 49 + "type": "text", 50 + "primaryKey": false, 51 + "notNull": false 52 + }, 53 + "access_token_expires_at": { 54 + "name": "access_token_expires_at", 55 + "type": "timestamp", 56 + "primaryKey": false, 57 + "notNull": false 58 + }, 59 + "refresh_token_expires_at": { 60 + "name": "refresh_token_expires_at", 61 + "type": "timestamp", 62 + "primaryKey": false, 63 + "notNull": false 64 + }, 65 + "scope": { 66 + "name": "scope", 67 + "type": "text", 68 + "primaryKey": false, 69 + "notNull": false 70 + }, 71 + "password": { 72 + "name": "password", 73 + "type": "text", 74 + "primaryKey": false, 75 + "notNull": false 76 + }, 77 + "created_at": { 78 + "name": "created_at", 79 + "type": "timestamp", 80 + "primaryKey": false, 81 + "notNull": true, 82 + "default": "now()" 83 + }, 84 + "updated_at": { 85 + "name": "updated_at", 86 + "type": "timestamp", 87 + "primaryKey": false, 88 + "notNull": true 89 + } 90 + }, 91 + "indexes": { 92 + "account_userId_idx": { 93 + "name": "account_userId_idx", 94 + "columns": [ 95 + { 96 + "expression": "user_id", 97 + "isExpression": false, 98 + "asc": true, 99 + "nulls": "last" 100 + } 101 + ], 102 + "isUnique": false, 103 + "concurrently": false, 104 + "method": "btree", 105 + "with": {} 106 + } 107 + }, 108 + "foreignKeys": { 109 + "account_user_id_user_id_fk": { 110 + "name": "account_user_id_user_id_fk", 111 + "tableFrom": "account", 112 + "tableTo": "user", 113 + "columnsFrom": ["user_id"], 114 + "columnsTo": ["id"], 115 + "onDelete": "cascade", 116 + "onUpdate": "no action" 117 + } 118 + }, 119 + "compositePrimaryKeys": {}, 120 + "uniqueConstraints": {}, 121 + "policies": {}, 122 + "checkConstraints": {}, 123 + "isRLSEnabled": false 124 + }, 125 + "public.activity": { 126 + "name": "activity", 127 + "schema": "", 128 + "columns": { 129 + "id": { 130 + "name": "id", 131 + "type": "text", 132 + "primaryKey": true, 133 + "notNull": true 134 + }, 135 + "task_id": { 136 + "name": "task_id", 137 + "type": "text", 138 + "primaryKey": false, 139 + "notNull": true 140 + }, 141 + "type": { 142 + "name": "type", 143 + "type": "text", 144 + "primaryKey": false, 145 + "notNull": true 146 + }, 147 + "created_at": { 148 + "name": "created_at", 149 + "type": "timestamp", 150 + "primaryKey": false, 151 + "notNull": true, 152 + "default": "now()" 153 + }, 154 + "user_id": { 155 + "name": "user_id", 156 + "type": "text", 157 + "primaryKey": false, 158 + "notNull": false 159 + }, 160 + "content": { 161 + "name": "content", 162 + "type": "text", 163 + "primaryKey": false, 164 + "notNull": false 165 + }, 166 + "external_user_name": { 167 + "name": "external_user_name", 168 + "type": "text", 169 + "primaryKey": false, 170 + "notNull": false 171 + }, 172 + "external_user_avatar": { 173 + "name": "external_user_avatar", 174 + "type": "text", 175 + "primaryKey": false, 176 + "notNull": false 177 + }, 178 + "external_source": { 179 + "name": "external_source", 180 + "type": "text", 181 + "primaryKey": false, 182 + "notNull": false 183 + }, 184 + "external_url": { 185 + "name": "external_url", 186 + "type": "text", 187 + "primaryKey": false, 188 + "notNull": false 189 + } 190 + }, 191 + "indexes": {}, 192 + "foreignKeys": { 193 + "activity_task_id_task_id_fk": { 194 + "name": "activity_task_id_task_id_fk", 195 + "tableFrom": "activity", 196 + "tableTo": "task", 197 + "columnsFrom": ["task_id"], 198 + "columnsTo": ["id"], 199 + "onDelete": "cascade", 200 + "onUpdate": "cascade" 201 + }, 202 + "activity_user_id_user_id_fk": { 203 + "name": "activity_user_id_user_id_fk", 204 + "tableFrom": "activity", 205 + "tableTo": "user", 206 + "columnsFrom": ["user_id"], 207 + "columnsTo": ["id"], 208 + "onDelete": "cascade", 209 + "onUpdate": "cascade" 210 + } 211 + }, 212 + "compositePrimaryKeys": {}, 213 + "uniqueConstraints": {}, 214 + "policies": {}, 215 + "checkConstraints": {}, 216 + "isRLSEnabled": false 217 + }, 218 + "public.apikey": { 219 + "name": "apikey", 220 + "schema": "", 221 + "columns": { 222 + "id": { 223 + "name": "id", 224 + "type": "text", 225 + "primaryKey": true, 226 + "notNull": true 227 + }, 228 + "name": { 229 + "name": "name", 230 + "type": "text", 231 + "primaryKey": false, 232 + "notNull": false 233 + }, 234 + "start": { 235 + "name": "start", 236 + "type": "text", 237 + "primaryKey": false, 238 + "notNull": false 239 + }, 240 + "prefix": { 241 + "name": "prefix", 242 + "type": "text", 243 + "primaryKey": false, 244 + "notNull": false 245 + }, 246 + "key": { 247 + "name": "key", 248 + "type": "text", 249 + "primaryKey": false, 250 + "notNull": true 251 + }, 252 + "user_id": { 253 + "name": "user_id", 254 + "type": "text", 255 + "primaryKey": false, 256 + "notNull": true 257 + }, 258 + "refill_interval": { 259 + "name": "refill_interval", 260 + "type": "integer", 261 + "primaryKey": false, 262 + "notNull": false 263 + }, 264 + "refill_amount": { 265 + "name": "refill_amount", 266 + "type": "integer", 267 + "primaryKey": false, 268 + "notNull": false 269 + }, 270 + "last_refill_at": { 271 + "name": "last_refill_at", 272 + "type": "timestamp", 273 + "primaryKey": false, 274 + "notNull": false 275 + }, 276 + "enabled": { 277 + "name": "enabled", 278 + "type": "boolean", 279 + "primaryKey": false, 280 + "notNull": false, 281 + "default": true 282 + }, 283 + "rate_limit_enabled": { 284 + "name": "rate_limit_enabled", 285 + "type": "boolean", 286 + "primaryKey": false, 287 + "notNull": false, 288 + "default": true 289 + }, 290 + "rate_limit_time_window": { 291 + "name": "rate_limit_time_window", 292 + "type": "integer", 293 + "primaryKey": false, 294 + "notNull": false, 295 + "default": 86400000 296 + }, 297 + "rate_limit_max": { 298 + "name": "rate_limit_max", 299 + "type": "integer", 300 + "primaryKey": false, 301 + "notNull": false, 302 + "default": 10 303 + }, 304 + "request_count": { 305 + "name": "request_count", 306 + "type": "integer", 307 + "primaryKey": false, 308 + "notNull": false, 309 + "default": 0 310 + }, 311 + "remaining": { 312 + "name": "remaining", 313 + "type": "integer", 314 + "primaryKey": false, 315 + "notNull": false 316 + }, 317 + "last_request": { 318 + "name": "last_request", 319 + "type": "timestamp", 320 + "primaryKey": false, 321 + "notNull": false 322 + }, 323 + "expires_at": { 324 + "name": "expires_at", 325 + "type": "timestamp", 326 + "primaryKey": false, 327 + "notNull": false 328 + }, 329 + "created_at": { 330 + "name": "created_at", 331 + "type": "timestamp", 332 + "primaryKey": false, 333 + "notNull": true 334 + }, 335 + "updated_at": { 336 + "name": "updated_at", 337 + "type": "timestamp", 338 + "primaryKey": false, 339 + "notNull": true 340 + }, 341 + "permissions": { 342 + "name": "permissions", 343 + "type": "text", 344 + "primaryKey": false, 345 + "notNull": false 346 + }, 347 + "metadata": { 348 + "name": "metadata", 349 + "type": "text", 350 + "primaryKey": false, 351 + "notNull": false 352 + } 353 + }, 354 + "indexes": { 355 + "apikey_key_idx": { 356 + "name": "apikey_key_idx", 357 + "columns": [ 358 + { 359 + "expression": "key", 360 + "isExpression": false, 361 + "asc": true, 362 + "nulls": "last" 363 + } 364 + ], 365 + "isUnique": false, 366 + "concurrently": false, 367 + "method": "btree", 368 + "with": {} 369 + }, 370 + "apikey_userId_idx": { 371 + "name": "apikey_userId_idx", 372 + "columns": [ 373 + { 374 + "expression": "user_id", 375 + "isExpression": false, 376 + "asc": true, 377 + "nulls": "last" 378 + } 379 + ], 380 + "isUnique": false, 381 + "concurrently": false, 382 + "method": "btree", 383 + "with": {} 384 + } 385 + }, 386 + "foreignKeys": { 387 + "apikey_user_id_user_id_fk": { 388 + "name": "apikey_user_id_user_id_fk", 389 + "tableFrom": "apikey", 390 + "tableTo": "user", 391 + "columnsFrom": ["user_id"], 392 + "columnsTo": ["id"], 393 + "onDelete": "cascade", 394 + "onUpdate": "no action" 395 + } 396 + }, 397 + "compositePrimaryKeys": {}, 398 + "uniqueConstraints": {}, 399 + "policies": {}, 400 + "checkConstraints": {}, 401 + "isRLSEnabled": false 402 + }, 403 + "public.column": { 404 + "name": "column", 405 + "schema": "", 406 + "columns": { 407 + "id": { 408 + "name": "id", 409 + "type": "text", 410 + "primaryKey": true, 411 + "notNull": true 412 + }, 413 + "project_id": { 414 + "name": "project_id", 415 + "type": "text", 416 + "primaryKey": false, 417 + "notNull": true 418 + }, 419 + "name": { 420 + "name": "name", 421 + "type": "text", 422 + "primaryKey": false, 423 + "notNull": true 424 + }, 425 + "slug": { 426 + "name": "slug", 427 + "type": "text", 428 + "primaryKey": false, 429 + "notNull": true 430 + }, 431 + "position": { 432 + "name": "position", 433 + "type": "integer", 434 + "primaryKey": false, 435 + "notNull": true, 436 + "default": 0 437 + }, 438 + "icon": { 439 + "name": "icon", 440 + "type": "text", 441 + "primaryKey": false, 442 + "notNull": false 443 + }, 444 + "color": { 445 + "name": "color", 446 + "type": "text", 447 + "primaryKey": false, 448 + "notNull": false 449 + }, 450 + "is_final": { 451 + "name": "is_final", 452 + "type": "boolean", 453 + "primaryKey": false, 454 + "notNull": true, 455 + "default": false 456 + }, 457 + "created_at": { 458 + "name": "created_at", 459 + "type": "timestamp", 460 + "primaryKey": false, 461 + "notNull": true, 462 + "default": "now()" 463 + }, 464 + "updated_at": { 465 + "name": "updated_at", 466 + "type": "timestamp", 467 + "primaryKey": false, 468 + "notNull": true, 469 + "default": "now()" 470 + } 471 + }, 472 + "indexes": { 473 + "column_projectId_idx": { 474 + "name": "column_projectId_idx", 475 + "columns": [ 476 + { 477 + "expression": "project_id", 478 + "isExpression": false, 479 + "asc": true, 480 + "nulls": "last" 481 + } 482 + ], 483 + "isUnique": false, 484 + "concurrently": false, 485 + "method": "btree", 486 + "with": {} 487 + } 488 + }, 489 + "foreignKeys": { 490 + "column_project_id_project_id_fk": { 491 + "name": "column_project_id_project_id_fk", 492 + "tableFrom": "column", 493 + "tableTo": "project", 494 + "columnsFrom": ["project_id"], 495 + "columnsTo": ["id"], 496 + "onDelete": "cascade", 497 + "onUpdate": "cascade" 498 + } 499 + }, 500 + "compositePrimaryKeys": {}, 501 + "uniqueConstraints": {}, 502 + "policies": {}, 503 + "checkConstraints": {}, 504 + "isRLSEnabled": false 505 + }, 506 + "public.external_link": { 507 + "name": "external_link", 508 + "schema": "", 509 + "columns": { 510 + "id": { 511 + "name": "id", 512 + "type": "text", 513 + "primaryKey": true, 514 + "notNull": true 515 + }, 516 + "task_id": { 517 + "name": "task_id", 518 + "type": "text", 519 + "primaryKey": false, 520 + "notNull": true 521 + }, 522 + "integration_id": { 523 + "name": "integration_id", 524 + "type": "text", 525 + "primaryKey": false, 526 + "notNull": true 527 + }, 528 + "resource_type": { 529 + "name": "resource_type", 530 + "type": "text", 531 + "primaryKey": false, 532 + "notNull": true 533 + }, 534 + "external_id": { 535 + "name": "external_id", 536 + "type": "text", 537 + "primaryKey": false, 538 + "notNull": true 539 + }, 540 + "url": { 541 + "name": "url", 542 + "type": "text", 543 + "primaryKey": false, 544 + "notNull": true 545 + }, 546 + "title": { 547 + "name": "title", 548 + "type": "text", 549 + "primaryKey": false, 550 + "notNull": false 551 + }, 552 + "metadata": { 553 + "name": "metadata", 554 + "type": "text", 555 + "primaryKey": false, 556 + "notNull": false 557 + }, 558 + "created_at": { 559 + "name": "created_at", 560 + "type": "timestamp", 561 + "primaryKey": false, 562 + "notNull": true, 563 + "default": "now()" 564 + }, 565 + "updated_at": { 566 + "name": "updated_at", 567 + "type": "timestamp", 568 + "primaryKey": false, 569 + "notNull": true, 570 + "default": "now()" 571 + } 572 + }, 573 + "indexes": { 574 + "external_link_taskId_idx": { 575 + "name": "external_link_taskId_idx", 576 + "columns": [ 577 + { 578 + "expression": "task_id", 579 + "isExpression": false, 580 + "asc": true, 581 + "nulls": "last" 582 + } 583 + ], 584 + "isUnique": false, 585 + "concurrently": false, 586 + "method": "btree", 587 + "with": {} 588 + }, 589 + "external_link_integrationId_idx": { 590 + "name": "external_link_integrationId_idx", 591 + "columns": [ 592 + { 593 + "expression": "integration_id", 594 + "isExpression": false, 595 + "asc": true, 596 + "nulls": "last" 597 + } 598 + ], 599 + "isUnique": false, 600 + "concurrently": false, 601 + "method": "btree", 602 + "with": {} 603 + }, 604 + "external_link_externalId_idx": { 605 + "name": "external_link_externalId_idx", 606 + "columns": [ 607 + { 608 + "expression": "external_id", 609 + "isExpression": false, 610 + "asc": true, 611 + "nulls": "last" 612 + } 613 + ], 614 + "isUnique": false, 615 + "concurrently": false, 616 + "method": "btree", 617 + "with": {} 618 + }, 619 + "external_link_resourceType_idx": { 620 + "name": "external_link_resourceType_idx", 621 + "columns": [ 622 + { 623 + "expression": "resource_type", 624 + "isExpression": false, 625 + "asc": true, 626 + "nulls": "last" 627 + } 628 + ], 629 + "isUnique": false, 630 + "concurrently": false, 631 + "method": "btree", 632 + "with": {} 633 + } 634 + }, 635 + "foreignKeys": { 636 + "external_link_task_id_task_id_fk": { 637 + "name": "external_link_task_id_task_id_fk", 638 + "tableFrom": "external_link", 639 + "tableTo": "task", 640 + "columnsFrom": ["task_id"], 641 + "columnsTo": ["id"], 642 + "onDelete": "cascade", 643 + "onUpdate": "cascade" 644 + }, 645 + "external_link_integration_id_integration_id_fk": { 646 + "name": "external_link_integration_id_integration_id_fk", 647 + "tableFrom": "external_link", 648 + "tableTo": "integration", 649 + "columnsFrom": ["integration_id"], 650 + "columnsTo": ["id"], 651 + "onDelete": "cascade", 652 + "onUpdate": "cascade" 653 + } 654 + }, 655 + "compositePrimaryKeys": {}, 656 + "uniqueConstraints": {}, 657 + "policies": {}, 658 + "checkConstraints": {}, 659 + "isRLSEnabled": false 660 + }, 661 + "public.github_integration": { 662 + "name": "github_integration", 663 + "schema": "", 664 + "columns": { 665 + "id": { 666 + "name": "id", 667 + "type": "text", 668 + "primaryKey": true, 669 + "notNull": true 670 + }, 671 + "project_id": { 672 + "name": "project_id", 673 + "type": "text", 674 + "primaryKey": false, 675 + "notNull": true 676 + }, 677 + "repository_owner": { 678 + "name": "repository_owner", 679 + "type": "text", 680 + "primaryKey": false, 681 + "notNull": true 682 + }, 683 + "repository_name": { 684 + "name": "repository_name", 685 + "type": "text", 686 + "primaryKey": false, 687 + "notNull": true 688 + }, 689 + "installation_id": { 690 + "name": "installation_id", 691 + "type": "integer", 692 + "primaryKey": false, 693 + "notNull": false 694 + }, 695 + "is_active": { 696 + "name": "is_active", 697 + "type": "boolean", 698 + "primaryKey": false, 699 + "notNull": false, 700 + "default": true 701 + }, 702 + "created_at": { 703 + "name": "created_at", 704 + "type": "timestamp", 705 + "primaryKey": false, 706 + "notNull": true, 707 + "default": "now()" 708 + }, 709 + "updated_at": { 710 + "name": "updated_at", 711 + "type": "timestamp", 712 + "primaryKey": false, 713 + "notNull": true, 714 + "default": "now()" 715 + } 716 + }, 717 + "indexes": {}, 718 + "foreignKeys": { 719 + "github_integration_project_id_project_id_fk": { 720 + "name": "github_integration_project_id_project_id_fk", 721 + "tableFrom": "github_integration", 722 + "tableTo": "project", 723 + "columnsFrom": ["project_id"], 724 + "columnsTo": ["id"], 725 + "onDelete": "cascade", 726 + "onUpdate": "cascade" 727 + } 728 + }, 729 + "compositePrimaryKeys": {}, 730 + "uniqueConstraints": { 731 + "github_integration_project_id_unique": { 732 + "name": "github_integration_project_id_unique", 733 + "nullsNotDistinct": false, 734 + "columns": ["project_id"] 735 + } 736 + }, 737 + "policies": {}, 738 + "checkConstraints": {}, 739 + "isRLSEnabled": false 740 + }, 741 + "public.integration": { 742 + "name": "integration", 743 + "schema": "", 744 + "columns": { 745 + "id": { 746 + "name": "id", 747 + "type": "text", 748 + "primaryKey": true, 749 + "notNull": true 750 + }, 751 + "project_id": { 752 + "name": "project_id", 753 + "type": "text", 754 + "primaryKey": false, 755 + "notNull": true 756 + }, 757 + "type": { 758 + "name": "type", 759 + "type": "text", 760 + "primaryKey": false, 761 + "notNull": true 762 + }, 763 + "config": { 764 + "name": "config", 765 + "type": "text", 766 + "primaryKey": false, 767 + "notNull": true 768 + }, 769 + "is_active": { 770 + "name": "is_active", 771 + "type": "boolean", 772 + "primaryKey": false, 773 + "notNull": false, 774 + "default": true 775 + }, 776 + "created_at": { 777 + "name": "created_at", 778 + "type": "timestamp", 779 + "primaryKey": false, 780 + "notNull": true, 781 + "default": "now()" 782 + }, 783 + "updated_at": { 784 + "name": "updated_at", 785 + "type": "timestamp", 786 + "primaryKey": false, 787 + "notNull": true, 788 + "default": "now()" 789 + } 790 + }, 791 + "indexes": { 792 + "integration_projectId_idx": { 793 + "name": "integration_projectId_idx", 794 + "columns": [ 795 + { 796 + "expression": "project_id", 797 + "isExpression": false, 798 + "asc": true, 799 + "nulls": "last" 800 + } 801 + ], 802 + "isUnique": false, 803 + "concurrently": false, 804 + "method": "btree", 805 + "with": {} 806 + }, 807 + "integration_type_idx": { 808 + "name": "integration_type_idx", 809 + "columns": [ 810 + { 811 + "expression": "type", 812 + "isExpression": false, 813 + "asc": true, 814 + "nulls": "last" 815 + } 816 + ], 817 + "isUnique": false, 818 + "concurrently": false, 819 + "method": "btree", 820 + "with": {} 821 + } 822 + }, 823 + "foreignKeys": { 824 + "integration_project_id_project_id_fk": { 825 + "name": "integration_project_id_project_id_fk", 826 + "tableFrom": "integration", 827 + "tableTo": "project", 828 + "columnsFrom": ["project_id"], 829 + "columnsTo": ["id"], 830 + "onDelete": "cascade", 831 + "onUpdate": "cascade" 832 + } 833 + }, 834 + "compositePrimaryKeys": {}, 835 + "uniqueConstraints": {}, 836 + "policies": {}, 837 + "checkConstraints": {}, 838 + "isRLSEnabled": false 839 + }, 840 + "public.invitation": { 841 + "name": "invitation", 842 + "schema": "", 843 + "columns": { 844 + "id": { 845 + "name": "id", 846 + "type": "text", 847 + "primaryKey": true, 848 + "notNull": true 849 + }, 850 + "workspace_id": { 851 + "name": "workspace_id", 852 + "type": "text", 853 + "primaryKey": false, 854 + "notNull": true 855 + }, 856 + "email": { 857 + "name": "email", 858 + "type": "text", 859 + "primaryKey": false, 860 + "notNull": true 861 + }, 862 + "role": { 863 + "name": "role", 864 + "type": "text", 865 + "primaryKey": false, 866 + "notNull": false 867 + }, 868 + "team_id": { 869 + "name": "team_id", 870 + "type": "text", 871 + "primaryKey": false, 872 + "notNull": false 873 + }, 874 + "status": { 875 + "name": "status", 876 + "type": "text", 877 + "primaryKey": false, 878 + "notNull": true, 879 + "default": "'pending'" 880 + }, 881 + "expires_at": { 882 + "name": "expires_at", 883 + "type": "timestamp", 884 + "primaryKey": false, 885 + "notNull": true 886 + }, 887 + "created_at": { 888 + "name": "created_at", 889 + "type": "timestamp", 890 + "primaryKey": false, 891 + "notNull": true, 892 + "default": "now()" 893 + }, 894 + "inviter_id": { 895 + "name": "inviter_id", 896 + "type": "text", 897 + "primaryKey": false, 898 + "notNull": true 899 + } 900 + }, 901 + "indexes": { 902 + "invitation_workspaceId_idx": { 903 + "name": "invitation_workspaceId_idx", 904 + "columns": [ 905 + { 906 + "expression": "workspace_id", 907 + "isExpression": false, 908 + "asc": true, 909 + "nulls": "last" 910 + } 911 + ], 912 + "isUnique": false, 913 + "concurrently": false, 914 + "method": "btree", 915 + "with": {} 916 + }, 917 + "invitation_email_idx": { 918 + "name": "invitation_email_idx", 919 + "columns": [ 920 + { 921 + "expression": "email", 922 + "isExpression": false, 923 + "asc": true, 924 + "nulls": "last" 925 + } 926 + ], 927 + "isUnique": false, 928 + "concurrently": false, 929 + "method": "btree", 930 + "with": {} 931 + } 932 + }, 933 + "foreignKeys": { 934 + "invitation_workspace_id_workspace_id_fk": { 935 + "name": "invitation_workspace_id_workspace_id_fk", 936 + "tableFrom": "invitation", 937 + "tableTo": "workspace", 938 + "columnsFrom": ["workspace_id"], 939 + "columnsTo": ["id"], 940 + "onDelete": "cascade", 941 + "onUpdate": "no action" 942 + }, 943 + "invitation_inviter_id_user_id_fk": { 944 + "name": "invitation_inviter_id_user_id_fk", 945 + "tableFrom": "invitation", 946 + "tableTo": "user", 947 + "columnsFrom": ["inviter_id"], 948 + "columnsTo": ["id"], 949 + "onDelete": "cascade", 950 + "onUpdate": "no action" 951 + } 952 + }, 953 + "compositePrimaryKeys": {}, 954 + "uniqueConstraints": {}, 955 + "policies": {}, 956 + "checkConstraints": {}, 957 + "isRLSEnabled": false 958 + }, 959 + "public.label": { 960 + "name": "label", 961 + "schema": "", 962 + "columns": { 963 + "id": { 964 + "name": "id", 965 + "type": "text", 966 + "primaryKey": true, 967 + "notNull": true 968 + }, 969 + "name": { 970 + "name": "name", 971 + "type": "text", 972 + "primaryKey": false, 973 + "notNull": true 974 + }, 975 + "color": { 976 + "name": "color", 977 + "type": "text", 978 + "primaryKey": false, 979 + "notNull": true 980 + }, 981 + "created_at": { 982 + "name": "created_at", 983 + "type": "timestamp", 984 + "primaryKey": false, 985 + "notNull": true, 986 + "default": "now()" 987 + }, 988 + "task_id": { 989 + "name": "task_id", 990 + "type": "text", 991 + "primaryKey": false, 992 + "notNull": false 993 + }, 994 + "workspace_id": { 995 + "name": "workspace_id", 996 + "type": "text", 997 + "primaryKey": false, 998 + "notNull": false 999 + } 1000 + }, 1001 + "indexes": {}, 1002 + "foreignKeys": { 1003 + "label_task_id_task_id_fk": { 1004 + "name": "label_task_id_task_id_fk", 1005 + "tableFrom": "label", 1006 + "tableTo": "task", 1007 + "columnsFrom": ["task_id"], 1008 + "columnsTo": ["id"], 1009 + "onDelete": "cascade", 1010 + "onUpdate": "cascade" 1011 + }, 1012 + "label_workspace_id_workspace_id_fk": { 1013 + "name": "label_workspace_id_workspace_id_fk", 1014 + "tableFrom": "label", 1015 + "tableTo": "workspace", 1016 + "columnsFrom": ["workspace_id"], 1017 + "columnsTo": ["id"], 1018 + "onDelete": "cascade", 1019 + "onUpdate": "cascade" 1020 + } 1021 + }, 1022 + "compositePrimaryKeys": {}, 1023 + "uniqueConstraints": {}, 1024 + "policies": {}, 1025 + "checkConstraints": {}, 1026 + "isRLSEnabled": false 1027 + }, 1028 + "public.notification": { 1029 + "name": "notification", 1030 + "schema": "", 1031 + "columns": { 1032 + "id": { 1033 + "name": "id", 1034 + "type": "text", 1035 + "primaryKey": true, 1036 + "notNull": true 1037 + }, 1038 + "user_id": { 1039 + "name": "user_id", 1040 + "type": "text", 1041 + "primaryKey": false, 1042 + "notNull": true 1043 + }, 1044 + "title": { 1045 + "name": "title", 1046 + "type": "text", 1047 + "primaryKey": false, 1048 + "notNull": true 1049 + }, 1050 + "content": { 1051 + "name": "content", 1052 + "type": "text", 1053 + "primaryKey": false, 1054 + "notNull": false 1055 + }, 1056 + "type": { 1057 + "name": "type", 1058 + "type": "text", 1059 + "primaryKey": false, 1060 + "notNull": true, 1061 + "default": "'info'" 1062 + }, 1063 + "is_read": { 1064 + "name": "is_read", 1065 + "type": "boolean", 1066 + "primaryKey": false, 1067 + "notNull": false, 1068 + "default": false 1069 + }, 1070 + "resource_id": { 1071 + "name": "resource_id", 1072 + "type": "text", 1073 + "primaryKey": false, 1074 + "notNull": false 1075 + }, 1076 + "resource_type": { 1077 + "name": "resource_type", 1078 + "type": "text", 1079 + "primaryKey": false, 1080 + "notNull": false 1081 + }, 1082 + "created_at": { 1083 + "name": "created_at", 1084 + "type": "timestamp with time zone", 1085 + "primaryKey": false, 1086 + "notNull": true, 1087 + "default": "now()" 1088 + } 1089 + }, 1090 + "indexes": {}, 1091 + "foreignKeys": { 1092 + "notification_user_id_user_id_fk": { 1093 + "name": "notification_user_id_user_id_fk", 1094 + "tableFrom": "notification", 1095 + "tableTo": "user", 1096 + "columnsFrom": ["user_id"], 1097 + "columnsTo": ["id"], 1098 + "onDelete": "cascade", 1099 + "onUpdate": "cascade" 1100 + } 1101 + }, 1102 + "compositePrimaryKeys": {}, 1103 + "uniqueConstraints": {}, 1104 + "policies": {}, 1105 + "checkConstraints": {}, 1106 + "isRLSEnabled": false 1107 + }, 1108 + "public.project": { 1109 + "name": "project", 1110 + "schema": "", 1111 + "columns": { 1112 + "id": { 1113 + "name": "id", 1114 + "type": "text", 1115 + "primaryKey": true, 1116 + "notNull": true 1117 + }, 1118 + "workspace_id": { 1119 + "name": "workspace_id", 1120 + "type": "text", 1121 + "primaryKey": false, 1122 + "notNull": true 1123 + }, 1124 + "slug": { 1125 + "name": "slug", 1126 + "type": "text", 1127 + "primaryKey": false, 1128 + "notNull": true 1129 + }, 1130 + "icon": { 1131 + "name": "icon", 1132 + "type": "text", 1133 + "primaryKey": false, 1134 + "notNull": false, 1135 + "default": "'Layout'" 1136 + }, 1137 + "name": { 1138 + "name": "name", 1139 + "type": "text", 1140 + "primaryKey": false, 1141 + "notNull": true 1142 + }, 1143 + "description": { 1144 + "name": "description", 1145 + "type": "text", 1146 + "primaryKey": false, 1147 + "notNull": false 1148 + }, 1149 + "created_at": { 1150 + "name": "created_at", 1151 + "type": "timestamp", 1152 + "primaryKey": false, 1153 + "notNull": true, 1154 + "default": "now()" 1155 + }, 1156 + "is_public": { 1157 + "name": "is_public", 1158 + "type": "boolean", 1159 + "primaryKey": false, 1160 + "notNull": false, 1161 + "default": false 1162 + } 1163 + }, 1164 + "indexes": {}, 1165 + "foreignKeys": { 1166 + "project_workspace_id_workspace_id_fk": { 1167 + "name": "project_workspace_id_workspace_id_fk", 1168 + "tableFrom": "project", 1169 + "tableTo": "workspace", 1170 + "columnsFrom": ["workspace_id"], 1171 + "columnsTo": ["id"], 1172 + "onDelete": "cascade", 1173 + "onUpdate": "cascade" 1174 + } 1175 + }, 1176 + "compositePrimaryKeys": {}, 1177 + "uniqueConstraints": {}, 1178 + "policies": {}, 1179 + "checkConstraints": {}, 1180 + "isRLSEnabled": false 1181 + }, 1182 + "public.session": { 1183 + "name": "session", 1184 + "schema": "", 1185 + "columns": { 1186 + "id": { 1187 + "name": "id", 1188 + "type": "text", 1189 + "primaryKey": true, 1190 + "notNull": true 1191 + }, 1192 + "expires_at": { 1193 + "name": "expires_at", 1194 + "type": "timestamp", 1195 + "primaryKey": false, 1196 + "notNull": true 1197 + }, 1198 + "token": { 1199 + "name": "token", 1200 + "type": "text", 1201 + "primaryKey": false, 1202 + "notNull": true 1203 + }, 1204 + "created_at": { 1205 + "name": "created_at", 1206 + "type": "timestamp", 1207 + "primaryKey": false, 1208 + "notNull": true, 1209 + "default": "now()" 1210 + }, 1211 + "updated_at": { 1212 + "name": "updated_at", 1213 + "type": "timestamp", 1214 + "primaryKey": false, 1215 + "notNull": true 1216 + }, 1217 + "ip_address": { 1218 + "name": "ip_address", 1219 + "type": "text", 1220 + "primaryKey": false, 1221 + "notNull": false 1222 + }, 1223 + "user_agent": { 1224 + "name": "user_agent", 1225 + "type": "text", 1226 + "primaryKey": false, 1227 + "notNull": false 1228 + }, 1229 + "user_id": { 1230 + "name": "user_id", 1231 + "type": "text", 1232 + "primaryKey": false, 1233 + "notNull": true 1234 + }, 1235 + "active_organization_id": { 1236 + "name": "active_organization_id", 1237 + "type": "text", 1238 + "primaryKey": false, 1239 + "notNull": false 1240 + }, 1241 + "active_team_id": { 1242 + "name": "active_team_id", 1243 + "type": "text", 1244 + "primaryKey": false, 1245 + "notNull": false 1246 + } 1247 + }, 1248 + "indexes": { 1249 + "session_userId_idx": { 1250 + "name": "session_userId_idx", 1251 + "columns": [ 1252 + { 1253 + "expression": "user_id", 1254 + "isExpression": false, 1255 + "asc": true, 1256 + "nulls": "last" 1257 + } 1258 + ], 1259 + "isUnique": false, 1260 + "concurrently": false, 1261 + "method": "btree", 1262 + "with": {} 1263 + } 1264 + }, 1265 + "foreignKeys": { 1266 + "session_user_id_user_id_fk": { 1267 + "name": "session_user_id_user_id_fk", 1268 + "tableFrom": "session", 1269 + "tableTo": "user", 1270 + "columnsFrom": ["user_id"], 1271 + "columnsTo": ["id"], 1272 + "onDelete": "cascade", 1273 + "onUpdate": "no action" 1274 + } 1275 + }, 1276 + "compositePrimaryKeys": {}, 1277 + "uniqueConstraints": { 1278 + "session_token_unique": { 1279 + "name": "session_token_unique", 1280 + "nullsNotDistinct": false, 1281 + "columns": ["token"] 1282 + } 1283 + }, 1284 + "policies": {}, 1285 + "checkConstraints": {}, 1286 + "isRLSEnabled": false 1287 + }, 1288 + "public.task": { 1289 + "name": "task", 1290 + "schema": "", 1291 + "columns": { 1292 + "id": { 1293 + "name": "id", 1294 + "type": "text", 1295 + "primaryKey": true, 1296 + "notNull": true 1297 + }, 1298 + "project_id": { 1299 + "name": "project_id", 1300 + "type": "text", 1301 + "primaryKey": false, 1302 + "notNull": true 1303 + }, 1304 + "position": { 1305 + "name": "position", 1306 + "type": "integer", 1307 + "primaryKey": false, 1308 + "notNull": false, 1309 + "default": 0 1310 + }, 1311 + "number": { 1312 + "name": "number", 1313 + "type": "integer", 1314 + "primaryKey": false, 1315 + "notNull": false, 1316 + "default": 1 1317 + }, 1318 + "assignee_id": { 1319 + "name": "assignee_id", 1320 + "type": "text", 1321 + "primaryKey": false, 1322 + "notNull": false 1323 + }, 1324 + "title": { 1325 + "name": "title", 1326 + "type": "text", 1327 + "primaryKey": false, 1328 + "notNull": true 1329 + }, 1330 + "description": { 1331 + "name": "description", 1332 + "type": "text", 1333 + "primaryKey": false, 1334 + "notNull": false 1335 + }, 1336 + "status": { 1337 + "name": "status", 1338 + "type": "text", 1339 + "primaryKey": false, 1340 + "notNull": true, 1341 + "default": "'to-do'" 1342 + }, 1343 + "column_id": { 1344 + "name": "column_id", 1345 + "type": "text", 1346 + "primaryKey": false, 1347 + "notNull": false 1348 + }, 1349 + "priority": { 1350 + "name": "priority", 1351 + "type": "text", 1352 + "primaryKey": false, 1353 + "notNull": false, 1354 + "default": "'low'" 1355 + }, 1356 + "due_date": { 1357 + "name": "due_date", 1358 + "type": "timestamp", 1359 + "primaryKey": false, 1360 + "notNull": false 1361 + }, 1362 + "created_at": { 1363 + "name": "created_at", 1364 + "type": "timestamp", 1365 + "primaryKey": false, 1366 + "notNull": true, 1367 + "default": "now()" 1368 + } 1369 + }, 1370 + "indexes": {}, 1371 + "foreignKeys": { 1372 + "task_project_id_project_id_fk": { 1373 + "name": "task_project_id_project_id_fk", 1374 + "tableFrom": "task", 1375 + "tableTo": "project", 1376 + "columnsFrom": ["project_id"], 1377 + "columnsTo": ["id"], 1378 + "onDelete": "cascade", 1379 + "onUpdate": "cascade" 1380 + }, 1381 + "task_assignee_id_user_id_fk": { 1382 + "name": "task_assignee_id_user_id_fk", 1383 + "tableFrom": "task", 1384 + "tableTo": "user", 1385 + "columnsFrom": ["assignee_id"], 1386 + "columnsTo": ["id"], 1387 + "onDelete": "cascade", 1388 + "onUpdate": "cascade" 1389 + }, 1390 + "task_column_id_column_id_fk": { 1391 + "name": "task_column_id_column_id_fk", 1392 + "tableFrom": "task", 1393 + "tableTo": "column", 1394 + "columnsFrom": ["column_id"], 1395 + "columnsTo": ["id"], 1396 + "onDelete": "set null", 1397 + "onUpdate": "cascade" 1398 + } 1399 + }, 1400 + "compositePrimaryKeys": {}, 1401 + "uniqueConstraints": {}, 1402 + "policies": {}, 1403 + "checkConstraints": {}, 1404 + "isRLSEnabled": false 1405 + }, 1406 + "public.team_member": { 1407 + "name": "team_member", 1408 + "schema": "", 1409 + "columns": { 1410 + "id": { 1411 + "name": "id", 1412 + "type": "text", 1413 + "primaryKey": true, 1414 + "notNull": true 1415 + }, 1416 + "team_id": { 1417 + "name": "team_id", 1418 + "type": "text", 1419 + "primaryKey": false, 1420 + "notNull": true 1421 + }, 1422 + "user_id": { 1423 + "name": "user_id", 1424 + "type": "text", 1425 + "primaryKey": false, 1426 + "notNull": true 1427 + }, 1428 + "created_at": { 1429 + "name": "created_at", 1430 + "type": "timestamp", 1431 + "primaryKey": false, 1432 + "notNull": false 1433 + } 1434 + }, 1435 + "indexes": { 1436 + "teamMember_teamId_idx": { 1437 + "name": "teamMember_teamId_idx", 1438 + "columns": [ 1439 + { 1440 + "expression": "team_id", 1441 + "isExpression": false, 1442 + "asc": true, 1443 + "nulls": "last" 1444 + } 1445 + ], 1446 + "isUnique": false, 1447 + "concurrently": false, 1448 + "method": "btree", 1449 + "with": {} 1450 + }, 1451 + "teamMember_userId_idx": { 1452 + "name": "teamMember_userId_idx", 1453 + "columns": [ 1454 + { 1455 + "expression": "user_id", 1456 + "isExpression": false, 1457 + "asc": true, 1458 + "nulls": "last" 1459 + } 1460 + ], 1461 + "isUnique": false, 1462 + "concurrently": false, 1463 + "method": "btree", 1464 + "with": {} 1465 + } 1466 + }, 1467 + "foreignKeys": { 1468 + "team_member_team_id_team_id_fk": { 1469 + "name": "team_member_team_id_team_id_fk", 1470 + "tableFrom": "team_member", 1471 + "tableTo": "team", 1472 + "columnsFrom": ["team_id"], 1473 + "columnsTo": ["id"], 1474 + "onDelete": "cascade", 1475 + "onUpdate": "no action" 1476 + }, 1477 + "team_member_user_id_user_id_fk": { 1478 + "name": "team_member_user_id_user_id_fk", 1479 + "tableFrom": "team_member", 1480 + "tableTo": "user", 1481 + "columnsFrom": ["user_id"], 1482 + "columnsTo": ["id"], 1483 + "onDelete": "cascade", 1484 + "onUpdate": "no action" 1485 + } 1486 + }, 1487 + "compositePrimaryKeys": {}, 1488 + "uniqueConstraints": {}, 1489 + "policies": {}, 1490 + "checkConstraints": {}, 1491 + "isRLSEnabled": false 1492 + }, 1493 + "public.team": { 1494 + "name": "team", 1495 + "schema": "", 1496 + "columns": { 1497 + "id": { 1498 + "name": "id", 1499 + "type": "text", 1500 + "primaryKey": true, 1501 + "notNull": true 1502 + }, 1503 + "name": { 1504 + "name": "name", 1505 + "type": "text", 1506 + "primaryKey": false, 1507 + "notNull": true 1508 + }, 1509 + "workspace_id": { 1510 + "name": "workspace_id", 1511 + "type": "text", 1512 + "primaryKey": false, 1513 + "notNull": true 1514 + }, 1515 + "created_at": { 1516 + "name": "created_at", 1517 + "type": "timestamp", 1518 + "primaryKey": false, 1519 + "notNull": true 1520 + }, 1521 + "updated_at": { 1522 + "name": "updated_at", 1523 + "type": "timestamp", 1524 + "primaryKey": false, 1525 + "notNull": false 1526 + } 1527 + }, 1528 + "indexes": { 1529 + "team_workspaceId_idx": { 1530 + "name": "team_workspaceId_idx", 1531 + "columns": [ 1532 + { 1533 + "expression": "workspace_id", 1534 + "isExpression": false, 1535 + "asc": true, 1536 + "nulls": "last" 1537 + } 1538 + ], 1539 + "isUnique": false, 1540 + "concurrently": false, 1541 + "method": "btree", 1542 + "with": {} 1543 + } 1544 + }, 1545 + "foreignKeys": { 1546 + "team_workspace_id_workspace_id_fk": { 1547 + "name": "team_workspace_id_workspace_id_fk", 1548 + "tableFrom": "team", 1549 + "tableTo": "workspace", 1550 + "columnsFrom": ["workspace_id"], 1551 + "columnsTo": ["id"], 1552 + "onDelete": "cascade", 1553 + "onUpdate": "no action" 1554 + } 1555 + }, 1556 + "compositePrimaryKeys": {}, 1557 + "uniqueConstraints": {}, 1558 + "policies": {}, 1559 + "checkConstraints": {}, 1560 + "isRLSEnabled": false 1561 + }, 1562 + "public.time_entry": { 1563 + "name": "time_entry", 1564 + "schema": "", 1565 + "columns": { 1566 + "id": { 1567 + "name": "id", 1568 + "type": "text", 1569 + "primaryKey": true, 1570 + "notNull": true 1571 + }, 1572 + "task_id": { 1573 + "name": "task_id", 1574 + "type": "text", 1575 + "primaryKey": false, 1576 + "notNull": true 1577 + }, 1578 + "user_id": { 1579 + "name": "user_id", 1580 + "type": "text", 1581 + "primaryKey": false, 1582 + "notNull": false 1583 + }, 1584 + "description": { 1585 + "name": "description", 1586 + "type": "text", 1587 + "primaryKey": false, 1588 + "notNull": false 1589 + }, 1590 + "start_time": { 1591 + "name": "start_time", 1592 + "type": "timestamp", 1593 + "primaryKey": false, 1594 + "notNull": true 1595 + }, 1596 + "end_time": { 1597 + "name": "end_time", 1598 + "type": "timestamp", 1599 + "primaryKey": false, 1600 + "notNull": false 1601 + }, 1602 + "duration": { 1603 + "name": "duration", 1604 + "type": "integer", 1605 + "primaryKey": false, 1606 + "notNull": false, 1607 + "default": 0 1608 + }, 1609 + "created_at": { 1610 + "name": "created_at", 1611 + "type": "timestamp", 1612 + "primaryKey": false, 1613 + "notNull": true, 1614 + "default": "now()" 1615 + } 1616 + }, 1617 + "indexes": {}, 1618 + "foreignKeys": { 1619 + "time_entry_task_id_task_id_fk": { 1620 + "name": "time_entry_task_id_task_id_fk", 1621 + "tableFrom": "time_entry", 1622 + "tableTo": "task", 1623 + "columnsFrom": ["task_id"], 1624 + "columnsTo": ["id"], 1625 + "onDelete": "cascade", 1626 + "onUpdate": "cascade" 1627 + }, 1628 + "time_entry_user_id_user_id_fk": { 1629 + "name": "time_entry_user_id_user_id_fk", 1630 + "tableFrom": "time_entry", 1631 + "tableTo": "user", 1632 + "columnsFrom": ["user_id"], 1633 + "columnsTo": ["id"], 1634 + "onDelete": "cascade", 1635 + "onUpdate": "cascade" 1636 + } 1637 + }, 1638 + "compositePrimaryKeys": {}, 1639 + "uniqueConstraints": {}, 1640 + "policies": {}, 1641 + "checkConstraints": {}, 1642 + "isRLSEnabled": false 1643 + }, 1644 + "public.user": { 1645 + "name": "user", 1646 + "schema": "", 1647 + "columns": { 1648 + "id": { 1649 + "name": "id", 1650 + "type": "text", 1651 + "primaryKey": true, 1652 + "notNull": true 1653 + }, 1654 + "name": { 1655 + "name": "name", 1656 + "type": "text", 1657 + "primaryKey": false, 1658 + "notNull": true 1659 + }, 1660 + "email": { 1661 + "name": "email", 1662 + "type": "text", 1663 + "primaryKey": false, 1664 + "notNull": true 1665 + }, 1666 + "email_verified": { 1667 + "name": "email_verified", 1668 + "type": "boolean", 1669 + "primaryKey": false, 1670 + "notNull": true 1671 + }, 1672 + "image": { 1673 + "name": "image", 1674 + "type": "text", 1675 + "primaryKey": false, 1676 + "notNull": false 1677 + }, 1678 + "created_at": { 1679 + "name": "created_at", 1680 + "type": "timestamp", 1681 + "primaryKey": false, 1682 + "notNull": true, 1683 + "default": "now()" 1684 + }, 1685 + "updated_at": { 1686 + "name": "updated_at", 1687 + "type": "timestamp", 1688 + "primaryKey": false, 1689 + "notNull": true, 1690 + "default": "now()" 1691 + }, 1692 + "is_anonymous": { 1693 + "name": "is_anonymous", 1694 + "type": "boolean", 1695 + "primaryKey": false, 1696 + "notNull": false, 1697 + "default": false 1698 + } 1699 + }, 1700 + "indexes": {}, 1701 + "foreignKeys": {}, 1702 + "compositePrimaryKeys": {}, 1703 + "uniqueConstraints": { 1704 + "user_email_unique": { 1705 + "name": "user_email_unique", 1706 + "nullsNotDistinct": false, 1707 + "columns": ["email"] 1708 + } 1709 + }, 1710 + "policies": {}, 1711 + "checkConstraints": {}, 1712 + "isRLSEnabled": false 1713 + }, 1714 + "public.verification": { 1715 + "name": "verification", 1716 + "schema": "", 1717 + "columns": { 1718 + "id": { 1719 + "name": "id", 1720 + "type": "text", 1721 + "primaryKey": true, 1722 + "notNull": true 1723 + }, 1724 + "identifier": { 1725 + "name": "identifier", 1726 + "type": "text", 1727 + "primaryKey": false, 1728 + "notNull": true 1729 + }, 1730 + "value": { 1731 + "name": "value", 1732 + "type": "text", 1733 + "primaryKey": false, 1734 + "notNull": true 1735 + }, 1736 + "expires_at": { 1737 + "name": "expires_at", 1738 + "type": "timestamp", 1739 + "primaryKey": false, 1740 + "notNull": true 1741 + }, 1742 + "created_at": { 1743 + "name": "created_at", 1744 + "type": "timestamp", 1745 + "primaryKey": false, 1746 + "notNull": true, 1747 + "default": "now()" 1748 + }, 1749 + "updated_at": { 1750 + "name": "updated_at", 1751 + "type": "timestamp", 1752 + "primaryKey": false, 1753 + "notNull": true, 1754 + "default": "now()" 1755 + } 1756 + }, 1757 + "indexes": { 1758 + "verification_identifier_idx": { 1759 + "name": "verification_identifier_idx", 1760 + "columns": [ 1761 + { 1762 + "expression": "identifier", 1763 + "isExpression": false, 1764 + "asc": true, 1765 + "nulls": "last" 1766 + } 1767 + ], 1768 + "isUnique": false, 1769 + "concurrently": false, 1770 + "method": "btree", 1771 + "with": {} 1772 + } 1773 + }, 1774 + "foreignKeys": {}, 1775 + "compositePrimaryKeys": {}, 1776 + "uniqueConstraints": {}, 1777 + "policies": {}, 1778 + "checkConstraints": {}, 1779 + "isRLSEnabled": false 1780 + }, 1781 + "public.workflow_rule": { 1782 + "name": "workflow_rule", 1783 + "schema": "", 1784 + "columns": { 1785 + "id": { 1786 + "name": "id", 1787 + "type": "text", 1788 + "primaryKey": true, 1789 + "notNull": true 1790 + }, 1791 + "project_id": { 1792 + "name": "project_id", 1793 + "type": "text", 1794 + "primaryKey": false, 1795 + "notNull": true 1796 + }, 1797 + "integration_type": { 1798 + "name": "integration_type", 1799 + "type": "text", 1800 + "primaryKey": false, 1801 + "notNull": true 1802 + }, 1803 + "event_type": { 1804 + "name": "event_type", 1805 + "type": "text", 1806 + "primaryKey": false, 1807 + "notNull": true 1808 + }, 1809 + "column_id": { 1810 + "name": "column_id", 1811 + "type": "text", 1812 + "primaryKey": false, 1813 + "notNull": true 1814 + }, 1815 + "created_at": { 1816 + "name": "created_at", 1817 + "type": "timestamp", 1818 + "primaryKey": false, 1819 + "notNull": true, 1820 + "default": "now()" 1821 + }, 1822 + "updated_at": { 1823 + "name": "updated_at", 1824 + "type": "timestamp", 1825 + "primaryKey": false, 1826 + "notNull": true, 1827 + "default": "now()" 1828 + } 1829 + }, 1830 + "indexes": { 1831 + "workflow_rule_projectId_idx": { 1832 + "name": "workflow_rule_projectId_idx", 1833 + "columns": [ 1834 + { 1835 + "expression": "project_id", 1836 + "isExpression": false, 1837 + "asc": true, 1838 + "nulls": "last" 1839 + } 1840 + ], 1841 + "isUnique": false, 1842 + "concurrently": false, 1843 + "method": "btree", 1844 + "with": {} 1845 + } 1846 + }, 1847 + "foreignKeys": { 1848 + "workflow_rule_project_id_project_id_fk": { 1849 + "name": "workflow_rule_project_id_project_id_fk", 1850 + "tableFrom": "workflow_rule", 1851 + "tableTo": "project", 1852 + "columnsFrom": ["project_id"], 1853 + "columnsTo": ["id"], 1854 + "onDelete": "cascade", 1855 + "onUpdate": "cascade" 1856 + }, 1857 + "workflow_rule_column_id_column_id_fk": { 1858 + "name": "workflow_rule_column_id_column_id_fk", 1859 + "tableFrom": "workflow_rule", 1860 + "tableTo": "column", 1861 + "columnsFrom": ["column_id"], 1862 + "columnsTo": ["id"], 1863 + "onDelete": "cascade", 1864 + "onUpdate": "cascade" 1865 + } 1866 + }, 1867 + "compositePrimaryKeys": {}, 1868 + "uniqueConstraints": {}, 1869 + "policies": {}, 1870 + "checkConstraints": {}, 1871 + "isRLSEnabled": false 1872 + }, 1873 + "public.workspace": { 1874 + "name": "workspace", 1875 + "schema": "", 1876 + "columns": { 1877 + "id": { 1878 + "name": "id", 1879 + "type": "text", 1880 + "primaryKey": true, 1881 + "notNull": true 1882 + }, 1883 + "name": { 1884 + "name": "name", 1885 + "type": "text", 1886 + "primaryKey": false, 1887 + "notNull": true 1888 + }, 1889 + "slug": { 1890 + "name": "slug", 1891 + "type": "text", 1892 + "primaryKey": false, 1893 + "notNull": true 1894 + }, 1895 + "logo": { 1896 + "name": "logo", 1897 + "type": "text", 1898 + "primaryKey": false, 1899 + "notNull": false 1900 + }, 1901 + "metadata": { 1902 + "name": "metadata", 1903 + "type": "text", 1904 + "primaryKey": false, 1905 + "notNull": false 1906 + }, 1907 + "description": { 1908 + "name": "description", 1909 + "type": "text", 1910 + "primaryKey": false, 1911 + "notNull": false 1912 + }, 1913 + "created_at": { 1914 + "name": "created_at", 1915 + "type": "timestamp", 1916 + "primaryKey": false, 1917 + "notNull": true 1918 + } 1919 + }, 1920 + "indexes": {}, 1921 + "foreignKeys": {}, 1922 + "compositePrimaryKeys": {}, 1923 + "uniqueConstraints": { 1924 + "workspace_slug_unique": { 1925 + "name": "workspace_slug_unique", 1926 + "nullsNotDistinct": false, 1927 + "columns": ["slug"] 1928 + } 1929 + }, 1930 + "policies": {}, 1931 + "checkConstraints": {}, 1932 + "isRLSEnabled": false 1933 + }, 1934 + "public.workspace_member": { 1935 + "name": "workspace_member", 1936 + "schema": "", 1937 + "columns": { 1938 + "id": { 1939 + "name": "id", 1940 + "type": "text", 1941 + "primaryKey": true, 1942 + "notNull": true 1943 + }, 1944 + "workspace_id": { 1945 + "name": "workspace_id", 1946 + "type": "text", 1947 + "primaryKey": false, 1948 + "notNull": true 1949 + }, 1950 + "user_id": { 1951 + "name": "user_id", 1952 + "type": "text", 1953 + "primaryKey": false, 1954 + "notNull": true 1955 + }, 1956 + "role": { 1957 + "name": "role", 1958 + "type": "text", 1959 + "primaryKey": false, 1960 + "notNull": true, 1961 + "default": "'member'" 1962 + }, 1963 + "joined_at": { 1964 + "name": "joined_at", 1965 + "type": "timestamp", 1966 + "primaryKey": false, 1967 + "notNull": true 1968 + } 1969 + }, 1970 + "indexes": { 1971 + "workspace_member_workspaceId_idx": { 1972 + "name": "workspace_member_workspaceId_idx", 1973 + "columns": [ 1974 + { 1975 + "expression": "workspace_id", 1976 + "isExpression": false, 1977 + "asc": true, 1978 + "nulls": "last" 1979 + } 1980 + ], 1981 + "isUnique": false, 1982 + "concurrently": false, 1983 + "method": "btree", 1984 + "with": {} 1985 + }, 1986 + "workspace_member_userId_idx": { 1987 + "name": "workspace_member_userId_idx", 1988 + "columns": [ 1989 + { 1990 + "expression": "user_id", 1991 + "isExpression": false, 1992 + "asc": true, 1993 + "nulls": "last" 1994 + } 1995 + ], 1996 + "isUnique": false, 1997 + "concurrently": false, 1998 + "method": "btree", 1999 + "with": {} 2000 + } 2001 + }, 2002 + "foreignKeys": { 2003 + "workspace_member_workspace_id_workspace_id_fk": { 2004 + "name": "workspace_member_workspace_id_workspace_id_fk", 2005 + "tableFrom": "workspace_member", 2006 + "tableTo": "workspace", 2007 + "columnsFrom": ["workspace_id"], 2008 + "columnsTo": ["id"], 2009 + "onDelete": "cascade", 2010 + "onUpdate": "no action" 2011 + }, 2012 + "workspace_member_user_id_user_id_fk": { 2013 + "name": "workspace_member_user_id_user_id_fk", 2014 + "tableFrom": "workspace_member", 2015 + "tableTo": "user", 2016 + "columnsFrom": ["user_id"], 2017 + "columnsTo": ["id"], 2018 + "onDelete": "cascade", 2019 + "onUpdate": "no action" 2020 + } 2021 + }, 2022 + "compositePrimaryKeys": {}, 2023 + "uniqueConstraints": {}, 2024 + "policies": {}, 2025 + "checkConstraints": {}, 2026 + "isRLSEnabled": false 2027 + } 2028 + }, 2029 + "enums": {}, 2030 + "schemas": {}, 2031 + "sequences": {}, 2032 + "roles": {}, 2033 + "policies": {}, 2034 + "views": {}, 2035 + "_meta": { 2036 + "columns": {}, 2037 + "schemas": {}, 2038 + "tables": {} 2039 + } 2040 + }
+7
apps/api/drizzle/meta/_journal.json
··· 85 85 "when": 1767037714394, 86 86 "tag": "0011_flashy_masked_marvel", 87 87 "breakpoints": true 88 + }, 89 + { 90 + "idx": 12, 91 + "version": "7", 92 + "when": 1770375054098, 93 + "tag": "0012_mixed_thor_girl", 94 + "breakpoints": true 88 95 } 89 96 ] 90 97 }
+71
apps/api/src/column/controllers/create-column.ts
··· 1 + import { eq, sql } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { columnTable } from "../../database/schema"; 5 + 6 + function toSlug(name: string): string { 7 + return name 8 + .toLowerCase() 9 + .trim() 10 + .replace(/[^a-z0-9]+/g, "-") 11 + .replace(/^-+|-+$/g, ""); 12 + } 13 + 14 + async function createColumn({ 15 + projectId, 16 + name, 17 + icon, 18 + color, 19 + isFinal, 20 + }: { 21 + projectId: string; 22 + name: string; 23 + icon?: string; 24 + color?: string; 25 + isFinal?: boolean; 26 + }) { 27 + const slug = toSlug(name); 28 + 29 + const existing = await db 30 + .select({ id: columnTable.id }) 31 + .from(columnTable) 32 + .where( 33 + sql`${columnTable.projectId} = ${projectId} AND ${columnTable.slug} = ${slug}`, 34 + ); 35 + 36 + if (existing.length > 0) { 37 + throw new HTTPException(409, { 38 + message: `Column with slug "${slug}" already exists in this project`, 39 + }); 40 + } 41 + 42 + const [maxPos] = await db 43 + .select({ 44 + maxPosition: sql<number>`COALESCE(MAX(${columnTable.position}), -1)`, 45 + }) 46 + .from(columnTable) 47 + .where(eq(columnTable.projectId, projectId)); 48 + 49 + const position = (maxPos?.maxPosition ?? -1) + 1; 50 + 51 + const [created] = await db 52 + .insert(columnTable) 53 + .values({ 54 + projectId, 55 + name, 56 + slug, 57 + position, 58 + icon: icon || null, 59 + color: color || null, 60 + isFinal: isFinal ?? false, 61 + }) 62 + .returning(); 63 + 64 + if (!created) { 65 + throw new HTTPException(500, { message: "Failed to create column" }); 66 + } 67 + 68 + return created; 69 + } 70 + 71 + export default createColumn;
+32
apps/api/src/column/controllers/delete-column.ts
··· 1 + import { eq, sql } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { columnTable, taskTable } from "../../database/schema"; 5 + 6 + async function deleteColumn(id: string) { 7 + const existing = await db.query.columnTable.findFirst({ 8 + where: eq(columnTable.id, id), 9 + }); 10 + 11 + if (!existing) { 12 + throw new HTTPException(404, { message: "Column not found" }); 13 + } 14 + 15 + const [taskCount] = await db 16 + .select({ count: sql<number>`count(*)` }) 17 + .from(taskTable) 18 + .where(eq(taskTable.columnId, id)); 19 + 20 + if (taskCount && taskCount.count > 0) { 21 + throw new HTTPException(409, { 22 + message: 23 + "Cannot delete column that contains tasks. Move or delete tasks first.", 24 + }); 25 + } 26 + 27 + await db.delete(columnTable).where(eq(columnTable.id, id)); 28 + 29 + return existing; 30 + } 31 + 32 + export default deleteColumn;
+15
apps/api/src/column/controllers/get-columns.ts
··· 1 + import { asc, eq } from "drizzle-orm"; 2 + import db from "../../database"; 3 + import { columnTable } from "../../database/schema"; 4 + 5 + async function getColumns(projectId: string) { 6 + const columns = await db 7 + .select() 8 + .from(columnTable) 9 + .where(eq(columnTable.projectId, projectId)) 10 + .orderBy(asc(columnTable.position)); 11 + 12 + return columns; 13 + } 14 + 15 + export default getColumns;
+34
apps/api/src/column/controllers/reorder-columns.ts
··· 1 + import { and, eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { columnTable } from "../../database/schema"; 5 + 6 + async function reorderColumns( 7 + projectId: string, 8 + columns: Array<{ id: string; position: number }>, 9 + ) { 10 + for (const col of columns) { 11 + const [updated] = await db 12 + .update(columnTable) 13 + .set({ position: col.position }) 14 + .where( 15 + and(eq(columnTable.id, col.id), eq(columnTable.projectId, projectId)), 16 + ) 17 + .returning({ id: columnTable.id }); 18 + 19 + if (!updated) { 20 + throw new HTTPException(400, { 21 + message: `Column ${col.id} does not belong to this project`, 22 + }); 23 + } 24 + } 25 + 26 + const updated = await db.query.columnTable.findMany({ 27 + where: eq(columnTable.projectId, projectId), 28 + orderBy: (columns, { asc }) => [asc(columns.position)], 29 + }); 30 + 31 + return updated; 32 + } 33 + 34 + export default reorderColumns;
+41
apps/api/src/column/controllers/update-column.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { columnTable } from "../../database/schema"; 5 + 6 + async function updateColumn( 7 + id: string, 8 + data: { 9 + name?: string; 10 + icon?: string | null; 11 + color?: string | null; 12 + isFinal?: boolean; 13 + }, 14 + ) { 15 + const existing = await db.query.columnTable.findFirst({ 16 + where: eq(columnTable.id, id), 17 + }); 18 + 19 + if (!existing) { 20 + throw new HTTPException(404, { message: "Column not found" }); 21 + } 22 + 23 + const [updated] = await db 24 + .update(columnTable) 25 + .set({ 26 + ...(data.name !== undefined && { name: data.name }), 27 + ...(data.icon !== undefined && { icon: data.icon }), 28 + ...(data.color !== undefined && { color: data.color }), 29 + ...(data.isFinal !== undefined && { isFinal: data.isFinal }), 30 + }) 31 + .where(eq(columnTable.id, id)) 32 + .returning(); 33 + 34 + if (!updated) { 35 + throw new HTTPException(500, { message: "Failed to update column" }); 36 + } 37 + 38 + return updated; 39 + } 40 + 41 + export default updateColumn;
+170
apps/api/src/column/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { describeRoute, resolver, validator } from "hono-openapi"; 3 + import * as v from "valibot"; 4 + import { workspaceAccess } from "../utils/workspace-access-middleware"; 5 + import createColumn from "./controllers/create-column"; 6 + import deleteColumn from "./controllers/delete-column"; 7 + import getColumns from "./controllers/get-columns"; 8 + import reorderColumns from "./controllers/reorder-columns"; 9 + import updateColumn from "./controllers/update-column"; 10 + 11 + const column = new Hono<{ 12 + Variables: { 13 + userId: string; 14 + }; 15 + }>() 16 + .get( 17 + "/:projectId", 18 + describeRoute({ 19 + operationId: "getColumns", 20 + tags: ["Columns"], 21 + description: "Get all columns for a project", 22 + responses: { 23 + 200: { 24 + description: "List of columns ordered by position", 25 + content: { 26 + "application/json": { schema: resolver(v.any()) }, 27 + }, 28 + }, 29 + }, 30 + }), 31 + validator("param", v.object({ projectId: v.string() })), 32 + workspaceAccess.fromProject("projectId"), 33 + async (c) => { 34 + const { projectId } = c.req.valid("param"); 35 + const columns = await getColumns(projectId); 36 + return c.json(columns); 37 + }, 38 + ) 39 + .post( 40 + "/:projectId", 41 + describeRoute({ 42 + operationId: "createColumn", 43 + tags: ["Columns"], 44 + description: "Create a new column in a project", 45 + responses: { 46 + 200: { 47 + description: "Column created successfully", 48 + content: { 49 + "application/json": { schema: resolver(v.any()) }, 50 + }, 51 + }, 52 + }, 53 + }), 54 + validator("param", v.object({ projectId: v.string() })), 55 + validator( 56 + "json", 57 + v.object({ 58 + name: v.string(), 59 + icon: v.optional(v.string()), 60 + color: v.optional(v.string()), 61 + isFinal: v.optional(v.boolean()), 62 + }), 63 + ), 64 + workspaceAccess.fromProject("projectId"), 65 + async (c) => { 66 + const { projectId } = c.req.valid("param"); 67 + const { name, icon, color, isFinal } = c.req.valid("json"); 68 + const result = await createColumn({ 69 + projectId, 70 + name, 71 + icon, 72 + color, 73 + isFinal, 74 + }); 75 + return c.json(result); 76 + }, 77 + ) 78 + .put( 79 + "/reorder/:projectId", 80 + describeRoute({ 81 + operationId: "reorderColumns", 82 + tags: ["Columns"], 83 + description: "Reorder columns in a project", 84 + responses: { 85 + 200: { 86 + description: "Columns reordered successfully", 87 + content: { 88 + "application/json": { schema: resolver(v.any()) }, 89 + }, 90 + }, 91 + }, 92 + }), 93 + validator("param", v.object({ projectId: v.string() })), 94 + validator( 95 + "json", 96 + v.object({ 97 + columns: v.array( 98 + v.object({ 99 + id: v.string(), 100 + position: v.number(), 101 + }), 102 + ), 103 + }), 104 + ), 105 + workspaceAccess.fromProject("projectId"), 106 + async (c) => { 107 + const { projectId } = c.req.valid("param"); 108 + const { columns } = c.req.valid("json"); 109 + const result = await reorderColumns(projectId, columns); 110 + return c.json(result); 111 + }, 112 + ) 113 + .put( 114 + "/:id", 115 + describeRoute({ 116 + operationId: "updateColumn", 117 + tags: ["Columns"], 118 + description: "Update a column", 119 + responses: { 120 + 200: { 121 + description: "Column updated successfully", 122 + content: { 123 + "application/json": { schema: resolver(v.any()) }, 124 + }, 125 + }, 126 + }, 127 + }), 128 + validator("param", v.object({ id: v.string() })), 129 + validator( 130 + "json", 131 + v.object({ 132 + name: v.optional(v.string()), 133 + icon: v.optional(v.nullable(v.string())), 134 + color: v.optional(v.nullable(v.string())), 135 + isFinal: v.optional(v.boolean()), 136 + }), 137 + ), 138 + workspaceAccess.fromColumn("id"), 139 + async (c) => { 140 + const { id } = c.req.valid("param"); 141 + const data = c.req.valid("json"); 142 + const result = await updateColumn(id, data); 143 + return c.json(result); 144 + }, 145 + ) 146 + .delete( 147 + "/:id", 148 + describeRoute({ 149 + operationId: "deleteColumn", 150 + tags: ["Columns"], 151 + description: "Delete a column", 152 + responses: { 153 + 200: { 154 + description: "Column deleted successfully", 155 + content: { 156 + "application/json": { schema: resolver(v.any()) }, 157 + }, 158 + }, 159 + }, 160 + }), 161 + validator("param", v.object({ id: v.string() })), 162 + workspaceAccess.fromColumn("id"), 163 + async (c) => { 164 + const { id } = c.req.valid("param"); 165 + const result = await deleteColumn(id); 166 + return c.json(result); 167 + }, 168 + ); 169 + 170 + export default column;
+8
apps/api/src/database/index.ts
··· 5 5 accountTableRelations, 6 6 activityTableRelations, 7 7 apikeyTableRelations, 8 + columnTableRelations, 8 9 externalLinkTableRelations, 9 10 githubIntegrationTableRelations, 10 11 integrationTableRelations, ··· 19 20 timeEntryTableRelations, 20 21 userTableRelations, 21 22 verificationTableRelations, 23 + workflowRuleTableRelations, 22 24 workspaceTableRelations, 23 25 workspaceUserTableRelations, 24 26 } from "./relations"; ··· 26 28 accountTable, 27 29 activityTable, 28 30 apikeyTable, 31 + columnTable, 29 32 externalLinkTable, 30 33 githubIntegrationTable, 31 34 integrationTable, ··· 40 43 timeEntryTable, 41 44 userTable, 42 45 verificationTable, 46 + workflowRuleTable, 43 47 workspaceTable, 44 48 workspaceUserTable, 45 49 } from "./schema"; ··· 56 60 accountTable, 57 61 activityTable, 58 62 apikeyTable, 63 + columnTable, 59 64 externalLinkTable, 60 65 githubIntegrationTable, 61 66 integrationTable, ··· 70 75 timeEntryTable, 71 76 userTable, 72 77 verificationTable, 78 + workflowRuleTable, 73 79 workspaceTable, 74 80 workspaceUserTable, 75 81 accountTableRelations, 76 82 activityTableRelations, 77 83 apikeyTableRelations, 84 + columnTableRelations, 78 85 externalLinkTableRelations, 79 86 githubIntegrationTableRelations, 80 87 integrationTableRelations, ··· 89 96 timeEntryTableRelations, 90 97 userTableRelations, 91 98 verificationTableRelations, 99 + workflowRuleTableRelations, 92 100 workspaceTableRelations, 93 101 workspaceUserTableRelations, 94 102 };
+31
apps/api/src/database/relations.ts
··· 3 3 accountTable, 4 4 activityTable, 5 5 apikeyTable, 6 + columnTable, 6 7 externalLinkTable, 7 8 githubIntegrationTable, 8 9 integrationTable, ··· 17 18 timeEntryTable, 18 19 userTable, 19 20 verificationTable, 21 + workflowRuleTable, 20 22 workspaceTable, 21 23 workspaceUserTable, 22 24 } from "./schema"; ··· 86 88 references: [workspaceTable.id], 87 89 }), 88 90 tasks: many(taskTable), 91 + columns: many(columnTable), 92 + workflowRules: many(workflowRuleTable), 89 93 githubIntegration: many(githubIntegrationTable), 90 94 integrations: many(integrationTable), 91 95 }), 92 96 ); 93 97 98 + export const columnTableRelations = relations(columnTable, ({ one, many }) => ({ 99 + project: one(projectTable, { 100 + fields: [columnTable.projectId], 101 + references: [projectTable.id], 102 + }), 103 + tasks: many(taskTable), 104 + workflowRules: many(workflowRuleTable), 105 + })); 106 + 107 + export const workflowRuleTableRelations = relations( 108 + workflowRuleTable, 109 + ({ one }) => ({ 110 + project: one(projectTable, { 111 + fields: [workflowRuleTable.projectId], 112 + references: [projectTable.id], 113 + }), 114 + column: one(columnTable, { 115 + fields: [workflowRuleTable.columnId], 116 + references: [columnTable.id], 117 + }), 118 + }), 119 + ); 120 + 94 121 export const taskTableRelations = relations(taskTable, ({ one, many }) => ({ 95 122 project: one(projectTable, { 96 123 fields: [taskTable.projectId], ··· 99 126 assignee: one(userTable, { 100 127 fields: [taskTable.userId], 101 128 references: [userTable.id], 129 + }), 130 + column: one(columnTable, { 131 + fields: [taskTable.columnId], 132 + references: [columnTable.id], 102 133 }), 103 134 timeEntries: many(timeEntryTable), 104 135 activities: many(activityTable),
+60
apps/api/src/database/schema.ts
··· 209 209 isPublic: boolean("is_public").default(false), 210 210 }); 211 211 212 + export const columnTable = pgTable( 213 + "column", 214 + { 215 + id: text("id") 216 + .$defaultFn(() => createId()) 217 + .primaryKey(), 218 + projectId: text("project_id") 219 + .notNull() 220 + .references(() => projectTable.id, { 221 + onDelete: "cascade", 222 + onUpdate: "cascade", 223 + }), 224 + name: text("name").notNull(), 225 + slug: text("slug").notNull(), 226 + position: integer("position").notNull().default(0), 227 + icon: text("icon"), 228 + color: text("color"), 229 + isFinal: boolean("is_final").default(false).notNull(), 230 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 231 + updatedAt: timestamp("updated_at", { mode: "date" }) 232 + .defaultNow() 233 + .$onUpdate(() => new Date()) 234 + .notNull(), 235 + }, 236 + (table) => [index("column_projectId_idx").on(table.projectId)], 237 + ); 238 + 239 + export const workflowRuleTable = pgTable( 240 + "workflow_rule", 241 + { 242 + id: text("id") 243 + .$defaultFn(() => createId()) 244 + .primaryKey(), 245 + projectId: text("project_id") 246 + .notNull() 247 + .references(() => projectTable.id, { 248 + onDelete: "cascade", 249 + onUpdate: "cascade", 250 + }), 251 + integrationType: text("integration_type").notNull(), 252 + eventType: text("event_type").notNull(), 253 + columnId: text("column_id") 254 + .notNull() 255 + .references(() => columnTable.id, { 256 + onDelete: "cascade", 257 + onUpdate: "cascade", 258 + }), 259 + createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 260 + updatedAt: timestamp("updated_at", { mode: "date" }) 261 + .defaultNow() 262 + .$onUpdate(() => new Date()) 263 + .notNull(), 264 + }, 265 + (table) => [index("workflow_rule_projectId_idx").on(table.projectId)], 266 + ); 267 + 212 268 export const taskTable = pgTable("task", { 213 269 id: text("id") 214 270 .$defaultFn(() => createId()) ··· 228 284 title: text("title").notNull(), 229 285 description: text("description"), 230 286 status: text("status").notNull().default("to-do"), 287 + columnId: text("column_id").references(() => columnTable.id, { 288 + onDelete: "set null", 289 + onUpdate: "cascade", 290 + }), 231 291 priority: text("priority").default("low"), 232 292 dueDate: timestamp("due_date", { mode: "date" }), 233 293 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
+8
apps/api/src/index.ts
··· 7 7 import { openAPIRouteHandler } from "hono-openapi"; 8 8 import activity from "./activity"; 9 9 import { auth } from "./auth"; 10 + import column from "./column"; 10 11 import config from "./config"; 11 12 import db from "./database"; 12 13 import externalLink from "./external-link"; ··· 15 16 } from "./github-integration"; 16 17 import invitation from "./invitation"; 17 18 import label from "./label"; 19 + import { migrateColumns } from "./migrations/column-migration"; 18 20 import notification from "./notification"; 19 21 import { initializePlugins } from "./plugins"; 20 22 import { migrateGitHubIntegration } from "./plugins/github/migration"; ··· 27 29 import { migrateSessionColumn } from "./utils/migrate-session-column"; 28 30 import { migrateWorkspaceUserEmail } from "./utils/migrate-workspace-user-email"; 29 31 import { verifyApiKey } from "./utils/verify-api-key"; 32 + import workflowRule from "./workflow-rule"; 30 33 31 34 type ApiKey = { 32 35 id: string; ··· 176 179 177 180 const projectApi = api.route("/project", project); 178 181 const taskApi = api.route("/task", task); 182 + const columnApi = api.route("/column", column); 179 183 const activityApi = api.route("/activity", activity); 180 184 const timeEntryApi = api.route("/time-entry", timeEntry); 181 185 const labelApi = api.route("/label", label); ··· 186 190 githubIntegration, 187 191 ); 188 192 const externalLinkApi = api.route("/external-link", externalLink); 193 + const workflowRuleApi = api.route("/workflow-rule", workflowRule); 189 194 const invitationApi = api.route("/invitation", invitation); 190 195 191 196 app.route("/api", api); ··· 202 207 console.log("✅ Database migrated successfully!"); 203 208 204 209 await migrateGitHubIntegration(); 210 + await migrateColumns(); 205 211 206 212 initializePlugins(); 207 213 } catch (error) { ··· 226 232 | typeof configApi 227 233 | typeof projectApi 228 234 | typeof taskApi 235 + | typeof columnApi 229 236 | typeof activityApi 230 237 | typeof timeEntryApi 231 238 | typeof labelApi ··· 233 240 | typeof searchApi 234 241 | typeof githubIntegrationApi 235 242 | typeof externalLinkApi 243 + | typeof workflowRuleApi 236 244 | typeof invitationApi 237 245 | typeof invitationPublicApi; 238 246
+165
apps/api/src/migrations/column-migration.ts
··· 1 + import { and, eq, sql } from "drizzle-orm"; 2 + import db from "../database"; 3 + import { 4 + columnTable, 5 + integrationTable, 6 + projectTable, 7 + taskTable, 8 + workflowRuleTable, 9 + } from "../database/schema"; 10 + 11 + const DEFAULT_COLUMNS = [ 12 + { name: "To Do", slug: "to-do", position: 0, isFinal: false }, 13 + { name: "In Progress", slug: "in-progress", position: 1, isFinal: false }, 14 + { name: "In Review", slug: "in-review", position: 2, isFinal: false }, 15 + { name: "Done", slug: "done", position: 3, isFinal: true }, 16 + ]; 17 + 18 + const EVENT_MAPPING: Record<string, string> = { 19 + onBranchPush: "branch_push", 20 + onPROpen: "pr_opened", 21 + onPRMerge: "pr_merged", 22 + }; 23 + 24 + export async function migrateColumns() { 25 + console.log("🔄 Starting column migration..."); 26 + 27 + const projects = await db.select().from(projectTable); 28 + 29 + if (projects.length === 0) { 30 + console.log("No projects found, skipping column migration"); 31 + return; 32 + } 33 + 34 + for (const project of projects) { 35 + const projectColumns = await db 36 + .select({ 37 + id: columnTable.id, 38 + slug: columnTable.slug, 39 + }) 40 + .from(columnTable) 41 + .where(eq(columnTable.projectId, project.id)); 42 + 43 + const columnMap = new Map<string, string>( 44 + projectColumns.map((column) => [column.slug, column.id]), 45 + ); 46 + 47 + for (const defaultColumn of DEFAULT_COLUMNS) { 48 + if (columnMap.has(defaultColumn.slug)) { 49 + continue; 50 + } 51 + 52 + const [inserted] = await db 53 + .insert(columnTable) 54 + .values({ 55 + projectId: project.id, 56 + name: defaultColumn.name, 57 + slug: defaultColumn.slug, 58 + position: defaultColumn.position, 59 + isFinal: defaultColumn.isFinal, 60 + }) 61 + .returning({ id: columnTable.id, slug: columnTable.slug }); 62 + 63 + if (inserted) { 64 + columnMap.set(inserted.slug, inserted.id); 65 + } 66 + } 67 + 68 + for (const [slug, columnId] of columnMap) { 69 + await db 70 + .update(taskTable) 71 + .set({ columnId }) 72 + .where( 73 + sql`${taskTable.projectId} = ${project.id} 74 + AND ${taskTable.status} = ${slug} 75 + AND ${taskTable.columnId} IS DISTINCT FROM ${columnId}`, 76 + ); 77 + } 78 + 79 + const integrations = await db.query.integrationTable.findMany({ 80 + where: eq(integrationTable.projectId, project.id), 81 + }); 82 + 83 + for (const integration of integrations) { 84 + if (integration.type !== "github" || !integration.isActive) continue; 85 + 86 + try { 87 + const config = JSON.parse(integration.config); 88 + const transitions = config.statusTransitions || {}; 89 + 90 + for (const [configKey, eventType] of Object.entries(EVENT_MAPPING)) { 91 + const targetSlug = transitions[configKey]; 92 + if (!targetSlug) continue; 93 + 94 + const targetColumnId = columnMap.get(targetSlug); 95 + if (!targetColumnId) continue; 96 + 97 + await upsertMigrationWorkflowRule( 98 + project.id, 99 + eventType as string, 100 + targetColumnId, 101 + ); 102 + } 103 + 104 + // Add default rules for issue events 105 + const todoColumnId = columnMap.get("to-do"); 106 + const doneColumnId = columnMap.get("done"); 107 + 108 + if (todoColumnId) { 109 + await upsertMigrationWorkflowRule( 110 + project.id, 111 + "issue_opened", 112 + todoColumnId, 113 + ); 114 + } 115 + 116 + if (doneColumnId) { 117 + await upsertMigrationWorkflowRule( 118 + project.id, 119 + "issue_closed", 120 + doneColumnId, 121 + ); 122 + } 123 + } catch { 124 + console.error( 125 + `Failed to migrate workflow rules for integration ${integration.id}`, 126 + ); 127 + } 128 + } 129 + } 130 + 131 + console.log( 132 + `✅ Column migration complete! Migrated ${projects.length} projects`, 133 + ); 134 + } 135 + 136 + async function upsertMigrationWorkflowRule( 137 + projectId: string, 138 + eventType: string, 139 + columnId: string, 140 + ) { 141 + const existing = await db.query.workflowRuleTable.findFirst({ 142 + where: and( 143 + eq(workflowRuleTable.projectId, projectId), 144 + eq(workflowRuleTable.integrationType, "github"), 145 + eq(workflowRuleTable.eventType, eventType), 146 + ), 147 + }); 148 + 149 + if (!existing) { 150 + await db.insert(workflowRuleTable).values({ 151 + projectId, 152 + integrationType: "github", 153 + eventType, 154 + columnId, 155 + }); 156 + return; 157 + } 158 + 159 + if (existing.columnId !== columnId) { 160 + await db 161 + .update(workflowRuleTable) 162 + .set({ columnId }) 163 + .where(eq(workflowRuleTable.id, existing.id)); 164 + } 165 + }
+21 -2
apps/api/src/plugins/github/services/task-service.ts
··· 1 1 import { and, eq } from "drizzle-orm"; 2 2 import db from "../../../database"; 3 - import { integrationTable, taskTable } from "../../../database/schema"; 3 + import { 4 + columnTable, 5 + integrationTable, 6 + taskTable, 7 + } from "../../../database/schema"; 4 8 5 9 export async function findTaskByNumber(projectId: string, taskNumber: number) { 6 10 return db.query.taskTable.findFirst({ ··· 18 22 } 19 23 20 24 export async function updateTaskStatus(taskId: string, newStatus: string) { 25 + const task = await db.query.taskTable.findFirst({ 26 + where: eq(taskTable.id, taskId), 27 + }); 28 + 29 + let columnId: string | null = null; 30 + if (task) { 31 + const column = await db.query.columnTable.findFirst({ 32 + where: and( 33 + eq(columnTable.projectId, task.projectId), 34 + eq(columnTable.slug, newStatus), 35 + ), 36 + }); 37 + columnId = column?.id ?? null; 38 + } 39 + 21 40 await db 22 41 .update(taskTable) 23 - .set({ status: newStatus }) 42 + .set({ status: newStatus, columnId }) 24 43 .where(eq(taskTable.id, taskId)); 25 44 } 26 45
+27
apps/api/src/plugins/github/utils/resolve-column.ts
··· 1 + import { and, eq } from "drizzle-orm"; 2 + import db from "../../../database"; 3 + import { columnTable, workflowRuleTable } from "../../../database/schema"; 4 + 5 + export async function resolveTargetStatus( 6 + projectId: string, 7 + eventType: string, 8 + fallbackStatus: string, 9 + ): Promise<string> { 10 + const rule = await db.query.workflowRuleTable.findFirst({ 11 + where: and( 12 + eq(workflowRuleTable.projectId, projectId), 13 + eq(workflowRuleTable.integrationType, "github"), 14 + eq(workflowRuleTable.eventType, eventType), 15 + ), 16 + }); 17 + 18 + if (!rule) { 19 + return fallbackStatus; 20 + } 21 + 22 + const column = await db.query.columnTable.findFirst({ 23 + where: eq(columnTable.id, rule.columnId), 24 + }); 25 + 26 + return column?.slug ?? fallbackStatus; 27 + }
+12 -5
apps/api/src/plugins/github/webhooks/issue-closed.ts
··· 2 2 import db from "../../../database"; 3 3 import { externalLinkTable, taskTable } from "../../../database/schema"; 4 4 import { updateExternalLink } from "../services/link-manager"; 5 - import { findAllIntegrationsByRepo } from "../services/task-service"; 5 + import { 6 + findAllIntegrationsByRepo, 7 + updateTaskStatus, 8 + } from "../services/task-service"; 9 + import { resolveTargetStatus } from "../utils/resolve-column"; 6 10 7 11 type IssueClosedPayload = { 8 12 action: string; ··· 56 60 continue; 57 61 } 58 62 59 - await db 60 - .update(taskTable) 61 - .set({ status: "done" }) 62 - .where(eq(taskTable.id, task.id)); 63 + const targetStatus = await resolveTargetStatus( 64 + task.projectId, 65 + "issue_closed", 66 + "done", 67 + ); 68 + 69 + await updateTaskStatus(task.id, targetStatus); 63 70 64 71 await updateExternalLink(externalLink.id, { 65 72 metadata: {
+19 -4
apps/api/src/plugins/github/webhooks/issue-opened.ts
··· 1 - import { eq } from "drizzle-orm"; 1 + import { and, eq } from "drizzle-orm"; 2 2 import db from "../../../database"; 3 - import { projectTable, taskTable } from "../../../database/schema"; 3 + import { columnTable, projectTable, taskTable } from "../../../database/schema"; 4 4 import getNextTaskNumber from "../../../task/controllers/get-next-task-number"; 5 5 import type { GitHubConfig } from "../config"; 6 6 import { createExternalLink, findExternalLink } from "../services/link-manager"; ··· 12 12 import { formatTaskDescriptionFromIssue } from "../utils/format"; 13 13 import { getGithubApp } from "../utils/github-app"; 14 14 import { addLabelsToIssue } from "../utils/labels"; 15 + import { resolveTargetStatus } from "../utils/resolve-column"; 15 16 16 17 type IssueOpenedPayload = { 17 18 action: string; ··· 69 70 70 71 const nextTaskNumber = await getNextTaskNumber(projectId); 71 72 73 + const resolvedStatus = await resolveTargetStatus( 74 + projectId, 75 + "issue_opened", 76 + "to-do", 77 + ); 78 + 79 + const targetStatus = status || resolvedStatus; 80 + const targetColumn = await db.query.columnTable.findFirst({ 81 + where: and( 82 + eq(columnTable.projectId, projectId), 83 + eq(columnTable.slug, targetStatus), 84 + ), 85 + }); 86 + 72 87 const taskValues: typeof taskTable.$inferInsert = { 73 88 projectId, 74 89 userId: null, 75 90 title: issue.title, 76 91 description: formatTaskDescriptionFromIssue(issue.body), 77 - status: "to-do", 92 + status: targetStatus, 93 + columnId: targetColumn?.id ?? null, 78 94 priority: null, 79 95 number: nextTaskNumber + 1, 80 96 }; 81 97 82 98 if (priority) taskValues.priority = priority; 83 - if (status) taskValues.status = status; 84 99 85 100 const [createdTask] = await db 86 101 .insert(taskTable)
+6 -1
apps/api/src/plugins/github/webhooks/pull-request-closed.ts
··· 8 8 findTaskById, 9 9 updateTaskStatus, 10 10 } from "../services/task-service"; 11 + import { resolveTargetStatus } from "../utils/resolve-column"; 11 12 12 13 type PRClosedPayload = { 13 14 action: string; ··· 85 86 }); 86 87 87 88 if (!hasOpenPRs) { 88 - const targetStatus = config.statusTransitions?.onPRMerge || "done"; 89 + const targetStatus = await resolveTargetStatus( 90 + integration.projectId, 91 + "pr_merged", 92 + config.statusTransitions?.onPRMerge || "done", 93 + ); 89 94 await updateTaskStatus(task.id, targetStatus); 90 95 } 91 96 }
+6 -1
apps/api/src/plugins/github/webhooks/pull-request-opened.ts
··· 6 6 updateTaskStatus, 7 7 } from "../services/task-service"; 8 8 import { extractTaskNumber } from "../utils/branch-matcher"; 9 + import { resolveTargetStatus } from "../utils/resolve-column"; 9 10 10 11 type PROpenedPayload = { 11 12 action: string; ··· 89 90 }, 90 91 }); 91 92 92 - const targetStatus = config.statusTransitions?.onPROpen || "in-review"; 93 + const targetStatus = await resolveTargetStatus( 94 + integration.projectId, 95 + "pr_opened", 96 + config.statusTransitions?.onPROpen || "in-review", 97 + ); 93 98 94 99 if (task.status !== targetStatus && task.status !== "done") { 95 100 await updateTaskStatus(task.id, targetStatus);
+6 -2
apps/api/src/plugins/github/webhooks/push.ts
··· 6 6 updateTaskStatus, 7 7 } from "../services/task-service"; 8 8 import { extractTaskNumberFromBranch } from "../utils/branch-matcher"; 9 + import { resolveTargetStatus } from "../utils/resolve-column"; 9 10 10 11 type PushPayload = { 11 12 ref: string; ··· 117 118 }, 118 119 }); 119 120 120 - const targetStatus = 121 - config.statusTransitions?.onBranchPush || "in-progress"; 121 + const targetStatus = await resolveTargetStatus( 122 + integration.projectId, 123 + "branch_push", 124 + config.statusTransitions?.onBranchPush || "in-progress", 125 + ); 122 126 console.log( 123 127 `[Push] Target status: ${targetStatus}, current: ${task.status}`, 124 128 );
+20 -1
apps/api/src/project/controllers/create-project.ts
··· 1 1 import db from "../../database"; 2 - import { projectTable } from "../../database/schema"; 2 + import { columnTable, projectTable } from "../../database/schema"; 3 + 4 + const DEFAULT_COLUMNS = [ 5 + { name: "To Do", slug: "to-do", position: 0, isFinal: false }, 6 + { name: "In Progress", slug: "in-progress", position: 1, isFinal: false }, 7 + { name: "In Review", slug: "in-review", position: 2, isFinal: false }, 8 + { name: "Done", slug: "done", position: 3, isFinal: true }, 9 + ]; 3 10 4 11 async function createProject( 5 12 workspaceId: string, ··· 16 23 slug, 17 24 }) 18 25 .returning(); 26 + 27 + if (createdProject) { 28 + for (const col of DEFAULT_COLUMNS) { 29 + await db.insert(columnTable).values({ 30 + projectId: createdProject.id, 31 + name: col.name, 32 + slug: col.slug, 33 + position: col.position, 34 + isFinal: col.isFinal, 35 + }); 36 + } 37 + } 19 38 20 39 return createdProject; 21 40 }
+1 -8
apps/api/src/schemas.ts
··· 28 28 userId: v.nullable(v.string()), 29 29 title: v.string(), 30 30 description: v.nullable(v.string()), 31 - status: v.picklist([ 32 - "to-do", 33 - "in-progress", 34 - "in-review", 35 - "done", 36 - "archived", 37 - "planned", 38 - ] as const), 31 + status: v.string(), 39 32 priority: v.picklist([ 40 33 "no-priority", 41 34 "low",
+10 -2
apps/api/src/task/controllers/create-task.ts
··· 1 - import { eq } from "drizzle-orm"; 1 + import { and, eq } from "drizzle-orm"; 2 2 import { HTTPException } from "hono/http-exception"; 3 3 import db from "../../database"; 4 - import { taskTable, userTable } from "../../database/schema"; 4 + import { columnTable, taskTable, userTable } from "../../database/schema"; 5 5 import { publishEvent } from "../../events"; 6 6 import getNextTaskNumber from "./get-next-task-number"; 7 7 ··· 29 29 30 30 const nextTaskNumber = await getNextTaskNumber(projectId); 31 31 32 + const column = await db.query.columnTable.findFirst({ 33 + where: and( 34 + eq(columnTable.projectId, projectId), 35 + eq(columnTable.slug, status || "to-do"), 36 + ), 37 + }); 38 + 32 39 const [createdTask] = await db 33 40 .insert(taskTable) 34 41 .values({ ··· 36 43 userId: userId || null, 37 44 title: title || "", 38 45 status: status || "", 46 + columnId: column?.id ?? null, 39 47 dueDate: dueDate || null, 40 48 description: description || "", 41 49 priority: priority || "",
+12 -11
apps/api/src/task/controllers/get-tasks.ts
··· 1 - import { eq, inArray } from "drizzle-orm"; 1 + import { asc, eq, inArray } from "drizzle-orm"; 2 2 import { HTTPException } from "hono/http-exception"; 3 3 import db from "../../database"; 4 4 import { 5 + columnTable, 5 6 externalLinkTable, 6 7 labelTable, 7 8 projectTable, 8 9 taskTable, 9 10 userTable, 10 11 } from "../../database/schema"; 11 - 12 - const DEFAULT_COLUMNS = [ 13 - { id: "to-do", name: "To Do" }, 14 - { id: "in-progress", name: "In Progress" }, 15 - { id: "in-review", name: "In Review" }, 16 - { id: "done", name: "Done" }, 17 - ] as const; 18 12 19 13 async function getTasks(projectId: string) { 20 14 const project = await db.query.projectTable.findFirst({ ··· 115 109 }); 116 110 } 117 111 118 - const columns = DEFAULT_COLUMNS.map((column) => ({ 119 - id: column.id, 112 + const projectColumns = await db 113 + .select() 114 + .from(columnTable) 115 + .where(eq(columnTable.projectId, projectId)) 116 + .orderBy(asc(columnTable.position)); 117 + 118 + const columns = projectColumns.map((column) => ({ 119 + id: column.slug, 120 120 name: column.name, 121 + isFinal: column.isFinal, 121 122 tasks: tasks 122 - .filter((task) => task.status === column.id) 123 + .filter((task) => task.status === column.slug) 123 124 .map((task) => ({ 124 125 ...task, 125 126 labels: taskLabelsMap.get(task.id) || [],
+10 -2
apps/api/src/task/controllers/import-tasks.ts
··· 1 - import { eq } from "drizzle-orm"; 1 + import { and, eq } from "drizzle-orm"; 2 2 import { HTTPException } from "hono/http-exception"; 3 3 import db from "../../database"; 4 - import { projectTable, taskTable } from "../../database/schema"; 4 + import { columnTable, projectTable, taskTable } from "../../database/schema"; 5 5 import { publishEvent } from "../../events"; 6 6 import getNextTaskNumber from "./get-next-task-number"; 7 7 ··· 32 32 33 33 for (const taskData of tasksToImport) { 34 34 try { 35 + const column = await db.query.columnTable.findFirst({ 36 + where: and( 37 + eq(columnTable.projectId, projectId), 38 + eq(columnTable.slug, taskData.status), 39 + ), 40 + }); 41 + 35 42 const [createdTask] = await db 36 43 .insert(taskTable) 37 44 .values({ ··· 39 46 userId: taskData.userId || null, 40 47 title: taskData.title, 41 48 status: taskData.status, 49 + columnId: column?.id ?? null, 42 50 dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null, 43 51 description: taskData.description || "", 44 52 priority: taskData.priority || "low",
+13 -3
apps/api/src/task/controllers/update-task-status.ts
··· 1 - import { eq } from "drizzle-orm"; 1 + import { and, eq } from "drizzle-orm"; 2 2 import { HTTPException } from "hono/http-exception"; 3 3 import db from "../../database"; 4 - import { taskTable } from "../../database/schema"; 4 + import { columnTable, taskTable } from "../../database/schema"; 5 5 6 6 async function updateTaskStatus({ 7 7 id, ··· 20 20 }); 21 21 } 22 22 23 - await db.update(taskTable).set({ status }).where(eq(taskTable.id, id)); 23 + const column = await db.query.columnTable.findFirst({ 24 + where: and( 25 + eq(columnTable.projectId, updatedTask.projectId), 26 + eq(columnTable.slug, status), 27 + ), 28 + }); 29 + 30 + await db 31 + .update(taskTable) 32 + .set({ status, columnId: column?.id ?? null }) 33 + .where(eq(taskTable.id, id)); 24 34 25 35 return updatedTask; 26 36 }
+10 -2
apps/api/src/task/controllers/update-task.ts
··· 1 - import { eq } from "drizzle-orm"; 1 + import { and, eq } from "drizzle-orm"; 2 2 import { HTTPException } from "hono/http-exception"; 3 3 import db from "../../database"; 4 - import { taskTable } from "../../database/schema"; 4 + import { columnTable, taskTable } from "../../database/schema"; 5 5 6 6 async function updateTask( 7 7 id: string, ··· 24 24 }); 25 25 } 26 26 27 + const column = await db.query.columnTable.findFirst({ 28 + where: and( 29 + eq(columnTable.projectId, projectId), 30 + eq(columnTable.slug, status), 31 + ), 32 + }); 33 + 27 34 const [updatedTask] = await db 28 35 .update(taskTable) 29 36 .set({ 30 37 title, 31 38 status, 39 + columnId: column?.id ?? null, 32 40 dueDate: dueDate || null, 33 41 projectId, 34 42 description,
+56 -2
apps/api/src/utils/workspace-access-middleware.ts
··· 10 10 | { type: "param"; key: string } 11 11 | { 12 12 type: "lookup"; 13 - resource: "project" | "task" | "label" | "timeEntry" | "activity"; 13 + resource: 14 + | "project" 15 + | "task" 16 + | "label" 17 + | "timeEntry" 18 + | "activity" 19 + | "column" 20 + | "workflowRule"; 14 21 idKey: string; 15 22 }; 16 23 ··· 72 79 } 73 80 74 81 async function lookupWorkspaceId( 75 - resource: "project" | "task" | "label" | "timeEntry" | "activity", 82 + resource: 83 + | "project" 84 + | "task" 85 + | "label" 86 + | "timeEntry" 87 + | "activity" 88 + | "column" 89 + | "workflowRule", 76 90 id: string, 77 91 ): Promise<string | null> { 78 92 try { ··· 148 162 return activity?.workspaceId || null; 149 163 } 150 164 165 + case "column": { 166 + const [column] = await db 167 + .select({ 168 + workspaceId: schema.projectTable.workspaceId, 169 + }) 170 + .from(schema.columnTable) 171 + .innerJoin( 172 + schema.projectTable, 173 + eq(schema.columnTable.projectId, schema.projectTable.id), 174 + ) 175 + .where(eq(schema.columnTable.id, id)) 176 + .limit(1); 177 + return column?.workspaceId || null; 178 + } 179 + 180 + case "workflowRule": { 181 + const [workflowRule] = await db 182 + .select({ 183 + workspaceId: schema.projectTable.workspaceId, 184 + }) 185 + .from(schema.workflowRuleTable) 186 + .innerJoin( 187 + schema.projectTable, 188 + eq(schema.workflowRuleTable.projectId, schema.projectTable.id), 189 + ) 190 + .where(eq(schema.workflowRuleTable.id, id)) 191 + .limit(1); 192 + return workflowRule?.workspaceId || null; 193 + } 194 + 151 195 default: 152 196 return null; 153 197 } ··· 198 242 fromActivity: (idKey = "id") => 199 243 workspaceAccessMiddleware({ 200 244 sources: [{ type: "lookup", resource: "activity", idKey }], 245 + }), 246 + 247 + fromColumn: (idKey = "id") => 248 + workspaceAccessMiddleware({ 249 + sources: [{ type: "lookup", resource: "column", idKey }], 250 + }), 251 + 252 + fromWorkflowRule: (idKey = "id") => 253 + workspaceAccessMiddleware({ 254 + sources: [{ type: "lookup", resource: "workflowRule", idKey }], 201 255 }), 202 256 };
+20
apps/api/src/workflow-rule/controllers/delete-workflow-rule.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { workflowRuleTable } from "../../database/schema"; 5 + 6 + async function deleteWorkflowRule(id: string) { 7 + const existing = await db.query.workflowRuleTable.findFirst({ 8 + where: eq(workflowRuleTable.id, id), 9 + }); 10 + 11 + if (!existing) { 12 + throw new HTTPException(404, { message: "Workflow rule not found" }); 13 + } 14 + 15 + await db.delete(workflowRuleTable).where(eq(workflowRuleTable.id, id)); 16 + 17 + return existing; 18 + } 19 + 20 + export default deleteWorkflowRule;
+25
apps/api/src/workflow-rule/controllers/get-workflow-rules.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import db from "../../database"; 3 + import { columnTable, workflowRuleTable } from "../../database/schema"; 4 + 5 + async function getWorkflowRules(projectId: string) { 6 + const rules = await db 7 + .select({ 8 + id: workflowRuleTable.id, 9 + projectId: workflowRuleTable.projectId, 10 + integrationType: workflowRuleTable.integrationType, 11 + eventType: workflowRuleTable.eventType, 12 + columnId: workflowRuleTable.columnId, 13 + columnName: columnTable.name, 14 + columnSlug: columnTable.slug, 15 + createdAt: workflowRuleTable.createdAt, 16 + updatedAt: workflowRuleTable.updatedAt, 17 + }) 18 + .from(workflowRuleTable) 19 + .leftJoin(columnTable, eq(workflowRuleTable.columnId, columnTable.id)) 20 + .where(eq(workflowRuleTable.projectId, projectId)); 21 + 22 + return rules; 23 + } 24 + 25 + export default getWorkflowRules;
+73
apps/api/src/workflow-rule/controllers/upsert-workflow-rule.ts
··· 1 + import { and, eq } from "drizzle-orm"; 2 + import { HTTPException } from "hono/http-exception"; 3 + import db from "../../database"; 4 + import { columnTable, workflowRuleTable } from "../../database/schema"; 5 + 6 + async function upsertWorkflowRule({ 7 + projectId, 8 + integrationType, 9 + eventType, 10 + columnId, 11 + }: { 12 + projectId: string; 13 + integrationType: string; 14 + eventType: string; 15 + columnId: string; 16 + }) { 17 + const targetColumn = await db.query.columnTable.findFirst({ 18 + where: and( 19 + eq(columnTable.id, columnId), 20 + eq(columnTable.projectId, projectId), 21 + ), 22 + }); 23 + 24 + if (!targetColumn) { 25 + throw new HTTPException(400, { 26 + message: "Column does not belong to the provided project", 27 + }); 28 + } 29 + 30 + const existing = await db.query.workflowRuleTable.findFirst({ 31 + where: and( 32 + eq(workflowRuleTable.projectId, projectId), 33 + eq(workflowRuleTable.integrationType, integrationType), 34 + eq(workflowRuleTable.eventType, eventType), 35 + ), 36 + }); 37 + 38 + if (existing) { 39 + const [updated] = await db 40 + .update(workflowRuleTable) 41 + .set({ columnId }) 42 + .where(eq(workflowRuleTable.id, existing.id)) 43 + .returning(); 44 + 45 + if (!updated) { 46 + throw new HTTPException(500, { 47 + message: "Failed to update workflow rule", 48 + }); 49 + } 50 + 51 + return updated; 52 + } 53 + 54 + const [created] = await db 55 + .insert(workflowRuleTable) 56 + .values({ 57 + projectId, 58 + integrationType, 59 + eventType, 60 + columnId, 61 + }) 62 + .returning(); 63 + 64 + if (!created) { 65 + throw new HTTPException(500, { 66 + message: "Failed to create workflow rule", 67 + }); 68 + } 69 + 70 + return created; 71 + } 72 + 73 + export default upsertWorkflowRule;
+98
apps/api/src/workflow-rule/index.ts
··· 1 + import { Hono } from "hono"; 2 + import { describeRoute, resolver, validator } from "hono-openapi"; 3 + import * as v from "valibot"; 4 + import { workspaceAccess } from "../utils/workspace-access-middleware"; 5 + import deleteWorkflowRule from "./controllers/delete-workflow-rule"; 6 + import getWorkflowRules from "./controllers/get-workflow-rules"; 7 + import upsertWorkflowRule from "./controllers/upsert-workflow-rule"; 8 + 9 + const workflowRule = new Hono<{ 10 + Variables: { 11 + userId: string; 12 + }; 13 + }>() 14 + .get( 15 + "/:projectId", 16 + describeRoute({ 17 + operationId: "getWorkflowRules", 18 + tags: ["Workflow Rules"], 19 + description: "Get all workflow rules for a project", 20 + responses: { 21 + 200: { 22 + description: "List of workflow rules", 23 + content: { 24 + "application/json": { schema: resolver(v.any()) }, 25 + }, 26 + }, 27 + }, 28 + }), 29 + validator("param", v.object({ projectId: v.string() })), 30 + workspaceAccess.fromProject("projectId"), 31 + async (c) => { 32 + const { projectId } = c.req.valid("param"); 33 + const rules = await getWorkflowRules(projectId); 34 + return c.json(rules); 35 + }, 36 + ) 37 + .put( 38 + "/:projectId", 39 + describeRoute({ 40 + operationId: "upsertWorkflowRule", 41 + tags: ["Workflow Rules"], 42 + description: "Create or update a workflow rule", 43 + responses: { 44 + 200: { 45 + description: "Workflow rule upserted successfully", 46 + content: { 47 + "application/json": { schema: resolver(v.any()) }, 48 + }, 49 + }, 50 + }, 51 + }), 52 + validator("param", v.object({ projectId: v.string() })), 53 + validator( 54 + "json", 55 + v.object({ 56 + integrationType: v.string(), 57 + eventType: v.string(), 58 + columnId: v.string(), 59 + }), 60 + ), 61 + workspaceAccess.fromProject("projectId"), 62 + async (c) => { 63 + const { projectId } = c.req.valid("param"); 64 + const { integrationType, eventType, columnId } = c.req.valid("json"); 65 + const result = await upsertWorkflowRule({ 66 + projectId, 67 + integrationType, 68 + eventType, 69 + columnId, 70 + }); 71 + return c.json(result); 72 + }, 73 + ) 74 + .delete( 75 + "/:id", 76 + describeRoute({ 77 + operationId: "deleteWorkflowRule", 78 + tags: ["Workflow Rules"], 79 + description: "Delete a workflow rule", 80 + responses: { 81 + 200: { 82 + description: "Workflow rule deleted successfully", 83 + content: { 84 + "application/json": { schema: resolver(v.any()) }, 85 + }, 86 + }, 87 + }, 88 + }), 89 + validator("param", v.object({ id: v.string() })), 90 + workspaceAccess.fromWorkflowRule("id"), 91 + async (c) => { 92 + const { id } = c.req.valid("param"); 93 + const result = await deleteWorkflowRule(id); 94 + return c.json(result); 95 + }, 96 + ); 97 + 98 + export default workflowRule;
+1 -1
apps/docs/next-env.d.ts
··· 1 1 /// <reference types="next" /> 2 2 /// <reference types="next/image-types/global" /> 3 - import "./.next/types/routes.d.ts"; 3 + import "./.next/dev/types/routes.d.ts"; 4 4 5 5 // NOTE: This file should not be edited 6 6 // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+11 -16
apps/web/src/components/bulk-selection/backlog-bulk-toolbar.tsx
··· 27 27 import { useGetActiveWorkspaceUsers } from "@/hooks/queries/workspace-users/use-get-active-workspace-users"; 28 28 import { getColumnIcon } from "@/lib/column"; 29 29 import useBacklogBulkSelectionStore from "@/store/backlog-bulk-selection"; 30 + import useProjectStore from "@/store/project"; 30 31 import { Button } from "../ui/button"; 31 32 32 33 function BacklogBulkToolbar() { 33 34 const { selectedTaskIds, clearSelection, selectAll } = 34 35 useBacklogBulkSelectionStore(); 36 + const { project } = useProjectStore(); 35 37 const { bulkMoveToBoard, bulkDelete, bulkArchive, bulkAssign } = 36 38 useBulkOperations(); 37 39 const { data: workspace } = useActiveWorkspace(); ··· 133 135 </Button> 134 136 </DropdownMenuTrigger> 135 137 <DropdownMenuContent align="center" className="w-48"> 136 - <DropdownMenuItem onClick={() => handleMoveToBoard("to-do")}> 137 - {getColumnIcon("to-do")} 138 - <span className="ml-2">To Do</span> 139 - </DropdownMenuItem> 140 - <DropdownMenuItem onClick={() => handleMoveToBoard("in-progress")}> 141 - {getColumnIcon("in-progress")} 142 - <span className="ml-2">In Progress</span> 143 - </DropdownMenuItem> 144 - <DropdownMenuItem onClick={() => handleMoveToBoard("in-review")}> 145 - {getColumnIcon("in-review")} 146 - <span className="ml-2">In Review</span> 147 - </DropdownMenuItem> 148 - <DropdownMenuItem onClick={() => handleMoveToBoard("done")}> 149 - {getColumnIcon("done")} 150 - <span className="ml-2">Done</span> 151 - </DropdownMenuItem> 138 + {(project?.columns ?? []).map((col) => ( 139 + <DropdownMenuItem 140 + key={col.id} 141 + onClick={() => handleMoveToBoard(col.id)} 142 + > 143 + {getColumnIcon(col.id)} 144 + <span className="ml-2">{col.name}</span> 145 + </DropdownMenuItem> 146 + ))} 152 147 </DropdownMenuContent> 153 148 </DropdownMenu> 154 149
+11 -16
apps/web/src/components/bulk-selection/bulk-toolbar.tsx
··· 14 14 import { useGetActiveWorkspaceUsers } from "@/hooks/queries/workspace-users/use-get-active-workspace-users"; 15 15 import { getColumnIcon } from "@/lib/column"; 16 16 import useBulkSelectionStore from "@/store/bulk-selection"; 17 + import useProjectStore from "@/store/project"; 17 18 import { Button } from "../ui/button"; 18 19 19 20 function BulkToolbar() { 20 21 const { selectedTaskIds, clearSelection, selectAll } = 21 22 useBulkSelectionStore(); 23 + const { project } = useProjectStore(); 22 24 const { 23 25 bulkMoveToBacklog, 24 26 bulkDelete, ··· 166 168 </CommandGroup> 167 169 168 170 <CommandGroup heading="Change Status"> 169 - <CommandItem onSelect={() => handleBulkChangeStatus("to-do")}> 170 - {getColumnIcon("to-do")} 171 - To Do 172 - </CommandItem> 173 - <CommandItem onSelect={() => handleBulkChangeStatus("in-progress")}> 174 - {getColumnIcon("in-progress")} 175 - In Progress 176 - </CommandItem> 177 - <CommandItem onSelect={() => handleBulkChangeStatus("in-review")}> 178 - {getColumnIcon("in-review")} 179 - In Review 180 - </CommandItem> 181 - <CommandItem onSelect={() => handleBulkChangeStatus("done")}> 182 - {getColumnIcon("done")} 183 - Done 184 - </CommandItem> 171 + {(project?.columns ?? []).map((col) => ( 172 + <CommandItem 173 + key={col.id} 174 + onSelect={() => handleBulkChangeStatus(col.id)} 175 + > 176 + {getColumnIcon(col.id)} 177 + {col.name} 178 + </CommandItem> 179 + ))} 185 180 </CommandGroup> 186 181 187 182 <CommandGroup heading="Assign to">
+3 -3
apps/web/src/components/kanban-board/column/column-header.tsx
··· 15 15 const { mutate: updateTask } = useUpdateTask(); 16 16 17 17 const handleArchiveTasks = () => { 18 - if (column.id !== "done") return; 18 + if (!column.isFinal) return; 19 19 20 20 if (column.tasks.length === 0) { 21 21 toast.info("No tasks to archive"); ··· 27 27 } 28 28 29 29 const updatedProject = produce(project, (draft) => { 30 - const doneColumn = draft?.columns?.find((col) => col.id === "done"); 30 + const doneColumn = draft?.columns?.find((col) => col.isFinal); 31 31 if (!doneColumn) return; 32 32 33 33 for (const task of doneColumn.tasks) { ··· 56 56 </span> 57 57 </div> 58 58 59 - {column.id === "done" && column.tasks.length > 0 && ( 59 + {column.isFinal && column.tasks.length > 0 && ( 60 60 <button 61 61 type="button" 62 62 onClick={handleArchiveTasks}
+8 -6
apps/web/src/components/kanban-board/task-card-context-menu/task-card-context-menu-content.tsx
··· 23 23 import { getColumnIcon } from "@/lib/column"; 24 24 import { generateLink } from "@/lib/generate-link"; 25 25 import { getPriorityIcon } from "@/lib/priority"; 26 + import useProjectStore from "@/store/project"; 26 27 import type Task from "@/types/task"; 27 28 28 29 type TaskCardContext = { ··· 41 42 taskCardContext, 42 43 onDeleteClick, 43 44 }: TaskCardContextMenuContentProps) { 45 + const { project } = useProjectStore(); 44 46 const { data: workspaceUsers } = useGetActiveWorkspaceUsers( 45 47 taskCardContext.worskpaceId, 46 48 ); ··· 146 148 <span>Status</span> 147 149 </ContextMenuSubTrigger> 148 150 <ContextMenuSubContent className="w-48"> 149 - {["to-do", "in-progress", "in-review", "done"].map((status) => ( 151 + {(project?.columns ?? []).map((col) => ( 150 152 <ContextMenuCheckboxItem 151 - key={status} 152 - checked={task.status === status} 153 - onCheckedChange={() => handleChange("status", status)} 153 + key={col.id} 154 + checked={task.status === col.id} 155 + onCheckedChange={() => handleChange("status", col.id)} 154 156 className="[&_svg]:text-muted-foreground" 155 157 > 156 - {getColumnIcon(status)} 157 - <span className="capitalize">{status}</span> 158 + {getColumnIcon(col.id)} 159 + <span>{col.name}</span> 158 160 </ContextMenuCheckboxItem> 159 161 ))} 160 162 </ContextMenuSubContent>
+14 -11
apps/web/src/components/list-view/index.tsx
··· 55 55 const [overColumnId, setOverColumnId] = useState<string | null>(null); 56 56 const [expandedSections, setExpandedSections] = useState< 57 57 Record<string, boolean> 58 - >({ 59 - "to-do": true, 60 - "in-progress": true, 61 - "in-review": true, 62 - done: true, 58 + >(() => { 59 + const sections: Record<string, boolean> = {}; 60 + if (project?.columns) { 61 + for (const col of project.columns) { 62 + sections[col.id] = true; 63 + } 64 + } 65 + return sections; 63 66 }); 64 67 const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); 65 68 const [activeColumn, setActiveColumn] = useState<string | null>(null); ··· 220 223 }; 221 224 222 225 const handleArchiveTasks = (column: ProjectWithTasks["columns"][number]) => { 223 - if (column.id !== "done" || column.tasks.length === 0) return; 226 + if (!column.isFinal || column.tasks.length === 0) return; 224 227 225 228 if (!confirm(`Archive all ${column.tasks.length} completed tasks?`)) { 226 229 return; 227 230 } 228 231 229 232 const updatedProject = produce(project, (draft) => { 230 - const doneColumn = draft?.columns?.find((col) => col.id === "done"); 231 - if (!doneColumn) return; 233 + const finalColumn = draft?.columns?.find((col) => col.isFinal); 234 + if (!finalColumn) return; 232 235 233 - for (const task of doneColumn.tasks) { 236 + for (const task of finalColumn.tasks) { 234 237 updateTask({ 235 238 ...task, 236 239 status: "archived", 237 240 }); 238 241 } 239 242 240 - doneColumn.tasks = []; 243 + finalColumn.tasks = []; 241 244 }); 242 245 243 246 setProject(updatedProject); ··· 303 306 <Plus className="w-3 h-3" /> 304 307 </button> 305 308 306 - {column.id === "done" && column.tasks.length > 0 && ( 309 + {column.isFinal && column.tasks.length > 0 && ( 307 310 <button 308 311 type="button" 309 312 onClick={() => handleArchiveTasks(column)}
+188
apps/web/src/components/project/column-editor.tsx
··· 1 + import { CheckCircle2, Circle, GripVertical, Plus, Trash2 } from "lucide-react"; 2 + import { useState } from "react"; 3 + import { toast } from "sonner"; 4 + import { Button } from "@/components/ui/button"; 5 + import { Input } from "@/components/ui/input"; 6 + import { Switch } from "@/components/ui/switch"; 7 + import { useCreateColumn } from "@/hooks/mutations/column/use-create-column"; 8 + import { useDeleteColumn } from "@/hooks/mutations/column/use-delete-column"; 9 + import { useReorderColumns } from "@/hooks/mutations/column/use-reorder-columns"; 10 + import { useUpdateColumn } from "@/hooks/mutations/column/use-update-column"; 11 + import { useGetColumns } from "@/hooks/queries/column/use-get-columns"; 12 + 13 + type ColumnEditorProps = { 14 + projectId: string; 15 + }; 16 + 17 + export default function ColumnEditor({ projectId }: ColumnEditorProps) { 18 + const { data: columns, isLoading } = useGetColumns(projectId); 19 + const { mutateAsync: createColumn } = useCreateColumn(); 20 + const { mutateAsync: updateColumn } = useUpdateColumn(); 21 + const { mutateAsync: deleteColumn } = useDeleteColumn(); 22 + const { mutateAsync: reorderColumns } = useReorderColumns(); 23 + const [newColumnName, setNewColumnName] = useState(""); 24 + const [draggedIndex, setDraggedIndex] = useState<number | null>(null); 25 + 26 + const handleCreate = async () => { 27 + if (!newColumnName.trim()) return; 28 + try { 29 + await createColumn({ 30 + projectId, 31 + data: { name: newColumnName.trim() }, 32 + }); 33 + setNewColumnName(""); 34 + toast.success("Column created"); 35 + } catch (error) { 36 + toast.error( 37 + error instanceof Error ? error.message : "Failed to create column", 38 + ); 39 + } 40 + }; 41 + 42 + const handleRename = async (id: string, name: string) => { 43 + try { 44 + await updateColumn({ id, projectId, data: { name } }); 45 + } catch (error) { 46 + toast.error( 47 + error instanceof Error ? error.message : "Failed to update column", 48 + ); 49 + } 50 + }; 51 + 52 + const handleToggleFinal = async (id: string, isFinal: boolean) => { 53 + try { 54 + await updateColumn({ id, projectId, data: { isFinal } }); 55 + toast.success( 56 + isFinal ? "Column marked as final" : "Column unmarked as final", 57 + ); 58 + } catch (error) { 59 + toast.error( 60 + error instanceof Error ? error.message : "Failed to update column", 61 + ); 62 + } 63 + }; 64 + 65 + const handleDelete = async (id: string) => { 66 + try { 67 + await deleteColumn({ id, projectId }); 68 + toast.success("Column deleted"); 69 + } catch (error) { 70 + toast.error( 71 + error instanceof Error ? error.message : "Failed to delete column", 72 + ); 73 + } 74 + }; 75 + 76 + const handleDragStart = (index: number) => { 77 + setDraggedIndex(index); 78 + }; 79 + 80 + const handleDragOver = (e: React.DragEvent, index: number) => { 81 + e.preventDefault(); 82 + if (draggedIndex === null || draggedIndex === index || !columns) return; 83 + 84 + const reordered = [...columns]; 85 + const [removed] = reordered.splice(draggedIndex, 1); 86 + reordered.splice(index, 0, removed); 87 + 88 + const updates = reordered.map((col, i) => ({ id: col.id, position: i })); 89 + reorderColumns({ projectId, columns: updates }); 90 + setDraggedIndex(index); 91 + }; 92 + 93 + const handleDragEnd = () => { 94 + setDraggedIndex(null); 95 + }; 96 + 97 + if (isLoading) { 98 + return ( 99 + <div className="text-sm text-muted-foreground">Loading columns...</div> 100 + ); 101 + } 102 + 103 + return ( 104 + <div className="space-y-3"> 105 + <div className="space-y-1"> 106 + {columns?.map((col, index) => ( 107 + // biome-ignore lint/a11y/useSemanticElements: false positive for role="listitem" 108 + <div 109 + key={col.id} 110 + role="listitem" 111 + draggable 112 + onDragStart={() => handleDragStart(index)} 113 + onDragOver={(e) => handleDragOver(e, index)} 114 + onDragEnd={handleDragEnd} 115 + className="flex items-center gap-2 p-2 border border-border rounded-md bg-sidebar hover:bg-sidebar-accent/50 transition-colors" 116 + > 117 + <GripVertical className="w-4 h-4 text-muted-foreground cursor-grab shrink-0" /> 118 + <Input 119 + defaultValue={col.name} 120 + className="h-8 text-sm flex-1" 121 + onBlur={(e) => { 122 + if (e.target.value !== col.name) { 123 + handleRename(col.id, e.target.value); 124 + } 125 + }} 126 + /> 127 + <div className="flex items-center gap-1.5 shrink-0"> 128 + <div 129 + className="flex items-center gap-2" 130 + title="Treat this as a done column" 131 + > 132 + {col.isFinal ? ( 133 + <CheckCircle2 className="w-3.5 h-3.5 text-muted-foreground" /> 134 + ) : ( 135 + <Circle className="w-3.5 h-3.5 text-muted-foreground" /> 136 + )} 137 + <span className="text-xs text-muted-foreground whitespace-nowrap"> 138 + Done column 139 + </span> 140 + <Switch 141 + checked={col.isFinal} 142 + onCheckedChange={(checked) => 143 + handleToggleFinal(col.id, checked) 144 + } 145 + aria-label={`Mark ${col.name} as done column`} 146 + className="scale-75" 147 + /> 148 + <span className="text-[11px] text-muted-foreground w-8"> 149 + {col.isFinal ? "On" : "Off"} 150 + </span> 151 + </div> 152 + <Button 153 + variant="ghost" 154 + size="sm" 155 + className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive" 156 + onClick={() => handleDelete(col.id)} 157 + > 158 + <Trash2 className="w-3.5 h-3.5" /> 159 + </Button> 160 + </div> 161 + </div> 162 + ))} 163 + </div> 164 + 165 + <div className="flex items-center gap-2"> 166 + <Input 167 + placeholder="New column name..." 168 + value={newColumnName} 169 + onChange={(e) => setNewColumnName(e.target.value)} 170 + className="h-8 text-sm flex-1" 171 + onKeyDown={(e) => { 172 + if (e.key === "Enter") handleCreate(); 173 + }} 174 + /> 175 + <Button 176 + variant="outline" 177 + size="sm" 178 + onClick={handleCreate} 179 + disabled={!newColumnName.trim()} 180 + className="h-8 gap-1" 181 + > 182 + <Plus className="w-3.5 h-3.5" /> 183 + Add 184 + </Button> 185 + </div> 186 + </div> 187 + ); 188 + }
+105
apps/web/src/components/project/workflow-editor.tsx
··· 1 + import { toast } from "sonner"; 2 + import { 3 + Select, 4 + SelectContent, 5 + SelectItem, 6 + SelectTrigger, 7 + SelectValue, 8 + } from "@/components/ui/select"; 9 + import { useUpsertWorkflowRule } from "@/hooks/mutations/workflow-rule/use-upsert-workflow-rule"; 10 + import { useGetColumns } from "@/hooks/queries/column/use-get-columns"; 11 + import { useGetWorkflowRules } from "@/hooks/queries/workflow-rule/use-get-workflow-rules"; 12 + 13 + const GITHUB_EVENTS = [ 14 + { eventType: "branch_push", label: "Branch Push" }, 15 + { eventType: "pr_opened", label: "PR Opened" }, 16 + { eventType: "pr_merged", label: "PR Merged" }, 17 + { eventType: "issue_opened", label: "Issue Opened" }, 18 + { eventType: "issue_closed", label: "Issue Closed" }, 19 + ]; 20 + 21 + type WorkflowEditorProps = { 22 + projectId: string; 23 + }; 24 + 25 + export default function WorkflowEditor({ projectId }: WorkflowEditorProps) { 26 + const { data: columns, isLoading: columnsLoading } = useGetColumns(projectId); 27 + const { data: rules, isLoading: rulesLoading } = 28 + useGetWorkflowRules(projectId); 29 + const { mutateAsync: upsertRule } = useUpsertWorkflowRule(); 30 + 31 + const handleChange = async (eventType: string, columnId: string) => { 32 + try { 33 + await upsertRule({ 34 + projectId, 35 + data: { 36 + integrationType: "github", 37 + eventType, 38 + columnId, 39 + }, 40 + }); 41 + toast.success("Workflow rule updated"); 42 + } catch (error) { 43 + toast.error( 44 + error instanceof Error ? error.message : "Failed to update rule", 45 + ); 46 + } 47 + }; 48 + 49 + if (columnsLoading || rulesLoading) { 50 + return <div className="text-sm text-muted-foreground">Loading...</div>; 51 + } 52 + 53 + if (!columns || columns.length === 0) { 54 + return ( 55 + <div className="text-sm text-muted-foreground"> 56 + Create columns first to configure automation rules. 57 + </div> 58 + ); 59 + } 60 + 61 + const githubRules = rules?.filter((r) => r.integrationType === "github"); 62 + 63 + return ( 64 + <div className="space-y-4"> 65 + <div className="space-y-1"> 66 + <h3 className="text-sm font-medium">GitHub</h3> 67 + <p className="text-xs text-muted-foreground"> 68 + When a GitHub event occurs, move the linked task to a column. 69 + </p> 70 + </div> 71 + 72 + <div className="space-y-2"> 73 + {GITHUB_EVENTS.map((event) => { 74 + const currentRule = githubRules?.find( 75 + (r) => r.eventType === event.eventType, 76 + ); 77 + 78 + return ( 79 + <div 80 + key={event.eventType} 81 + className="flex items-center justify-between gap-4 p-3 border border-border rounded-md bg-sidebar" 82 + > 83 + <span className="text-sm">{event.label}</span> 84 + <Select 85 + value={currentRule?.columnId ?? ""} 86 + onValueChange={(value) => handleChange(event.eventType, value)} 87 + > 88 + <SelectTrigger className="w-48 h-8 text-sm"> 89 + <SelectValue placeholder="Select column..." /> 90 + </SelectTrigger> 91 + <SelectContent> 92 + {columns.map((col) => ( 93 + <SelectItem key={col.id} value={col.id}> 94 + {col.name} 95 + </SelectItem> 96 + ))} 97 + </SelectContent> 98 + </Select> 99 + </div> 100 + ); 101 + })} 102 + </div> 103 + </div> 104 + ); 105 + }
+8 -8
apps/web/src/components/task/task-status-popover.tsx
··· 11 11 import { useUpdateTaskStatus } from "@/hooks/mutations/task/use-update-task-status"; 12 12 import { useNumberedShortcuts } from "@/hooks/use-numbered-shortcuts"; 13 13 import { getColumnIcon } from "@/lib/column"; 14 + import useProjectStore from "@/store/project"; 14 15 import type Task from "@/types/task"; 15 16 16 17 type TaskStatusPopoverProps = { ··· 18 19 children: React.ReactNode; 19 20 }; 20 21 21 - const statusOptions = [ 22 - { value: "to-do", label: "To Do" }, 23 - { value: "in-progress", label: "In Progress" }, 24 - { value: "in-review", label: "In Review" }, 25 - { value: "done", label: "Done" }, 26 - ]; 27 - 28 22 export default function TaskStatusPopover({ 29 23 task, 30 24 children, 31 25 }: TaskStatusPopoverProps) { 32 26 const [open, setOpen] = useState(false); 27 + const { project } = useProjectStore(); 28 + const statusOptions = 29 + project?.columns?.map((col) => ({ 30 + value: col.id, 31 + label: col.name, 32 + })) ?? []; 33 33 const { mutateAsync: updateTaskStatus } = useUpdateTaskStatus(); 34 34 35 35 const handleStatusChange = useCallback( ··· 56 56 statusOptions.map((status) => ({ 57 57 onSelect: () => handleStatusChange(status.value), 58 58 })), 59 - [handleStatusChange], 59 + [handleStatusChange, statusOptions], 60 60 ); 61 61 62 62 useNumberedShortcuts(open, shortcutOptions);
+20
apps/web/src/fetchers/column/create-column.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function createColumn( 4 + projectId: string, 5 + data: { name: string; icon?: string; color?: string; isFinal?: boolean }, 6 + ) { 7 + const response = await client.column[":projectId"].$post({ 8 + param: { projectId }, 9 + json: data, 10 + }); 11 + 12 + if (!response.ok) { 13 + const error = await response.text(); 14 + throw new Error(error); 15 + } 16 + 17 + return response.json(); 18 + } 19 + 20 + export default createColumn;
+16
apps/web/src/fetchers/column/delete-column.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function deleteColumn(id: string) { 4 + const response = await client.column[":id"].$delete({ 5 + param: { id }, 6 + }); 7 + 8 + if (!response.ok) { 9 + const error = await response.text(); 10 + throw new Error(error); 11 + } 12 + 13 + return response.json(); 14 + } 15 + 16 + export default deleteColumn;
+16
apps/web/src/fetchers/column/get-columns.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function getColumns(projectId: string) { 4 + const response = await client.column[":projectId"].$get({ 5 + param: { projectId }, 6 + }); 7 + 8 + if (!response.ok) { 9 + const error = await response.text(); 10 + throw new Error(error); 11 + } 12 + 13 + return response.json(); 14 + } 15 + 16 + export default getColumns;
+20
apps/web/src/fetchers/column/reorder-columns.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function reorderColumns( 4 + projectId: string, 5 + columns: Array<{ id: string; position: number }>, 6 + ) { 7 + const response = await client.column.reorder[":projectId"].$put({ 8 + param: { projectId }, 9 + json: { columns }, 10 + }); 11 + 12 + if (!response.ok) { 13 + const error = await response.text(); 14 + throw new Error(error); 15 + } 16 + 17 + return response.json(); 18 + } 19 + 20 + export default reorderColumns;
+25
apps/web/src/fetchers/column/update-column.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function updateColumn( 4 + id: string, 5 + data: { 6 + name?: string; 7 + icon?: string | null; 8 + color?: string | null; 9 + isFinal?: boolean; 10 + }, 11 + ) { 12 + const response = await client.column[":id"].$put({ 13 + param: { id }, 14 + json: data, 15 + }); 16 + 17 + if (!response.ok) { 18 + const error = await response.text(); 19 + throw new Error(error); 20 + } 21 + 22 + return response.json(); 23 + } 24 + 25 + export default updateColumn;
+16
apps/web/src/fetchers/workflow-rule/delete-workflow-rule.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function deleteWorkflowRule(id: string) { 4 + const response = await client["workflow-rule"][":id"].$delete({ 5 + param: { id }, 6 + }); 7 + 8 + if (!response.ok) { 9 + const error = await response.text(); 10 + throw new Error(error); 11 + } 12 + 13 + return response.json(); 14 + } 15 + 16 + export default deleteWorkflowRule;
+16
apps/web/src/fetchers/workflow-rule/get-workflow-rules.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function getWorkflowRules(projectId: string) { 4 + const response = await client["workflow-rule"][":projectId"].$get({ 5 + param: { projectId }, 6 + }); 7 + 8 + if (!response.ok) { 9 + const error = await response.text(); 10 + throw new Error(error); 11 + } 12 + 13 + return response.json(); 14 + } 15 + 16 + export default getWorkflowRules;
+20
apps/web/src/fetchers/workflow-rule/upsert-workflow-rule.ts
··· 1 + import { client } from "@kaneo/libs"; 2 + 3 + async function upsertWorkflowRule( 4 + projectId: string, 5 + data: { integrationType: string; eventType: string; columnId: string }, 6 + ) { 7 + const response = await client["workflow-rule"][":projectId"].$put({ 8 + param: { projectId }, 9 + json: data, 10 + }); 11 + 12 + if (!response.ok) { 13 + const error = await response.text(); 14 + throw new Error(error); 15 + } 16 + 17 + return response.json(); 18 + } 19 + 20 + export default upsertWorkflowRule;
+24
apps/web/src/hooks/mutations/column/use-create-column.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import createColumn from "@/fetchers/column/create-column"; 3 + 4 + export function useCreateColumn() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ 9 + projectId, 10 + data, 11 + }: { 12 + projectId: string; 13 + data: { name: string; icon?: string; color?: string; isFinal?: boolean }; 14 + }) => createColumn(projectId, data), 15 + onSuccess: (_, variables) => { 16 + queryClient.invalidateQueries({ 17 + queryKey: ["columns", variables.projectId], 18 + }); 19 + queryClient.invalidateQueries({ 20 + queryKey: ["tasks"], 21 + }); 22 + }, 23 + }); 24 + }
+18
apps/web/src/hooks/mutations/column/use-delete-column.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import deleteColumn from "@/fetchers/column/delete-column"; 3 + 4 + export function useDeleteColumn() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ id }: { id: string; projectId: string }) => deleteColumn(id), 9 + onSuccess: (_, variables) => { 10 + queryClient.invalidateQueries({ 11 + queryKey: ["columns", variables.projectId], 12 + }); 13 + queryClient.invalidateQueries({ 14 + queryKey: ["tasks"], 15 + }); 16 + }, 17 + }); 18 + }
+24
apps/web/src/hooks/mutations/column/use-reorder-columns.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import reorderColumns from "@/fetchers/column/reorder-columns"; 3 + 4 + export function useReorderColumns() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ 9 + projectId, 10 + columns, 11 + }: { 12 + projectId: string; 13 + columns: Array<{ id: string; position: number }>; 14 + }) => reorderColumns(projectId, columns), 15 + onSuccess: (_, variables) => { 16 + queryClient.invalidateQueries({ 17 + queryKey: ["columns", variables.projectId], 18 + }); 19 + queryClient.invalidateQueries({ 20 + queryKey: ["tasks"], 21 + }); 22 + }, 23 + }); 24 + }
+30
apps/web/src/hooks/mutations/column/use-update-column.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import updateColumn from "@/fetchers/column/update-column"; 3 + 4 + export function useUpdateColumn() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ 9 + id, 10 + data, 11 + }: { 12 + id: string; 13 + projectId: string; 14 + data: { 15 + name?: string; 16 + icon?: string | null; 17 + color?: string | null; 18 + isFinal?: boolean; 19 + }; 20 + }) => updateColumn(id, data), 21 + onSuccess: (_, variables) => { 22 + queryClient.invalidateQueries({ 23 + queryKey: ["columns", variables.projectId], 24 + }); 25 + queryClient.invalidateQueries({ 26 + queryKey: ["tasks"], 27 + }); 28 + }, 29 + }); 30 + }
+16
apps/web/src/hooks/mutations/workflow-rule/use-delete-workflow-rule.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import deleteWorkflowRule from "@/fetchers/workflow-rule/delete-workflow-rule"; 3 + 4 + export function useDeleteWorkflowRule() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ id }: { id: string; projectId: string }) => 9 + deleteWorkflowRule(id), 10 + onSuccess: (_, variables) => { 11 + queryClient.invalidateQueries({ 12 + queryKey: ["workflow-rules", variables.projectId], 13 + }); 14 + }, 15 + }); 16 + }
+21
apps/web/src/hooks/mutations/workflow-rule/use-upsert-workflow-rule.ts
··· 1 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 + import upsertWorkflowRule from "@/fetchers/workflow-rule/upsert-workflow-rule"; 3 + 4 + export function useUpsertWorkflowRule() { 5 + const queryClient = useQueryClient(); 6 + 7 + return useMutation({ 8 + mutationFn: ({ 9 + projectId, 10 + data, 11 + }: { 12 + projectId: string; 13 + data: { integrationType: string; eventType: string; columnId: string }; 14 + }) => upsertWorkflowRule(projectId, data), 15 + onSuccess: (_, variables) => { 16 + queryClient.invalidateQueries({ 17 + queryKey: ["workflow-rules", variables.projectId], 18 + }); 19 + }, 20 + }); 21 + }
+10
apps/web/src/hooks/queries/column/use-get-columns.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import getColumns from "@/fetchers/column/get-columns"; 3 + 4 + export function useGetColumns(projectId: string) { 5 + return useQuery({ 6 + queryKey: ["columns", projectId], 7 + queryFn: () => getColumns(projectId), 8 + enabled: !!projectId, 9 + }); 10 + }
+10
apps/web/src/hooks/queries/workflow-rule/use-get-workflow-rules.ts
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import getWorkflowRules from "@/fetchers/workflow-rule/get-workflow-rules"; 3 + 4 + export function useGetWorkflowRules(projectId: string) { 5 + return useQuery({ 6 + queryKey: ["workflow-rules", projectId], 7 + queryFn: () => getWorkflowRules(projectId), 8 + enabled: !!projectId, 9 + }); 10 + }
+25
apps/web/src/routeTree.gen.ts
··· 39 39 import { Route as LayoutAuthenticatedDashboardSettingsAccountPreferencesRouteImport } from './routes/_layout/_authenticated/dashboard/settings/account/preferences' 40 40 import { Route as LayoutAuthenticatedDashboardSettingsAccountInformationRouteImport } from './routes/_layout/_authenticated/dashboard/settings/account/information' 41 41 import { Route as LayoutAuthenticatedDashboardSettingsAccountDeveloperRouteImport } from './routes/_layout/_authenticated/dashboard/settings/account/developer' 42 + import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow' 42 43 import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/visibility' 43 44 import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/integrations' 44 45 import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/general' ··· 215 216 path: '/developer', 216 217 getParentRoute: () => LayoutAuthenticatedDashboardSettingsAccountRoute, 217 218 } as any) 219 + const LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute = 220 + LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRouteImport.update( 221 + { 222 + id: '/$projectId/workflow', 223 + path: '/$projectId/workflow', 224 + getParentRoute: () => LayoutAuthenticatedDashboardSettingsProjectsRoute, 225 + } as any, 226 + ) 218 227 const LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute = 219 228 LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRouteImport.update( 220 229 { ··· 308 317 '/dashboard/settings/projects/$projectId/general': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRoute 309 318 '/dashboard/settings/projects/$projectId/integrations': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute 310 319 '/dashboard/settings/projects/$projectId/visibility': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute 320 + '/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 311 321 '/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 312 322 '/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 313 323 '/dashboard/workspace/$workspaceId/project/$projectId/': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute ··· 343 353 '/dashboard/settings/projects/$projectId/general': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRoute 344 354 '/dashboard/settings/projects/$projectId/integrations': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute 345 355 '/dashboard/settings/projects/$projectId/visibility': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute 356 + '/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 346 357 '/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 347 358 '/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 348 359 '/dashboard/workspace/$workspaceId/project/$projectId': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute ··· 383 394 '/_layout/_authenticated/dashboard/settings/projects/$projectId/general': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRoute 384 395 '/_layout/_authenticated/dashboard/settings/projects/$projectId/integrations': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute 385 396 '/_layout/_authenticated/dashboard/settings/projects/$projectId/visibility': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute 397 + '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 386 398 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 387 399 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 388 400 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute ··· 422 434 | '/dashboard/settings/projects/$projectId/general' 423 435 | '/dashboard/settings/projects/$projectId/integrations' 424 436 | '/dashboard/settings/projects/$projectId/visibility' 437 + | '/dashboard/settings/projects/$projectId/workflow' 425 438 | '/dashboard/workspace/$workspaceId/project/$projectId/backlog' 426 439 | '/dashboard/workspace/$workspaceId/project/$projectId/board' 427 440 | '/dashboard/workspace/$workspaceId/project/$projectId/' ··· 457 470 | '/dashboard/settings/projects/$projectId/general' 458 471 | '/dashboard/settings/projects/$projectId/integrations' 459 472 | '/dashboard/settings/projects/$projectId/visibility' 473 + | '/dashboard/settings/projects/$projectId/workflow' 460 474 | '/dashboard/workspace/$workspaceId/project/$projectId/backlog' 461 475 | '/dashboard/workspace/$workspaceId/project/$projectId/board' 462 476 | '/dashboard/workspace/$workspaceId/project/$projectId' ··· 496 510 | '/_layout/_authenticated/dashboard/settings/projects/$projectId/general' 497 511 | '/_layout/_authenticated/dashboard/settings/projects/$projectId/integrations' 498 512 | '/_layout/_authenticated/dashboard/settings/projects/$projectId/visibility' 513 + | '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow' 499 514 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog' 500 515 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board' 501 516 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/' ··· 723 738 preLoaderRoute: typeof LayoutAuthenticatedDashboardSettingsAccountDeveloperRouteImport 724 739 parentRoute: typeof LayoutAuthenticatedDashboardSettingsAccountRoute 725 740 } 741 + '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow': { 742 + id: '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow' 743 + path: '/$projectId/workflow' 744 + fullPath: '/dashboard/settings/projects/$projectId/workflow' 745 + preLoaderRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRouteImport 746 + parentRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsRoute 747 + } 726 748 '/_layout/_authenticated/dashboard/settings/projects/$projectId/visibility': { 727 749 id: '/_layout/_authenticated/dashboard/settings/projects/$projectId/visibility' 728 750 path: '/$projectId/visibility' ··· 800 822 LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRoute 801 823 LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute 802 824 LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute 825 + LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute: typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 803 826 } 804 827 805 828 const LayoutAuthenticatedDashboardSettingsProjectsRouteChildren: LayoutAuthenticatedDashboardSettingsProjectsRouteChildren = ··· 810 833 LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRoute, 811 834 LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute: 812 835 LayoutAuthenticatedDashboardSettingsProjectsProjectIdVisibilityRoute, 836 + LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute: 837 + LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute, 813 838 } 814 839 815 840 const LayoutAuthenticatedDashboardSettingsProjectsRouteWithChildren =
+25 -1
apps/web/src/routes/_layout/_authenticated/dashboard/settings/projects.tsx
··· 3 3 Link, 4 4 Outlet, 5 5 useLocation, 6 + useNavigate, 6 7 } from "@tanstack/react-router"; 7 - import { ChevronRight, Eye, Plug, Settings } from "lucide-react"; 8 + import { ChevronRight, Eye, GitBranch, Plug, Settings } from "lucide-react"; 9 + import { useEffect } from "react"; 8 10 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; 9 11 import { 10 12 Collapsible, ··· 35 37 title: "Integrations", 36 38 icon: Plug, 37 39 }, 40 + { 41 + title: "Workflow", 42 + icon: GitBranch, 43 + }, 38 44 ]; 39 45 40 46 function RouteComponent() { 41 47 const { workspace, role } = useWorkspacePermission(); 42 48 const location = useLocation(); 49 + const navigate = useNavigate(); 43 50 const { data: projects } = useGetProjects({ 44 51 workspaceId: workspace?.id || "", 45 52 }); 46 53 54 + useEffect(() => { 55 + const isProjectsRoot = 56 + location.pathname === "/dashboard/settings/projects" || 57 + location.pathname === "/dashboard/settings/projects/"; 58 + 59 + if (!isProjectsRoot || !projects || projects.length === 0) { 60 + return; 61 + } 62 + 63 + void navigate({ 64 + to: "/dashboard/settings/projects/$projectId/general", 65 + params: { projectId: projects[0].id }, 66 + replace: true, 67 + }); 68 + }, [location.pathname, navigate, projects]); 69 + 47 70 return ( 48 71 <div className="flex gap-6 h-full"> 49 72 <div className="w-64 flex-shrink-0"> ··· 95 118 General: `/dashboard/settings/projects/${project.id}/general`, 96 119 Visibility: `/dashboard/settings/projects/${project.id}/visibility`, 97 120 Integrations: `/dashboard/settings/projects/${project.id}/integrations`, 121 + Workflow: `/dashboard/settings/projects/${project.id}/workflow`, 98 122 }; 99 123 100 124 const toUrl =
+50
apps/web/src/routes/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow.tsx
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import PageTitle from "@/components/page-title"; 3 + import ColumnEditor from "@/components/project/column-editor"; 4 + import WorkflowEditor from "@/components/project/workflow-editor"; 5 + 6 + export const Route = createFileRoute( 7 + "/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow", 8 + )({ 9 + component: RouteComponent, 10 + }); 11 + 12 + function RouteComponent() { 13 + const { projectId } = Route.useParams(); 14 + 15 + return ( 16 + <> 17 + <PageTitle title="Workflow Settings" /> 18 + <div className="max-w-4xl mx-auto space-y-8"> 19 + <div className="space-y-2"> 20 + <h1 className="text-2xl font-semibold">Workflow</h1> 21 + <p className="text-muted-foreground"> 22 + Configure board columns and automation rules for this project. 23 + </p> 24 + </div> 25 + 26 + <div className="space-y-6"> 27 + <div className="space-y-1"> 28 + <h2 className="text-md font-medium">Columns</h2> 29 + <p className="text-xs text-muted-foreground"> 30 + Manage the columns that appear on your board. Drag to reorder. 31 + Turn on "Done column" for stages that represent completed work. 32 + </p> 33 + </div> 34 + <ColumnEditor projectId={projectId} /> 35 + </div> 36 + 37 + <div className="space-y-6"> 38 + <div className="space-y-1"> 39 + <h2 className="text-md font-medium">Automation Rules</h2> 40 + <p className="text-xs text-muted-foreground"> 41 + Map integration events to columns. When an event occurs, the 42 + linked task moves to the specified column. 43 + </p> 44 + </div> 45 + <WorkflowEditor projectId={projectId} /> 46 + </div> 47 + </div> 48 + </> 49 + ); 50 + }