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: gantt view and task start date (#1083)

- API/schema/migration for task start date
- Gantt route, backlog/board nav, import/export, task UI

Co-authored-by: Andrej <44305048+andrejsshell@users.noreply.github.com>

authored by

Tin Sever
Andrej
and committed by
GitHub
e1cc1ea3 b2c07f56

+3280 -24
+1
apps/api/drizzle/0017_white_omega_flight.sql
··· 1 + ALTER TABLE "task" ADD COLUMN IF NOT EXISTS "start_date" timestamp;
+2506
apps/api/drizzle/meta/0017_snapshot.json
··· 1 + { 2 + "id": "8d81a01e-bf63-4463-8a66-4d4a2bbb36a0", 3 + "prevId": "2f1cbcfc-998f-474d-b027-b538ced42904", 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 + "config_id": { 229 + "name": "config_id", 230 + "type": "text", 231 + "primaryKey": false, 232 + "notNull": true, 233 + "default": "'default'" 234 + }, 235 + "name": { 236 + "name": "name", 237 + "type": "text", 238 + "primaryKey": false, 239 + "notNull": false 240 + }, 241 + "start": { 242 + "name": "start", 243 + "type": "text", 244 + "primaryKey": false, 245 + "notNull": false 246 + }, 247 + "reference_id": { 248 + "name": "reference_id", 249 + "type": "text", 250 + "primaryKey": false, 251 + "notNull": true 252 + }, 253 + "prefix": { 254 + "name": "prefix", 255 + "type": "text", 256 + "primaryKey": false, 257 + "notNull": false 258 + }, 259 + "key": { 260 + "name": "key", 261 + "type": "text", 262 + "primaryKey": false, 263 + "notNull": true 264 + }, 265 + "user_id": { 266 + "name": "user_id", 267 + "type": "text", 268 + "primaryKey": false, 269 + "notNull": false 270 + }, 271 + "refill_interval": { 272 + "name": "refill_interval", 273 + "type": "integer", 274 + "primaryKey": false, 275 + "notNull": false 276 + }, 277 + "refill_amount": { 278 + "name": "refill_amount", 279 + "type": "integer", 280 + "primaryKey": false, 281 + "notNull": false 282 + }, 283 + "last_refill_at": { 284 + "name": "last_refill_at", 285 + "type": "timestamp", 286 + "primaryKey": false, 287 + "notNull": false 288 + }, 289 + "enabled": { 290 + "name": "enabled", 291 + "type": "boolean", 292 + "primaryKey": false, 293 + "notNull": false, 294 + "default": true 295 + }, 296 + "rate_limit_enabled": { 297 + "name": "rate_limit_enabled", 298 + "type": "boolean", 299 + "primaryKey": false, 300 + "notNull": false, 301 + "default": true 302 + }, 303 + "rate_limit_time_window": { 304 + "name": "rate_limit_time_window", 305 + "type": "integer", 306 + "primaryKey": false, 307 + "notNull": false, 308 + "default": 86400000 309 + }, 310 + "rate_limit_max": { 311 + "name": "rate_limit_max", 312 + "type": "integer", 313 + "primaryKey": false, 314 + "notNull": false, 315 + "default": 10 316 + }, 317 + "request_count": { 318 + "name": "request_count", 319 + "type": "integer", 320 + "primaryKey": false, 321 + "notNull": false, 322 + "default": 0 323 + }, 324 + "remaining": { 325 + "name": "remaining", 326 + "type": "integer", 327 + "primaryKey": false, 328 + "notNull": false 329 + }, 330 + "last_request": { 331 + "name": "last_request", 332 + "type": "timestamp", 333 + "primaryKey": false, 334 + "notNull": false 335 + }, 336 + "expires_at": { 337 + "name": "expires_at", 338 + "type": "timestamp", 339 + "primaryKey": false, 340 + "notNull": false 341 + }, 342 + "created_at": { 343 + "name": "created_at", 344 + "type": "timestamp", 345 + "primaryKey": false, 346 + "notNull": true 347 + }, 348 + "updated_at": { 349 + "name": "updated_at", 350 + "type": "timestamp", 351 + "primaryKey": false, 352 + "notNull": true 353 + }, 354 + "permissions": { 355 + "name": "permissions", 356 + "type": "text", 357 + "primaryKey": false, 358 + "notNull": false 359 + }, 360 + "metadata": { 361 + "name": "metadata", 362 + "type": "text", 363 + "primaryKey": false, 364 + "notNull": false 365 + } 366 + }, 367 + "indexes": { 368 + "apikey_configId_idx": { 369 + "name": "apikey_configId_idx", 370 + "columns": [ 371 + { 372 + "expression": "config_id", 373 + "isExpression": false, 374 + "asc": true, 375 + "nulls": "last" 376 + } 377 + ], 378 + "isUnique": false, 379 + "concurrently": false, 380 + "method": "btree", 381 + "with": {} 382 + }, 383 + "apikey_key_idx": { 384 + "name": "apikey_key_idx", 385 + "columns": [ 386 + { 387 + "expression": "key", 388 + "isExpression": false, 389 + "asc": true, 390 + "nulls": "last" 391 + } 392 + ], 393 + "isUnique": false, 394 + "concurrently": false, 395 + "method": "btree", 396 + "with": {} 397 + }, 398 + "apikey_referenceId_idx": { 399 + "name": "apikey_referenceId_idx", 400 + "columns": [ 401 + { 402 + "expression": "reference_id", 403 + "isExpression": false, 404 + "asc": true, 405 + "nulls": "last" 406 + } 407 + ], 408 + "isUnique": false, 409 + "concurrently": false, 410 + "method": "btree", 411 + "with": {} 412 + }, 413 + "apikey_userId_idx": { 414 + "name": "apikey_userId_idx", 415 + "columns": [ 416 + { 417 + "expression": "user_id", 418 + "isExpression": false, 419 + "asc": true, 420 + "nulls": "last" 421 + } 422 + ], 423 + "isUnique": false, 424 + "concurrently": false, 425 + "method": "btree", 426 + "with": {} 427 + } 428 + }, 429 + "foreignKeys": { 430 + "apikey_reference_id_user_id_fk": { 431 + "name": "apikey_reference_id_user_id_fk", 432 + "tableFrom": "apikey", 433 + "tableTo": "user", 434 + "columnsFrom": ["reference_id"], 435 + "columnsTo": ["id"], 436 + "onDelete": "cascade", 437 + "onUpdate": "no action" 438 + }, 439 + "apikey_user_id_user_id_fk": { 440 + "name": "apikey_user_id_user_id_fk", 441 + "tableFrom": "apikey", 442 + "tableTo": "user", 443 + "columnsFrom": ["user_id"], 444 + "columnsTo": ["id"], 445 + "onDelete": "cascade", 446 + "onUpdate": "no action" 447 + } 448 + }, 449 + "compositePrimaryKeys": {}, 450 + "uniqueConstraints": {}, 451 + "policies": {}, 452 + "checkConstraints": {}, 453 + "isRLSEnabled": false 454 + }, 455 + "public.asset": { 456 + "name": "asset", 457 + "schema": "", 458 + "columns": { 459 + "id": { 460 + "name": "id", 461 + "type": "text", 462 + "primaryKey": true, 463 + "notNull": true 464 + }, 465 + "workspace_id": { 466 + "name": "workspace_id", 467 + "type": "text", 468 + "primaryKey": false, 469 + "notNull": true 470 + }, 471 + "project_id": { 472 + "name": "project_id", 473 + "type": "text", 474 + "primaryKey": false, 475 + "notNull": true 476 + }, 477 + "task_id": { 478 + "name": "task_id", 479 + "type": "text", 480 + "primaryKey": false, 481 + "notNull": false 482 + }, 483 + "activity_id": { 484 + "name": "activity_id", 485 + "type": "text", 486 + "primaryKey": false, 487 + "notNull": false 488 + }, 489 + "object_key": { 490 + "name": "object_key", 491 + "type": "text", 492 + "primaryKey": false, 493 + "notNull": true 494 + }, 495 + "filename": { 496 + "name": "filename", 497 + "type": "text", 498 + "primaryKey": false, 499 + "notNull": true 500 + }, 501 + "mime_type": { 502 + "name": "mime_type", 503 + "type": "text", 504 + "primaryKey": false, 505 + "notNull": true 506 + }, 507 + "size": { 508 + "name": "size", 509 + "type": "integer", 510 + "primaryKey": false, 511 + "notNull": true 512 + }, 513 + "kind": { 514 + "name": "kind", 515 + "type": "text", 516 + "primaryKey": false, 517 + "notNull": true, 518 + "default": "'image'" 519 + }, 520 + "surface": { 521 + "name": "surface", 522 + "type": "text", 523 + "primaryKey": false, 524 + "notNull": true, 525 + "default": "'description'" 526 + }, 527 + "created_by": { 528 + "name": "created_by", 529 + "type": "text", 530 + "primaryKey": false, 531 + "notNull": false 532 + }, 533 + "created_at": { 534 + "name": "created_at", 535 + "type": "timestamp", 536 + "primaryKey": false, 537 + "notNull": true, 538 + "default": "now()" 539 + } 540 + }, 541 + "indexes": { 542 + "asset_workspaceId_idx": { 543 + "name": "asset_workspaceId_idx", 544 + "columns": [ 545 + { 546 + "expression": "workspace_id", 547 + "isExpression": false, 548 + "asc": true, 549 + "nulls": "last" 550 + } 551 + ], 552 + "isUnique": false, 553 + "concurrently": false, 554 + "method": "btree", 555 + "with": {} 556 + }, 557 + "asset_projectId_idx": { 558 + "name": "asset_projectId_idx", 559 + "columns": [ 560 + { 561 + "expression": "project_id", 562 + "isExpression": false, 563 + "asc": true, 564 + "nulls": "last" 565 + } 566 + ], 567 + "isUnique": false, 568 + "concurrently": false, 569 + "method": "btree", 570 + "with": {} 571 + }, 572 + "asset_taskId_idx": { 573 + "name": "asset_taskId_idx", 574 + "columns": [ 575 + { 576 + "expression": "task_id", 577 + "isExpression": false, 578 + "asc": true, 579 + "nulls": "last" 580 + } 581 + ], 582 + "isUnique": false, 583 + "concurrently": false, 584 + "method": "btree", 585 + "with": {} 586 + }, 587 + "asset_activityId_idx": { 588 + "name": "asset_activityId_idx", 589 + "columns": [ 590 + { 591 + "expression": "activity_id", 592 + "isExpression": false, 593 + "asc": true, 594 + "nulls": "last" 595 + } 596 + ], 597 + "isUnique": false, 598 + "concurrently": false, 599 + "method": "btree", 600 + "with": {} 601 + } 602 + }, 603 + "foreignKeys": { 604 + "asset_workspace_id_workspace_id_fk": { 605 + "name": "asset_workspace_id_workspace_id_fk", 606 + "tableFrom": "asset", 607 + "tableTo": "workspace", 608 + "columnsFrom": ["workspace_id"], 609 + "columnsTo": ["id"], 610 + "onDelete": "cascade", 611 + "onUpdate": "cascade" 612 + }, 613 + "asset_project_id_project_id_fk": { 614 + "name": "asset_project_id_project_id_fk", 615 + "tableFrom": "asset", 616 + "tableTo": "project", 617 + "columnsFrom": ["project_id"], 618 + "columnsTo": ["id"], 619 + "onDelete": "cascade", 620 + "onUpdate": "cascade" 621 + }, 622 + "asset_task_id_task_id_fk": { 623 + "name": "asset_task_id_task_id_fk", 624 + "tableFrom": "asset", 625 + "tableTo": "task", 626 + "columnsFrom": ["task_id"], 627 + "columnsTo": ["id"], 628 + "onDelete": "cascade", 629 + "onUpdate": "cascade" 630 + }, 631 + "asset_activity_id_activity_id_fk": { 632 + "name": "asset_activity_id_activity_id_fk", 633 + "tableFrom": "asset", 634 + "tableTo": "activity", 635 + "columnsFrom": ["activity_id"], 636 + "columnsTo": ["id"], 637 + "onDelete": "cascade", 638 + "onUpdate": "cascade" 639 + }, 640 + "asset_created_by_user_id_fk": { 641 + "name": "asset_created_by_user_id_fk", 642 + "tableFrom": "asset", 643 + "tableTo": "user", 644 + "columnsFrom": ["created_by"], 645 + "columnsTo": ["id"], 646 + "onDelete": "set null", 647 + "onUpdate": "cascade" 648 + } 649 + }, 650 + "compositePrimaryKeys": {}, 651 + "uniqueConstraints": { 652 + "asset_object_key_unique": { 653 + "name": "asset_object_key_unique", 654 + "nullsNotDistinct": false, 655 + "columns": ["object_key"] 656 + } 657 + }, 658 + "policies": {}, 659 + "checkConstraints": {}, 660 + "isRLSEnabled": false 661 + }, 662 + "public.column": { 663 + "name": "column", 664 + "schema": "", 665 + "columns": { 666 + "id": { 667 + "name": "id", 668 + "type": "text", 669 + "primaryKey": true, 670 + "notNull": true 671 + }, 672 + "project_id": { 673 + "name": "project_id", 674 + "type": "text", 675 + "primaryKey": false, 676 + "notNull": true 677 + }, 678 + "name": { 679 + "name": "name", 680 + "type": "text", 681 + "primaryKey": false, 682 + "notNull": true 683 + }, 684 + "slug": { 685 + "name": "slug", 686 + "type": "text", 687 + "primaryKey": false, 688 + "notNull": true 689 + }, 690 + "position": { 691 + "name": "position", 692 + "type": "integer", 693 + "primaryKey": false, 694 + "notNull": true, 695 + "default": 0 696 + }, 697 + "icon": { 698 + "name": "icon", 699 + "type": "text", 700 + "primaryKey": false, 701 + "notNull": false 702 + }, 703 + "color": { 704 + "name": "color", 705 + "type": "text", 706 + "primaryKey": false, 707 + "notNull": false 708 + }, 709 + "is_final": { 710 + "name": "is_final", 711 + "type": "boolean", 712 + "primaryKey": false, 713 + "notNull": true, 714 + "default": false 715 + }, 716 + "created_at": { 717 + "name": "created_at", 718 + "type": "timestamp", 719 + "primaryKey": false, 720 + "notNull": true, 721 + "default": "now()" 722 + }, 723 + "updated_at": { 724 + "name": "updated_at", 725 + "type": "timestamp", 726 + "primaryKey": false, 727 + "notNull": true, 728 + "default": "now()" 729 + } 730 + }, 731 + "indexes": { 732 + "column_projectId_idx": { 733 + "name": "column_projectId_idx", 734 + "columns": [ 735 + { 736 + "expression": "project_id", 737 + "isExpression": false, 738 + "asc": true, 739 + "nulls": "last" 740 + } 741 + ], 742 + "isUnique": false, 743 + "concurrently": false, 744 + "method": "btree", 745 + "with": {} 746 + } 747 + }, 748 + "foreignKeys": { 749 + "column_project_id_project_id_fk": { 750 + "name": "column_project_id_project_id_fk", 751 + "tableFrom": "column", 752 + "tableTo": "project", 753 + "columnsFrom": ["project_id"], 754 + "columnsTo": ["id"], 755 + "onDelete": "cascade", 756 + "onUpdate": "cascade" 757 + } 758 + }, 759 + "compositePrimaryKeys": {}, 760 + "uniqueConstraints": {}, 761 + "policies": {}, 762 + "checkConstraints": {}, 763 + "isRLSEnabled": false 764 + }, 765 + "public.comment": { 766 + "name": "comment", 767 + "schema": "", 768 + "columns": { 769 + "id": { 770 + "name": "id", 771 + "type": "text", 772 + "primaryKey": true, 773 + "notNull": true 774 + }, 775 + "task_id": { 776 + "name": "task_id", 777 + "type": "text", 778 + "primaryKey": false, 779 + "notNull": true 780 + }, 781 + "user_id": { 782 + "name": "user_id", 783 + "type": "text", 784 + "primaryKey": false, 785 + "notNull": true 786 + }, 787 + "content": { 788 + "name": "content", 789 + "type": "text", 790 + "primaryKey": false, 791 + "notNull": true 792 + }, 793 + "created_at": { 794 + "name": "created_at", 795 + "type": "timestamp", 796 + "primaryKey": false, 797 + "notNull": true, 798 + "default": "now()" 799 + }, 800 + "updated_at": { 801 + "name": "updated_at", 802 + "type": "timestamp", 803 + "primaryKey": false, 804 + "notNull": true, 805 + "default": "now()" 806 + } 807 + }, 808 + "indexes": { 809 + "comment_task_idx": { 810 + "name": "comment_task_idx", 811 + "columns": [ 812 + { 813 + "expression": "task_id", 814 + "isExpression": false, 815 + "asc": true, 816 + "nulls": "last" 817 + } 818 + ], 819 + "isUnique": false, 820 + "concurrently": false, 821 + "method": "btree", 822 + "with": {} 823 + }, 824 + "comment_user_idx": { 825 + "name": "comment_user_idx", 826 + "columns": [ 827 + { 828 + "expression": "user_id", 829 + "isExpression": false, 830 + "asc": true, 831 + "nulls": "last" 832 + } 833 + ], 834 + "isUnique": false, 835 + "concurrently": false, 836 + "method": "btree", 837 + "with": {} 838 + } 839 + }, 840 + "foreignKeys": { 841 + "comment_task_id_task_id_fk": { 842 + "name": "comment_task_id_task_id_fk", 843 + "tableFrom": "comment", 844 + "tableTo": "task", 845 + "columnsFrom": ["task_id"], 846 + "columnsTo": ["id"], 847 + "onDelete": "cascade", 848 + "onUpdate": "cascade" 849 + }, 850 + "comment_user_id_user_id_fk": { 851 + "name": "comment_user_id_user_id_fk", 852 + "tableFrom": "comment", 853 + "tableTo": "user", 854 + "columnsFrom": ["user_id"], 855 + "columnsTo": ["id"], 856 + "onDelete": "cascade", 857 + "onUpdate": "cascade" 858 + } 859 + }, 860 + "compositePrimaryKeys": {}, 861 + "uniqueConstraints": {}, 862 + "policies": {}, 863 + "checkConstraints": {}, 864 + "isRLSEnabled": false 865 + }, 866 + "public.external_link": { 867 + "name": "external_link", 868 + "schema": "", 869 + "columns": { 870 + "id": { 871 + "name": "id", 872 + "type": "text", 873 + "primaryKey": true, 874 + "notNull": true 875 + }, 876 + "task_id": { 877 + "name": "task_id", 878 + "type": "text", 879 + "primaryKey": false, 880 + "notNull": true 881 + }, 882 + "integration_id": { 883 + "name": "integration_id", 884 + "type": "text", 885 + "primaryKey": false, 886 + "notNull": true 887 + }, 888 + "resource_type": { 889 + "name": "resource_type", 890 + "type": "text", 891 + "primaryKey": false, 892 + "notNull": true 893 + }, 894 + "external_id": { 895 + "name": "external_id", 896 + "type": "text", 897 + "primaryKey": false, 898 + "notNull": true 899 + }, 900 + "url": { 901 + "name": "url", 902 + "type": "text", 903 + "primaryKey": false, 904 + "notNull": true 905 + }, 906 + "title": { 907 + "name": "title", 908 + "type": "text", 909 + "primaryKey": false, 910 + "notNull": false 911 + }, 912 + "metadata": { 913 + "name": "metadata", 914 + "type": "text", 915 + "primaryKey": false, 916 + "notNull": false 917 + }, 918 + "created_at": { 919 + "name": "created_at", 920 + "type": "timestamp", 921 + "primaryKey": false, 922 + "notNull": true, 923 + "default": "now()" 924 + }, 925 + "updated_at": { 926 + "name": "updated_at", 927 + "type": "timestamp", 928 + "primaryKey": false, 929 + "notNull": true, 930 + "default": "now()" 931 + } 932 + }, 933 + "indexes": { 934 + "external_link_taskId_idx": { 935 + "name": "external_link_taskId_idx", 936 + "columns": [ 937 + { 938 + "expression": "task_id", 939 + "isExpression": false, 940 + "asc": true, 941 + "nulls": "last" 942 + } 943 + ], 944 + "isUnique": false, 945 + "concurrently": false, 946 + "method": "btree", 947 + "with": {} 948 + }, 949 + "external_link_integrationId_idx": { 950 + "name": "external_link_integrationId_idx", 951 + "columns": [ 952 + { 953 + "expression": "integration_id", 954 + "isExpression": false, 955 + "asc": true, 956 + "nulls": "last" 957 + } 958 + ], 959 + "isUnique": false, 960 + "concurrently": false, 961 + "method": "btree", 962 + "with": {} 963 + }, 964 + "external_link_externalId_idx": { 965 + "name": "external_link_externalId_idx", 966 + "columns": [ 967 + { 968 + "expression": "external_id", 969 + "isExpression": false, 970 + "asc": true, 971 + "nulls": "last" 972 + } 973 + ], 974 + "isUnique": false, 975 + "concurrently": false, 976 + "method": "btree", 977 + "with": {} 978 + }, 979 + "external_link_resourceType_idx": { 980 + "name": "external_link_resourceType_idx", 981 + "columns": [ 982 + { 983 + "expression": "resource_type", 984 + "isExpression": false, 985 + "asc": true, 986 + "nulls": "last" 987 + } 988 + ], 989 + "isUnique": false, 990 + "concurrently": false, 991 + "method": "btree", 992 + "with": {} 993 + } 994 + }, 995 + "foreignKeys": { 996 + "external_link_task_id_task_id_fk": { 997 + "name": "external_link_task_id_task_id_fk", 998 + "tableFrom": "external_link", 999 + "tableTo": "task", 1000 + "columnsFrom": ["task_id"], 1001 + "columnsTo": ["id"], 1002 + "onDelete": "cascade", 1003 + "onUpdate": "cascade" 1004 + }, 1005 + "external_link_integration_id_integration_id_fk": { 1006 + "name": "external_link_integration_id_integration_id_fk", 1007 + "tableFrom": "external_link", 1008 + "tableTo": "integration", 1009 + "columnsFrom": ["integration_id"], 1010 + "columnsTo": ["id"], 1011 + "onDelete": "cascade", 1012 + "onUpdate": "cascade" 1013 + } 1014 + }, 1015 + "compositePrimaryKeys": {}, 1016 + "uniqueConstraints": {}, 1017 + "policies": {}, 1018 + "checkConstraints": {}, 1019 + "isRLSEnabled": false 1020 + }, 1021 + "public.github_integration": { 1022 + "name": "github_integration", 1023 + "schema": "", 1024 + "columns": { 1025 + "id": { 1026 + "name": "id", 1027 + "type": "text", 1028 + "primaryKey": true, 1029 + "notNull": true 1030 + }, 1031 + "project_id": { 1032 + "name": "project_id", 1033 + "type": "text", 1034 + "primaryKey": false, 1035 + "notNull": true 1036 + }, 1037 + "repository_owner": { 1038 + "name": "repository_owner", 1039 + "type": "text", 1040 + "primaryKey": false, 1041 + "notNull": true 1042 + }, 1043 + "repository_name": { 1044 + "name": "repository_name", 1045 + "type": "text", 1046 + "primaryKey": false, 1047 + "notNull": true 1048 + }, 1049 + "installation_id": { 1050 + "name": "installation_id", 1051 + "type": "integer", 1052 + "primaryKey": false, 1053 + "notNull": false 1054 + }, 1055 + "is_active": { 1056 + "name": "is_active", 1057 + "type": "boolean", 1058 + "primaryKey": false, 1059 + "notNull": false, 1060 + "default": true 1061 + }, 1062 + "created_at": { 1063 + "name": "created_at", 1064 + "type": "timestamp", 1065 + "primaryKey": false, 1066 + "notNull": true, 1067 + "default": "now()" 1068 + }, 1069 + "updated_at": { 1070 + "name": "updated_at", 1071 + "type": "timestamp", 1072 + "primaryKey": false, 1073 + "notNull": true, 1074 + "default": "now()" 1075 + } 1076 + }, 1077 + "indexes": {}, 1078 + "foreignKeys": { 1079 + "github_integration_project_id_project_id_fk": { 1080 + "name": "github_integration_project_id_project_id_fk", 1081 + "tableFrom": "github_integration", 1082 + "tableTo": "project", 1083 + "columnsFrom": ["project_id"], 1084 + "columnsTo": ["id"], 1085 + "onDelete": "cascade", 1086 + "onUpdate": "cascade" 1087 + } 1088 + }, 1089 + "compositePrimaryKeys": {}, 1090 + "uniqueConstraints": { 1091 + "github_integration_project_id_unique": { 1092 + "name": "github_integration_project_id_unique", 1093 + "nullsNotDistinct": false, 1094 + "columns": ["project_id"] 1095 + } 1096 + }, 1097 + "policies": {}, 1098 + "checkConstraints": {}, 1099 + "isRLSEnabled": false 1100 + }, 1101 + "public.integration": { 1102 + "name": "integration", 1103 + "schema": "", 1104 + "columns": { 1105 + "id": { 1106 + "name": "id", 1107 + "type": "text", 1108 + "primaryKey": true, 1109 + "notNull": true 1110 + }, 1111 + "project_id": { 1112 + "name": "project_id", 1113 + "type": "text", 1114 + "primaryKey": false, 1115 + "notNull": true 1116 + }, 1117 + "type": { 1118 + "name": "type", 1119 + "type": "text", 1120 + "primaryKey": false, 1121 + "notNull": true 1122 + }, 1123 + "config": { 1124 + "name": "config", 1125 + "type": "text", 1126 + "primaryKey": false, 1127 + "notNull": true 1128 + }, 1129 + "is_active": { 1130 + "name": "is_active", 1131 + "type": "boolean", 1132 + "primaryKey": false, 1133 + "notNull": false, 1134 + "default": true 1135 + }, 1136 + "created_at": { 1137 + "name": "created_at", 1138 + "type": "timestamp", 1139 + "primaryKey": false, 1140 + "notNull": true, 1141 + "default": "now()" 1142 + }, 1143 + "updated_at": { 1144 + "name": "updated_at", 1145 + "type": "timestamp", 1146 + "primaryKey": false, 1147 + "notNull": true, 1148 + "default": "now()" 1149 + } 1150 + }, 1151 + "indexes": { 1152 + "integration_projectId_idx": { 1153 + "name": "integration_projectId_idx", 1154 + "columns": [ 1155 + { 1156 + "expression": "project_id", 1157 + "isExpression": false, 1158 + "asc": true, 1159 + "nulls": "last" 1160 + } 1161 + ], 1162 + "isUnique": false, 1163 + "concurrently": false, 1164 + "method": "btree", 1165 + "with": {} 1166 + }, 1167 + "integration_type_idx": { 1168 + "name": "integration_type_idx", 1169 + "columns": [ 1170 + { 1171 + "expression": "type", 1172 + "isExpression": false, 1173 + "asc": true, 1174 + "nulls": "last" 1175 + } 1176 + ], 1177 + "isUnique": false, 1178 + "concurrently": false, 1179 + "method": "btree", 1180 + "with": {} 1181 + } 1182 + }, 1183 + "foreignKeys": { 1184 + "integration_project_id_project_id_fk": { 1185 + "name": "integration_project_id_project_id_fk", 1186 + "tableFrom": "integration", 1187 + "tableTo": "project", 1188 + "columnsFrom": ["project_id"], 1189 + "columnsTo": ["id"], 1190 + "onDelete": "cascade", 1191 + "onUpdate": "cascade" 1192 + } 1193 + }, 1194 + "compositePrimaryKeys": {}, 1195 + "uniqueConstraints": {}, 1196 + "policies": {}, 1197 + "checkConstraints": {}, 1198 + "isRLSEnabled": false 1199 + }, 1200 + "public.invitation": { 1201 + "name": "invitation", 1202 + "schema": "", 1203 + "columns": { 1204 + "id": { 1205 + "name": "id", 1206 + "type": "text", 1207 + "primaryKey": true, 1208 + "notNull": true 1209 + }, 1210 + "workspace_id": { 1211 + "name": "workspace_id", 1212 + "type": "text", 1213 + "primaryKey": false, 1214 + "notNull": true 1215 + }, 1216 + "email": { 1217 + "name": "email", 1218 + "type": "text", 1219 + "primaryKey": false, 1220 + "notNull": true 1221 + }, 1222 + "role": { 1223 + "name": "role", 1224 + "type": "text", 1225 + "primaryKey": false, 1226 + "notNull": false 1227 + }, 1228 + "team_id": { 1229 + "name": "team_id", 1230 + "type": "text", 1231 + "primaryKey": false, 1232 + "notNull": false 1233 + }, 1234 + "status": { 1235 + "name": "status", 1236 + "type": "text", 1237 + "primaryKey": false, 1238 + "notNull": true, 1239 + "default": "'pending'" 1240 + }, 1241 + "expires_at": { 1242 + "name": "expires_at", 1243 + "type": "timestamp", 1244 + "primaryKey": false, 1245 + "notNull": true 1246 + }, 1247 + "created_at": { 1248 + "name": "created_at", 1249 + "type": "timestamp", 1250 + "primaryKey": false, 1251 + "notNull": true, 1252 + "default": "now()" 1253 + }, 1254 + "inviter_id": { 1255 + "name": "inviter_id", 1256 + "type": "text", 1257 + "primaryKey": false, 1258 + "notNull": true 1259 + } 1260 + }, 1261 + "indexes": { 1262 + "invitation_workspaceId_idx": { 1263 + "name": "invitation_workspaceId_idx", 1264 + "columns": [ 1265 + { 1266 + "expression": "workspace_id", 1267 + "isExpression": false, 1268 + "asc": true, 1269 + "nulls": "last" 1270 + } 1271 + ], 1272 + "isUnique": false, 1273 + "concurrently": false, 1274 + "method": "btree", 1275 + "with": {} 1276 + }, 1277 + "invitation_email_idx": { 1278 + "name": "invitation_email_idx", 1279 + "columns": [ 1280 + { 1281 + "expression": "email", 1282 + "isExpression": false, 1283 + "asc": true, 1284 + "nulls": "last" 1285 + } 1286 + ], 1287 + "isUnique": false, 1288 + "concurrently": false, 1289 + "method": "btree", 1290 + "with": {} 1291 + } 1292 + }, 1293 + "foreignKeys": { 1294 + "invitation_workspace_id_workspace_id_fk": { 1295 + "name": "invitation_workspace_id_workspace_id_fk", 1296 + "tableFrom": "invitation", 1297 + "tableTo": "workspace", 1298 + "columnsFrom": ["workspace_id"], 1299 + "columnsTo": ["id"], 1300 + "onDelete": "cascade", 1301 + "onUpdate": "no action" 1302 + }, 1303 + "invitation_inviter_id_user_id_fk": { 1304 + "name": "invitation_inviter_id_user_id_fk", 1305 + "tableFrom": "invitation", 1306 + "tableTo": "user", 1307 + "columnsFrom": ["inviter_id"], 1308 + "columnsTo": ["id"], 1309 + "onDelete": "cascade", 1310 + "onUpdate": "no action" 1311 + } 1312 + }, 1313 + "compositePrimaryKeys": {}, 1314 + "uniqueConstraints": {}, 1315 + "policies": {}, 1316 + "checkConstraints": {}, 1317 + "isRLSEnabled": false 1318 + }, 1319 + "public.label": { 1320 + "name": "label", 1321 + "schema": "", 1322 + "columns": { 1323 + "id": { 1324 + "name": "id", 1325 + "type": "text", 1326 + "primaryKey": true, 1327 + "notNull": true 1328 + }, 1329 + "name": { 1330 + "name": "name", 1331 + "type": "text", 1332 + "primaryKey": false, 1333 + "notNull": true 1334 + }, 1335 + "color": { 1336 + "name": "color", 1337 + "type": "text", 1338 + "primaryKey": false, 1339 + "notNull": true 1340 + }, 1341 + "created_at": { 1342 + "name": "created_at", 1343 + "type": "timestamp", 1344 + "primaryKey": false, 1345 + "notNull": true, 1346 + "default": "now()" 1347 + }, 1348 + "task_id": { 1349 + "name": "task_id", 1350 + "type": "text", 1351 + "primaryKey": false, 1352 + "notNull": false 1353 + }, 1354 + "workspace_id": { 1355 + "name": "workspace_id", 1356 + "type": "text", 1357 + "primaryKey": false, 1358 + "notNull": false 1359 + } 1360 + }, 1361 + "indexes": {}, 1362 + "foreignKeys": { 1363 + "label_task_id_task_id_fk": { 1364 + "name": "label_task_id_task_id_fk", 1365 + "tableFrom": "label", 1366 + "tableTo": "task", 1367 + "columnsFrom": ["task_id"], 1368 + "columnsTo": ["id"], 1369 + "onDelete": "cascade", 1370 + "onUpdate": "cascade" 1371 + }, 1372 + "label_workspace_id_workspace_id_fk": { 1373 + "name": "label_workspace_id_workspace_id_fk", 1374 + "tableFrom": "label", 1375 + "tableTo": "workspace", 1376 + "columnsFrom": ["workspace_id"], 1377 + "columnsTo": ["id"], 1378 + "onDelete": "cascade", 1379 + "onUpdate": "cascade" 1380 + } 1381 + }, 1382 + "compositePrimaryKeys": {}, 1383 + "uniqueConstraints": {}, 1384 + "policies": {}, 1385 + "checkConstraints": {}, 1386 + "isRLSEnabled": false 1387 + }, 1388 + "public.notification": { 1389 + "name": "notification", 1390 + "schema": "", 1391 + "columns": { 1392 + "id": { 1393 + "name": "id", 1394 + "type": "text", 1395 + "primaryKey": true, 1396 + "notNull": true 1397 + }, 1398 + "user_id": { 1399 + "name": "user_id", 1400 + "type": "text", 1401 + "primaryKey": false, 1402 + "notNull": true 1403 + }, 1404 + "title": { 1405 + "name": "title", 1406 + "type": "text", 1407 + "primaryKey": false, 1408 + "notNull": true 1409 + }, 1410 + "content": { 1411 + "name": "content", 1412 + "type": "text", 1413 + "primaryKey": false, 1414 + "notNull": false 1415 + }, 1416 + "type": { 1417 + "name": "type", 1418 + "type": "text", 1419 + "primaryKey": false, 1420 + "notNull": true, 1421 + "default": "'info'" 1422 + }, 1423 + "is_read": { 1424 + "name": "is_read", 1425 + "type": "boolean", 1426 + "primaryKey": false, 1427 + "notNull": false, 1428 + "default": false 1429 + }, 1430 + "resource_id": { 1431 + "name": "resource_id", 1432 + "type": "text", 1433 + "primaryKey": false, 1434 + "notNull": false 1435 + }, 1436 + "resource_type": { 1437 + "name": "resource_type", 1438 + "type": "text", 1439 + "primaryKey": false, 1440 + "notNull": false 1441 + }, 1442 + "created_at": { 1443 + "name": "created_at", 1444 + "type": "timestamp with time zone", 1445 + "primaryKey": false, 1446 + "notNull": true, 1447 + "default": "now()" 1448 + } 1449 + }, 1450 + "indexes": {}, 1451 + "foreignKeys": { 1452 + "notification_user_id_user_id_fk": { 1453 + "name": "notification_user_id_user_id_fk", 1454 + "tableFrom": "notification", 1455 + "tableTo": "user", 1456 + "columnsFrom": ["user_id"], 1457 + "columnsTo": ["id"], 1458 + "onDelete": "cascade", 1459 + "onUpdate": "cascade" 1460 + } 1461 + }, 1462 + "compositePrimaryKeys": {}, 1463 + "uniqueConstraints": {}, 1464 + "policies": {}, 1465 + "checkConstraints": {}, 1466 + "isRLSEnabled": false 1467 + }, 1468 + "public.project": { 1469 + "name": "project", 1470 + "schema": "", 1471 + "columns": { 1472 + "id": { 1473 + "name": "id", 1474 + "type": "text", 1475 + "primaryKey": true, 1476 + "notNull": true 1477 + }, 1478 + "workspace_id": { 1479 + "name": "workspace_id", 1480 + "type": "text", 1481 + "primaryKey": false, 1482 + "notNull": true 1483 + }, 1484 + "slug": { 1485 + "name": "slug", 1486 + "type": "text", 1487 + "primaryKey": false, 1488 + "notNull": true 1489 + }, 1490 + "icon": { 1491 + "name": "icon", 1492 + "type": "text", 1493 + "primaryKey": false, 1494 + "notNull": false, 1495 + "default": "'Layout'" 1496 + }, 1497 + "name": { 1498 + "name": "name", 1499 + "type": "text", 1500 + "primaryKey": false, 1501 + "notNull": true 1502 + }, 1503 + "description": { 1504 + "name": "description", 1505 + "type": "text", 1506 + "primaryKey": false, 1507 + "notNull": false 1508 + }, 1509 + "created_at": { 1510 + "name": "created_at", 1511 + "type": "timestamp", 1512 + "primaryKey": false, 1513 + "notNull": true, 1514 + "default": "now()" 1515 + }, 1516 + "is_public": { 1517 + "name": "is_public", 1518 + "type": "boolean", 1519 + "primaryKey": false, 1520 + "notNull": false, 1521 + "default": false 1522 + }, 1523 + "archived_at": { 1524 + "name": "archived_at", 1525 + "type": "timestamp", 1526 + "primaryKey": false, 1527 + "notNull": false 1528 + } 1529 + }, 1530 + "indexes": {}, 1531 + "foreignKeys": { 1532 + "project_workspace_id_workspace_id_fk": { 1533 + "name": "project_workspace_id_workspace_id_fk", 1534 + "tableFrom": "project", 1535 + "tableTo": "workspace", 1536 + "columnsFrom": ["workspace_id"], 1537 + "columnsTo": ["id"], 1538 + "onDelete": "cascade", 1539 + "onUpdate": "cascade" 1540 + } 1541 + }, 1542 + "compositePrimaryKeys": {}, 1543 + "uniqueConstraints": {}, 1544 + "policies": {}, 1545 + "checkConstraints": {}, 1546 + "isRLSEnabled": false 1547 + }, 1548 + "public.session": { 1549 + "name": "session", 1550 + "schema": "", 1551 + "columns": { 1552 + "id": { 1553 + "name": "id", 1554 + "type": "text", 1555 + "primaryKey": true, 1556 + "notNull": true 1557 + }, 1558 + "expires_at": { 1559 + "name": "expires_at", 1560 + "type": "timestamp", 1561 + "primaryKey": false, 1562 + "notNull": true 1563 + }, 1564 + "token": { 1565 + "name": "token", 1566 + "type": "text", 1567 + "primaryKey": false, 1568 + "notNull": true 1569 + }, 1570 + "created_at": { 1571 + "name": "created_at", 1572 + "type": "timestamp", 1573 + "primaryKey": false, 1574 + "notNull": true, 1575 + "default": "now()" 1576 + }, 1577 + "updated_at": { 1578 + "name": "updated_at", 1579 + "type": "timestamp", 1580 + "primaryKey": false, 1581 + "notNull": true 1582 + }, 1583 + "ip_address": { 1584 + "name": "ip_address", 1585 + "type": "text", 1586 + "primaryKey": false, 1587 + "notNull": false 1588 + }, 1589 + "user_agent": { 1590 + "name": "user_agent", 1591 + "type": "text", 1592 + "primaryKey": false, 1593 + "notNull": false 1594 + }, 1595 + "user_id": { 1596 + "name": "user_id", 1597 + "type": "text", 1598 + "primaryKey": false, 1599 + "notNull": true 1600 + }, 1601 + "active_organization_id": { 1602 + "name": "active_organization_id", 1603 + "type": "text", 1604 + "primaryKey": false, 1605 + "notNull": false 1606 + }, 1607 + "active_team_id": { 1608 + "name": "active_team_id", 1609 + "type": "text", 1610 + "primaryKey": false, 1611 + "notNull": false 1612 + } 1613 + }, 1614 + "indexes": { 1615 + "session_userId_idx": { 1616 + "name": "session_userId_idx", 1617 + "columns": [ 1618 + { 1619 + "expression": "user_id", 1620 + "isExpression": false, 1621 + "asc": true, 1622 + "nulls": "last" 1623 + } 1624 + ], 1625 + "isUnique": false, 1626 + "concurrently": false, 1627 + "method": "btree", 1628 + "with": {} 1629 + } 1630 + }, 1631 + "foreignKeys": { 1632 + "session_user_id_user_id_fk": { 1633 + "name": "session_user_id_user_id_fk", 1634 + "tableFrom": "session", 1635 + "tableTo": "user", 1636 + "columnsFrom": ["user_id"], 1637 + "columnsTo": ["id"], 1638 + "onDelete": "cascade", 1639 + "onUpdate": "no action" 1640 + } 1641 + }, 1642 + "compositePrimaryKeys": {}, 1643 + "uniqueConstraints": { 1644 + "session_token_unique": { 1645 + "name": "session_token_unique", 1646 + "nullsNotDistinct": false, 1647 + "columns": ["token"] 1648 + } 1649 + }, 1650 + "policies": {}, 1651 + "checkConstraints": {}, 1652 + "isRLSEnabled": false 1653 + }, 1654 + "public.task_relation": { 1655 + "name": "task_relation", 1656 + "schema": "", 1657 + "columns": { 1658 + "id": { 1659 + "name": "id", 1660 + "type": "text", 1661 + "primaryKey": true, 1662 + "notNull": true 1663 + }, 1664 + "source_task_id": { 1665 + "name": "source_task_id", 1666 + "type": "text", 1667 + "primaryKey": false, 1668 + "notNull": true 1669 + }, 1670 + "target_task_id": { 1671 + "name": "target_task_id", 1672 + "type": "text", 1673 + "primaryKey": false, 1674 + "notNull": true 1675 + }, 1676 + "relation_type": { 1677 + "name": "relation_type", 1678 + "type": "text", 1679 + "primaryKey": false, 1680 + "notNull": true 1681 + }, 1682 + "created_at": { 1683 + "name": "created_at", 1684 + "type": "timestamp", 1685 + "primaryKey": false, 1686 + "notNull": true, 1687 + "default": "now()" 1688 + } 1689 + }, 1690 + "indexes": { 1691 + "task_relation_source_idx": { 1692 + "name": "task_relation_source_idx", 1693 + "columns": [ 1694 + { 1695 + "expression": "source_task_id", 1696 + "isExpression": false, 1697 + "asc": true, 1698 + "nulls": "last" 1699 + } 1700 + ], 1701 + "isUnique": false, 1702 + "concurrently": false, 1703 + "method": "btree", 1704 + "with": {} 1705 + }, 1706 + "task_relation_target_idx": { 1707 + "name": "task_relation_target_idx", 1708 + "columns": [ 1709 + { 1710 + "expression": "target_task_id", 1711 + "isExpression": false, 1712 + "asc": true, 1713 + "nulls": "last" 1714 + } 1715 + ], 1716 + "isUnique": false, 1717 + "concurrently": false, 1718 + "method": "btree", 1719 + "with": {} 1720 + } 1721 + }, 1722 + "foreignKeys": { 1723 + "task_relation_source_task_id_task_id_fk": { 1724 + "name": "task_relation_source_task_id_task_id_fk", 1725 + "tableFrom": "task_relation", 1726 + "tableTo": "task", 1727 + "columnsFrom": ["source_task_id"], 1728 + "columnsTo": ["id"], 1729 + "onDelete": "cascade", 1730 + "onUpdate": "cascade" 1731 + }, 1732 + "task_relation_target_task_id_task_id_fk": { 1733 + "name": "task_relation_target_task_id_task_id_fk", 1734 + "tableFrom": "task_relation", 1735 + "tableTo": "task", 1736 + "columnsFrom": ["target_task_id"], 1737 + "columnsTo": ["id"], 1738 + "onDelete": "cascade", 1739 + "onUpdate": "cascade" 1740 + } 1741 + }, 1742 + "compositePrimaryKeys": {}, 1743 + "uniqueConstraints": {}, 1744 + "policies": {}, 1745 + "checkConstraints": {}, 1746 + "isRLSEnabled": false 1747 + }, 1748 + "public.task": { 1749 + "name": "task", 1750 + "schema": "", 1751 + "columns": { 1752 + "id": { 1753 + "name": "id", 1754 + "type": "text", 1755 + "primaryKey": true, 1756 + "notNull": true 1757 + }, 1758 + "project_id": { 1759 + "name": "project_id", 1760 + "type": "text", 1761 + "primaryKey": false, 1762 + "notNull": true 1763 + }, 1764 + "position": { 1765 + "name": "position", 1766 + "type": "integer", 1767 + "primaryKey": false, 1768 + "notNull": false, 1769 + "default": 0 1770 + }, 1771 + "number": { 1772 + "name": "number", 1773 + "type": "integer", 1774 + "primaryKey": false, 1775 + "notNull": false, 1776 + "default": 1 1777 + }, 1778 + "assignee_id": { 1779 + "name": "assignee_id", 1780 + "type": "text", 1781 + "primaryKey": false, 1782 + "notNull": false 1783 + }, 1784 + "title": { 1785 + "name": "title", 1786 + "type": "text", 1787 + "primaryKey": false, 1788 + "notNull": true 1789 + }, 1790 + "description": { 1791 + "name": "description", 1792 + "type": "text", 1793 + "primaryKey": false, 1794 + "notNull": false 1795 + }, 1796 + "status": { 1797 + "name": "status", 1798 + "type": "text", 1799 + "primaryKey": false, 1800 + "notNull": true, 1801 + "default": "'to-do'" 1802 + }, 1803 + "column_id": { 1804 + "name": "column_id", 1805 + "type": "text", 1806 + "primaryKey": false, 1807 + "notNull": false 1808 + }, 1809 + "priority": { 1810 + "name": "priority", 1811 + "type": "text", 1812 + "primaryKey": false, 1813 + "notNull": false, 1814 + "default": "'low'" 1815 + }, 1816 + "start_date": { 1817 + "name": "start_date", 1818 + "type": "timestamp", 1819 + "primaryKey": false, 1820 + "notNull": false 1821 + }, 1822 + "due_date": { 1823 + "name": "due_date", 1824 + "type": "timestamp", 1825 + "primaryKey": false, 1826 + "notNull": false 1827 + }, 1828 + "created_at": { 1829 + "name": "created_at", 1830 + "type": "timestamp", 1831 + "primaryKey": false, 1832 + "notNull": true, 1833 + "default": "now()" 1834 + } 1835 + }, 1836 + "indexes": {}, 1837 + "foreignKeys": { 1838 + "task_project_id_project_id_fk": { 1839 + "name": "task_project_id_project_id_fk", 1840 + "tableFrom": "task", 1841 + "tableTo": "project", 1842 + "columnsFrom": ["project_id"], 1843 + "columnsTo": ["id"], 1844 + "onDelete": "cascade", 1845 + "onUpdate": "cascade" 1846 + }, 1847 + "task_assignee_id_user_id_fk": { 1848 + "name": "task_assignee_id_user_id_fk", 1849 + "tableFrom": "task", 1850 + "tableTo": "user", 1851 + "columnsFrom": ["assignee_id"], 1852 + "columnsTo": ["id"], 1853 + "onDelete": "cascade", 1854 + "onUpdate": "cascade" 1855 + }, 1856 + "task_column_id_column_id_fk": { 1857 + "name": "task_column_id_column_id_fk", 1858 + "tableFrom": "task", 1859 + "tableTo": "column", 1860 + "columnsFrom": ["column_id"], 1861 + "columnsTo": ["id"], 1862 + "onDelete": "set null", 1863 + "onUpdate": "cascade" 1864 + } 1865 + }, 1866 + "compositePrimaryKeys": {}, 1867 + "uniqueConstraints": {}, 1868 + "policies": {}, 1869 + "checkConstraints": {}, 1870 + "isRLSEnabled": false 1871 + }, 1872 + "public.team": { 1873 + "name": "team", 1874 + "schema": "", 1875 + "columns": { 1876 + "id": { 1877 + "name": "id", 1878 + "type": "text", 1879 + "primaryKey": true, 1880 + "notNull": true 1881 + }, 1882 + "name": { 1883 + "name": "name", 1884 + "type": "text", 1885 + "primaryKey": false, 1886 + "notNull": true 1887 + }, 1888 + "workspace_id": { 1889 + "name": "workspace_id", 1890 + "type": "text", 1891 + "primaryKey": false, 1892 + "notNull": true 1893 + }, 1894 + "created_at": { 1895 + "name": "created_at", 1896 + "type": "timestamp", 1897 + "primaryKey": false, 1898 + "notNull": true 1899 + }, 1900 + "updated_at": { 1901 + "name": "updated_at", 1902 + "type": "timestamp", 1903 + "primaryKey": false, 1904 + "notNull": false 1905 + } 1906 + }, 1907 + "indexes": { 1908 + "team_workspaceId_idx": { 1909 + "name": "team_workspaceId_idx", 1910 + "columns": [ 1911 + { 1912 + "expression": "workspace_id", 1913 + "isExpression": false, 1914 + "asc": true, 1915 + "nulls": "last" 1916 + } 1917 + ], 1918 + "isUnique": false, 1919 + "concurrently": false, 1920 + "method": "btree", 1921 + "with": {} 1922 + } 1923 + }, 1924 + "foreignKeys": { 1925 + "team_workspace_id_workspace_id_fk": { 1926 + "name": "team_workspace_id_workspace_id_fk", 1927 + "tableFrom": "team", 1928 + "tableTo": "workspace", 1929 + "columnsFrom": ["workspace_id"], 1930 + "columnsTo": ["id"], 1931 + "onDelete": "cascade", 1932 + "onUpdate": "no action" 1933 + } 1934 + }, 1935 + "compositePrimaryKeys": {}, 1936 + "uniqueConstraints": {}, 1937 + "policies": {}, 1938 + "checkConstraints": {}, 1939 + "isRLSEnabled": false 1940 + }, 1941 + "public.team_member": { 1942 + "name": "team_member", 1943 + "schema": "", 1944 + "columns": { 1945 + "id": { 1946 + "name": "id", 1947 + "type": "text", 1948 + "primaryKey": true, 1949 + "notNull": true 1950 + }, 1951 + "team_id": { 1952 + "name": "team_id", 1953 + "type": "text", 1954 + "primaryKey": false, 1955 + "notNull": true 1956 + }, 1957 + "user_id": { 1958 + "name": "user_id", 1959 + "type": "text", 1960 + "primaryKey": false, 1961 + "notNull": true 1962 + }, 1963 + "created_at": { 1964 + "name": "created_at", 1965 + "type": "timestamp", 1966 + "primaryKey": false, 1967 + "notNull": false 1968 + } 1969 + }, 1970 + "indexes": { 1971 + "teamMember_teamId_idx": { 1972 + "name": "teamMember_teamId_idx", 1973 + "columns": [ 1974 + { 1975 + "expression": "team_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 + "teamMember_userId_idx": { 1987 + "name": "teamMember_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 + "team_member_team_id_team_id_fk": { 2004 + "name": "team_member_team_id_team_id_fk", 2005 + "tableFrom": "team_member", 2006 + "tableTo": "team", 2007 + "columnsFrom": ["team_id"], 2008 + "columnsTo": ["id"], 2009 + "onDelete": "cascade", 2010 + "onUpdate": "no action" 2011 + }, 2012 + "team_member_user_id_user_id_fk": { 2013 + "name": "team_member_user_id_user_id_fk", 2014 + "tableFrom": "team_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 + "public.time_entry": { 2029 + "name": "time_entry", 2030 + "schema": "", 2031 + "columns": { 2032 + "id": { 2033 + "name": "id", 2034 + "type": "text", 2035 + "primaryKey": true, 2036 + "notNull": true 2037 + }, 2038 + "task_id": { 2039 + "name": "task_id", 2040 + "type": "text", 2041 + "primaryKey": false, 2042 + "notNull": true 2043 + }, 2044 + "user_id": { 2045 + "name": "user_id", 2046 + "type": "text", 2047 + "primaryKey": false, 2048 + "notNull": false 2049 + }, 2050 + "description": { 2051 + "name": "description", 2052 + "type": "text", 2053 + "primaryKey": false, 2054 + "notNull": false 2055 + }, 2056 + "start_time": { 2057 + "name": "start_time", 2058 + "type": "timestamp", 2059 + "primaryKey": false, 2060 + "notNull": true 2061 + }, 2062 + "end_time": { 2063 + "name": "end_time", 2064 + "type": "timestamp", 2065 + "primaryKey": false, 2066 + "notNull": false 2067 + }, 2068 + "duration": { 2069 + "name": "duration", 2070 + "type": "integer", 2071 + "primaryKey": false, 2072 + "notNull": false, 2073 + "default": 0 2074 + }, 2075 + "created_at": { 2076 + "name": "created_at", 2077 + "type": "timestamp", 2078 + "primaryKey": false, 2079 + "notNull": true, 2080 + "default": "now()" 2081 + } 2082 + }, 2083 + "indexes": {}, 2084 + "foreignKeys": { 2085 + "time_entry_task_id_task_id_fk": { 2086 + "name": "time_entry_task_id_task_id_fk", 2087 + "tableFrom": "time_entry", 2088 + "tableTo": "task", 2089 + "columnsFrom": ["task_id"], 2090 + "columnsTo": ["id"], 2091 + "onDelete": "cascade", 2092 + "onUpdate": "cascade" 2093 + }, 2094 + "time_entry_user_id_user_id_fk": { 2095 + "name": "time_entry_user_id_user_id_fk", 2096 + "tableFrom": "time_entry", 2097 + "tableTo": "user", 2098 + "columnsFrom": ["user_id"], 2099 + "columnsTo": ["id"], 2100 + "onDelete": "cascade", 2101 + "onUpdate": "cascade" 2102 + } 2103 + }, 2104 + "compositePrimaryKeys": {}, 2105 + "uniqueConstraints": {}, 2106 + "policies": {}, 2107 + "checkConstraints": {}, 2108 + "isRLSEnabled": false 2109 + }, 2110 + "public.user": { 2111 + "name": "user", 2112 + "schema": "", 2113 + "columns": { 2114 + "id": { 2115 + "name": "id", 2116 + "type": "text", 2117 + "primaryKey": true, 2118 + "notNull": true 2119 + }, 2120 + "name": { 2121 + "name": "name", 2122 + "type": "text", 2123 + "primaryKey": false, 2124 + "notNull": true 2125 + }, 2126 + "email": { 2127 + "name": "email", 2128 + "type": "text", 2129 + "primaryKey": false, 2130 + "notNull": true 2131 + }, 2132 + "email_verified": { 2133 + "name": "email_verified", 2134 + "type": "boolean", 2135 + "primaryKey": false, 2136 + "notNull": true 2137 + }, 2138 + "image": { 2139 + "name": "image", 2140 + "type": "text", 2141 + "primaryKey": false, 2142 + "notNull": false 2143 + }, 2144 + "created_at": { 2145 + "name": "created_at", 2146 + "type": "timestamp", 2147 + "primaryKey": false, 2148 + "notNull": true, 2149 + "default": "now()" 2150 + }, 2151 + "updated_at": { 2152 + "name": "updated_at", 2153 + "type": "timestamp", 2154 + "primaryKey": false, 2155 + "notNull": true, 2156 + "default": "now()" 2157 + }, 2158 + "is_anonymous": { 2159 + "name": "is_anonymous", 2160 + "type": "boolean", 2161 + "primaryKey": false, 2162 + "notNull": false, 2163 + "default": false 2164 + } 2165 + }, 2166 + "indexes": {}, 2167 + "foreignKeys": {}, 2168 + "compositePrimaryKeys": {}, 2169 + "uniqueConstraints": { 2170 + "user_email_unique": { 2171 + "name": "user_email_unique", 2172 + "nullsNotDistinct": false, 2173 + "columns": ["email"] 2174 + } 2175 + }, 2176 + "policies": {}, 2177 + "checkConstraints": {}, 2178 + "isRLSEnabled": false 2179 + }, 2180 + "public.verification": { 2181 + "name": "verification", 2182 + "schema": "", 2183 + "columns": { 2184 + "id": { 2185 + "name": "id", 2186 + "type": "text", 2187 + "primaryKey": true, 2188 + "notNull": true 2189 + }, 2190 + "identifier": { 2191 + "name": "identifier", 2192 + "type": "text", 2193 + "primaryKey": false, 2194 + "notNull": true 2195 + }, 2196 + "value": { 2197 + "name": "value", 2198 + "type": "text", 2199 + "primaryKey": false, 2200 + "notNull": true 2201 + }, 2202 + "expires_at": { 2203 + "name": "expires_at", 2204 + "type": "timestamp", 2205 + "primaryKey": false, 2206 + "notNull": true 2207 + }, 2208 + "created_at": { 2209 + "name": "created_at", 2210 + "type": "timestamp", 2211 + "primaryKey": false, 2212 + "notNull": true, 2213 + "default": "now()" 2214 + }, 2215 + "updated_at": { 2216 + "name": "updated_at", 2217 + "type": "timestamp", 2218 + "primaryKey": false, 2219 + "notNull": true, 2220 + "default": "now()" 2221 + } 2222 + }, 2223 + "indexes": { 2224 + "verification_identifier_idx": { 2225 + "name": "verification_identifier_idx", 2226 + "columns": [ 2227 + { 2228 + "expression": "identifier", 2229 + "isExpression": false, 2230 + "asc": true, 2231 + "nulls": "last" 2232 + } 2233 + ], 2234 + "isUnique": false, 2235 + "concurrently": false, 2236 + "method": "btree", 2237 + "with": {} 2238 + } 2239 + }, 2240 + "foreignKeys": {}, 2241 + "compositePrimaryKeys": {}, 2242 + "uniqueConstraints": {}, 2243 + "policies": {}, 2244 + "checkConstraints": {}, 2245 + "isRLSEnabled": false 2246 + }, 2247 + "public.workflow_rule": { 2248 + "name": "workflow_rule", 2249 + "schema": "", 2250 + "columns": { 2251 + "id": { 2252 + "name": "id", 2253 + "type": "text", 2254 + "primaryKey": true, 2255 + "notNull": true 2256 + }, 2257 + "project_id": { 2258 + "name": "project_id", 2259 + "type": "text", 2260 + "primaryKey": false, 2261 + "notNull": true 2262 + }, 2263 + "integration_type": { 2264 + "name": "integration_type", 2265 + "type": "text", 2266 + "primaryKey": false, 2267 + "notNull": true 2268 + }, 2269 + "event_type": { 2270 + "name": "event_type", 2271 + "type": "text", 2272 + "primaryKey": false, 2273 + "notNull": true 2274 + }, 2275 + "column_id": { 2276 + "name": "column_id", 2277 + "type": "text", 2278 + "primaryKey": false, 2279 + "notNull": true 2280 + }, 2281 + "created_at": { 2282 + "name": "created_at", 2283 + "type": "timestamp", 2284 + "primaryKey": false, 2285 + "notNull": true, 2286 + "default": "now()" 2287 + }, 2288 + "updated_at": { 2289 + "name": "updated_at", 2290 + "type": "timestamp", 2291 + "primaryKey": false, 2292 + "notNull": true, 2293 + "default": "now()" 2294 + } 2295 + }, 2296 + "indexes": { 2297 + "workflow_rule_projectId_idx": { 2298 + "name": "workflow_rule_projectId_idx", 2299 + "columns": [ 2300 + { 2301 + "expression": "project_id", 2302 + "isExpression": false, 2303 + "asc": true, 2304 + "nulls": "last" 2305 + } 2306 + ], 2307 + "isUnique": false, 2308 + "concurrently": false, 2309 + "method": "btree", 2310 + "with": {} 2311 + } 2312 + }, 2313 + "foreignKeys": { 2314 + "workflow_rule_project_id_project_id_fk": { 2315 + "name": "workflow_rule_project_id_project_id_fk", 2316 + "tableFrom": "workflow_rule", 2317 + "tableTo": "project", 2318 + "columnsFrom": ["project_id"], 2319 + "columnsTo": ["id"], 2320 + "onDelete": "cascade", 2321 + "onUpdate": "cascade" 2322 + }, 2323 + "workflow_rule_column_id_column_id_fk": { 2324 + "name": "workflow_rule_column_id_column_id_fk", 2325 + "tableFrom": "workflow_rule", 2326 + "tableTo": "column", 2327 + "columnsFrom": ["column_id"], 2328 + "columnsTo": ["id"], 2329 + "onDelete": "cascade", 2330 + "onUpdate": "cascade" 2331 + } 2332 + }, 2333 + "compositePrimaryKeys": {}, 2334 + "uniqueConstraints": {}, 2335 + "policies": {}, 2336 + "checkConstraints": {}, 2337 + "isRLSEnabled": false 2338 + }, 2339 + "public.workspace": { 2340 + "name": "workspace", 2341 + "schema": "", 2342 + "columns": { 2343 + "id": { 2344 + "name": "id", 2345 + "type": "text", 2346 + "primaryKey": true, 2347 + "notNull": true 2348 + }, 2349 + "name": { 2350 + "name": "name", 2351 + "type": "text", 2352 + "primaryKey": false, 2353 + "notNull": true 2354 + }, 2355 + "slug": { 2356 + "name": "slug", 2357 + "type": "text", 2358 + "primaryKey": false, 2359 + "notNull": true 2360 + }, 2361 + "logo": { 2362 + "name": "logo", 2363 + "type": "text", 2364 + "primaryKey": false, 2365 + "notNull": false 2366 + }, 2367 + "metadata": { 2368 + "name": "metadata", 2369 + "type": "text", 2370 + "primaryKey": false, 2371 + "notNull": false 2372 + }, 2373 + "description": { 2374 + "name": "description", 2375 + "type": "text", 2376 + "primaryKey": false, 2377 + "notNull": false 2378 + }, 2379 + "created_at": { 2380 + "name": "created_at", 2381 + "type": "timestamp", 2382 + "primaryKey": false, 2383 + "notNull": true 2384 + } 2385 + }, 2386 + "indexes": {}, 2387 + "foreignKeys": {}, 2388 + "compositePrimaryKeys": {}, 2389 + "uniqueConstraints": { 2390 + "workspace_slug_unique": { 2391 + "name": "workspace_slug_unique", 2392 + "nullsNotDistinct": false, 2393 + "columns": ["slug"] 2394 + } 2395 + }, 2396 + "policies": {}, 2397 + "checkConstraints": {}, 2398 + "isRLSEnabled": false 2399 + }, 2400 + "public.workspace_member": { 2401 + "name": "workspace_member", 2402 + "schema": "", 2403 + "columns": { 2404 + "id": { 2405 + "name": "id", 2406 + "type": "text", 2407 + "primaryKey": true, 2408 + "notNull": true 2409 + }, 2410 + "workspace_id": { 2411 + "name": "workspace_id", 2412 + "type": "text", 2413 + "primaryKey": false, 2414 + "notNull": true 2415 + }, 2416 + "user_id": { 2417 + "name": "user_id", 2418 + "type": "text", 2419 + "primaryKey": false, 2420 + "notNull": true 2421 + }, 2422 + "role": { 2423 + "name": "role", 2424 + "type": "text", 2425 + "primaryKey": false, 2426 + "notNull": true, 2427 + "default": "'member'" 2428 + }, 2429 + "joined_at": { 2430 + "name": "joined_at", 2431 + "type": "timestamp", 2432 + "primaryKey": false, 2433 + "notNull": true 2434 + } 2435 + }, 2436 + "indexes": { 2437 + "workspace_member_workspaceId_idx": { 2438 + "name": "workspace_member_workspaceId_idx", 2439 + "columns": [ 2440 + { 2441 + "expression": "workspace_id", 2442 + "isExpression": false, 2443 + "asc": true, 2444 + "nulls": "last" 2445 + } 2446 + ], 2447 + "isUnique": false, 2448 + "concurrently": false, 2449 + "method": "btree", 2450 + "with": {} 2451 + }, 2452 + "workspace_member_userId_idx": { 2453 + "name": "workspace_member_userId_idx", 2454 + "columns": [ 2455 + { 2456 + "expression": "user_id", 2457 + "isExpression": false, 2458 + "asc": true, 2459 + "nulls": "last" 2460 + } 2461 + ], 2462 + "isUnique": false, 2463 + "concurrently": false, 2464 + "method": "btree", 2465 + "with": {} 2466 + } 2467 + }, 2468 + "foreignKeys": { 2469 + "workspace_member_workspace_id_workspace_id_fk": { 2470 + "name": "workspace_member_workspace_id_workspace_id_fk", 2471 + "tableFrom": "workspace_member", 2472 + "tableTo": "workspace", 2473 + "columnsFrom": ["workspace_id"], 2474 + "columnsTo": ["id"], 2475 + "onDelete": "cascade", 2476 + "onUpdate": "no action" 2477 + }, 2478 + "workspace_member_user_id_user_id_fk": { 2479 + "name": "workspace_member_user_id_user_id_fk", 2480 + "tableFrom": "workspace_member", 2481 + "tableTo": "user", 2482 + "columnsFrom": ["user_id"], 2483 + "columnsTo": ["id"], 2484 + "onDelete": "cascade", 2485 + "onUpdate": "no action" 2486 + } 2487 + }, 2488 + "compositePrimaryKeys": {}, 2489 + "uniqueConstraints": {}, 2490 + "policies": {}, 2491 + "checkConstraints": {}, 2492 + "isRLSEnabled": false 2493 + } 2494 + }, 2495 + "enums": {}, 2496 + "schemas": {}, 2497 + "sequences": {}, 2498 + "roles": {}, 2499 + "policies": {}, 2500 + "views": {}, 2501 + "_meta": { 2502 + "columns": {}, 2503 + "schemas": {}, 2504 + "tables": {} 2505 + } 2506 + }
+7
apps/api/drizzle/meta/_journal.json
··· 120 120 "when": 1773952461673, 121 121 "tag": "0016_add_task_relation", 122 122 "breakpoints": true 123 + }, 124 + { 125 + "idx": 17, 126 + "version": "7", 127 + "when": 1774269662424, 128 + "tag": "0017_white_omega_flight", 129 + "breakpoints": true 123 130 } 124 131 ] 125 132 }
+1
apps/api/src/database/schema.ts
··· 291 291 onUpdate: "cascade", 292 292 }), 293 293 priority: text("priority").default("low"), 294 + startDate: timestamp("start_date", { mode: "date" }), 294 295 dueDate: timestamp("due_date", { mode: "date" }), 295 296 createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(), 296 297 });
+1
apps/api/src/schemas.ts
··· 37 37 "high", 38 38 "urgent", 39 39 ] as const), 40 + startDate: v.optional(v.date()), 40 41 dueDate: v.optional(v.date()), 41 42 createdAt: v.date(), 42 43 });
+3
apps/api/src/task/controllers/create-task.ts
··· 10 10 userId, 11 11 title, 12 12 status, 13 + startDate, 13 14 dueDate, 14 15 description, 15 16 priority, ··· 18 19 userId?: string; 19 20 title: string; 20 21 status: string; 22 + startDate?: Date; 21 23 dueDate?: Date; 22 24 description?: string; 23 25 priority?: string; ··· 58 60 title: title || "", 59 61 status: status || "", 60 62 columnId: column?.id ?? null, 63 + startDate: startDate || null, 61 64 dueDate: dueDate || null, 62 65 description: description || "", 63 66 priority: priority || "",
+1
apps/api/src/task/controllers/get-task.ts
··· 12 12 description: taskTable.description, 13 13 status: taskTable.status, 14 14 priority: taskTable.priority, 15 + startDate: taskTable.startDate, 15 16 dueDate: taskTable.dueDate, 16 17 position: taskTable.position, 17 18 createdAt: taskTable.createdAt,
+1
apps/api/src/task/controllers/get-tasks.ts
··· 127 127 description: taskTable.description, 128 128 status: taskTable.status, 129 129 priority: taskTable.priority, 130 + startDate: taskTable.startDate, 130 131 dueDate: taskTable.dueDate, 131 132 position: taskTable.position, 132 133 createdAt: taskTable.createdAt,
+2
apps/api/src/task/controllers/import-tasks.ts
··· 10 10 description?: string; 11 11 status: string; 12 12 priority?: string; 13 + startDate?: string; 13 14 dueDate?: string; 14 15 userId?: string | null; 15 16 }; ··· 47 48 title: taskData.title, 48 49 status: taskData.status, 49 50 columnId: column?.id ?? null, 51 + startDate: taskData.startDate ? new Date(taskData.startDate) : null, 50 52 dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null, 51 53 description: taskData.description || "", 52 54 priority: taskData.priority || "low",
+2
apps/api/src/task/controllers/update-task.ts
··· 7 7 id: string, 8 8 title: string, 9 9 status: string, 10 + startDate: Date | undefined, 10 11 dueDate: Date | undefined, 11 12 projectId: string, 12 13 description: string, ··· 37 38 title, 38 39 status, 39 40 columnId: column?.id ?? null, 41 + startDate: startDate || null, 40 42 dueDate: dueDate || null, 41 43 projectId, 42 44 description,
+15 -2
apps/api/src/task/index.ts
··· 178 178 v.object({ 179 179 title: v.string(), 180 180 description: v.string(), 181 + startDate: v.optional(v.string()), 181 182 dueDate: v.optional(v.string()), 182 183 priority: v.string(), 183 184 status: v.string(), ··· 187 188 workspaceAccess.fromProject("projectId"), 188 189 async (c) => { 189 190 const { projectId } = c.req.param(); 190 - const { title, description, dueDate, priority, status, userId } = 191 - c.req.valid("json"); 191 + const { 192 + title, 193 + description, 194 + startDate, 195 + dueDate, 196 + priority, 197 + status, 198 + userId, 199 + } = c.req.valid("json"); 192 200 193 201 const task = await createTask({ 194 202 projectId, 195 203 userId, 196 204 title, 197 205 description, 206 + startDate: startDate ? new Date(startDate) : undefined, 198 207 dueDate: dueDate ? new Date(dueDate) : undefined, 199 208 priority, 200 209 status, ··· 249 258 v.object({ 250 259 title: v.string(), 251 260 description: v.string(), 261 + startDate: v.optional(v.string()), 252 262 dueDate: v.optional(v.string()), 253 263 priority: v.string(), 254 264 status: v.string(), ··· 266 276 const { 267 277 title, 268 278 description, 279 + startDate, 269 280 dueDate, 270 281 priority, 271 282 status, ··· 282 293 id, 283 294 title, 284 295 status, 296 + startDate ? new Date(startDate) : undefined, 285 297 dueDate ? new Date(dueDate) : undefined, 286 298 projectId, 287 299 description, ··· 357 369 description: v.optional(v.string()), 358 370 status: v.string(), 359 371 priority: v.optional(v.string()), 372 + startDate: v.optional(v.string()), 360 373 dueDate: v.optional(v.string()), 361 374 userId: v.optional(v.nullable(v.string())), 362 375 }),
+1
apps/web/src/components/backlog-list-view/index.tsx
··· 448 448 449 449 <CreateTaskModal 450 450 open={isTaskModalOpen} 451 + projectId={project?.id} 451 452 onClose={() => setIsTaskModalOpen(false)} 452 453 status={activeColumn ?? "planned"} 453 454 />
+5 -1
apps/web/src/components/command-palette/index.tsx
··· 1 - import { useNavigate } from "@tanstack/react-router"; 1 + import { useLocation, useNavigate } from "@tanstack/react-router"; 2 2 import { ArrowDownIcon, ArrowUpIcon, CornerDownLeftIcon } from "lucide-react"; 3 3 import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; 4 4 import SearchCommandMenu from "@/components/search-command-menu"; ··· 43 43 function CommandPalette() { 44 44 const { setTheme } = useUserPreferencesStore(); 45 45 const navigate = useNavigate(); 46 + const location = useLocation(); 46 47 const { data: workspace } = useActiveWorkspace(); 47 48 const [open, setOpen] = useState(false); 48 49 const [isSearchOpen, setIsSearchOpen] = useState(false); 49 50 const [isCreateTaskOpen, setIsCreateTaskOpen] = useState(false); 50 51 const [isCreateProjectOpen, setIsCreateProjectOpen] = useState(false); 51 52 const [isCreateWorkspaceOpen, setIsCreateWorkspaceOpen] = useState(false); 53 + const projectIdFromRoute = 54 + location.pathname.match(/\/project\/([^/]+)/)?.[1] ?? undefined; 52 55 53 56 useRegisterShortcuts({ 54 57 shortcuts: { ··· 310 313 <SearchCommandMenu open={isSearchOpen} setOpen={setIsSearchOpen} /> 311 314 <CreateTaskModal 312 315 open={isCreateTaskOpen} 316 + projectId={projectIdFromRoute} 313 317 onClose={() => setIsCreateTaskOpen(false)} 314 318 /> 315 319 <CreateWorkspaceModal
+21 -5
apps/web/src/components/common/header/mobile-project-nav.tsx
··· 1 - import { Check, Menu, Plus } from "lucide-react"; 1 + import { CalendarDays, Check, Menu, Plus, SquareKanban } from "lucide-react"; 2 2 import { Button } from "@/components/ui/button"; 3 3 import { 4 4 Popover, ··· 12 12 type MobileProjectNavProps = { 13 13 workspaceId: string; 14 14 projectId: string; 15 - activeView: "backlog" | "board"; 15 + activeView: "backlog" | "board" | "gantt"; 16 16 onSelectBoard: () => void; 17 17 onSelectBacklog: () => void; 18 + onSelectGantt: () => void; 18 19 onSelectProject: (projectId: string) => void; 19 20 onAddProject: () => void; 20 21 }; ··· 25 26 activeView, 26 27 onSelectBoard, 27 28 onSelectBacklog, 29 + onSelectGantt, 28 30 onSelectProject, 29 31 onAddProject, 30 32 }: MobileProjectNavProps) { ··· 49 51 <p className="px-1 text-[11px] font-medium tracking-wide text-muted-foreground uppercase"> 50 52 View 51 53 </p> 52 - <div className="grid grid-cols-2 gap-1"> 54 + <div className="grid grid-cols-3 gap-1"> 53 55 <button 54 56 type="button" 55 57 onClick={onSelectBacklog} 56 58 className={cn( 57 - "rounded-md border px-2 py-1.5 text-xs font-medium transition-colors", 59 + "flex w-full items-center justify-center gap-1 whitespace-nowrap rounded-md border px-2 py-1.5 text-xs font-medium transition-colors", 58 60 activeView === "backlog" 59 61 ? "border-border bg-secondary text-foreground" 60 62 : "border-transparent text-muted-foreground hover:bg-accent", ··· 66 68 type="button" 67 69 onClick={onSelectBoard} 68 70 className={cn( 69 - "rounded-md border px-2 py-1.5 text-xs font-medium transition-colors", 71 + "flex w-full items-center justify-center gap-1 whitespace-nowrap rounded-md border px-2 py-1.5 text-xs font-medium transition-colors", 70 72 activeView === "board" 71 73 ? "border-border bg-secondary text-foreground" 72 74 : "border-transparent text-muted-foreground hover:bg-accent", 73 75 )} 74 76 > 77 + <SquareKanban className="size-3.5" /> 75 78 Board 79 + </button> 80 + <button 81 + type="button" 82 + onClick={onSelectGantt} 83 + className={cn( 84 + "flex w-full items-center justify-center gap-1 whitespace-nowrap rounded-md border px-2 py-1.5 text-xs font-medium transition-colors", 85 + activeView === "gantt" 86 + ? "border-border bg-secondary text-foreground" 87 + : "border-transparent text-muted-foreground hover:bg-accent", 88 + )} 89 + > 90 + <CalendarDays className="size-3.5" /> 91 + Gantt 76 92 </button> 77 93 </div> 78 94 </div>
+33 -8
apps/web/src/components/common/project-layout.tsx
··· 1 1 import { useLocation, useNavigate } from "@tanstack/react-router"; 2 - import { SquareKanban, SquircleDashed } from "lucide-react"; 2 + import { CalendarDays, SquareKanban, SquircleDashed } from "lucide-react"; 3 3 import { type ReactNode, useState } from "react"; 4 4 import MobileProjectNav from "@/components/common/header/mobile-project-nav"; 5 5 import ProjectCrumbSelect from "@/components/common/header/project-crumb-select"; ··· 25 25 headerActions?: ReactNode; 26 26 children: ReactNode; 27 27 showViewSwitcher?: boolean; 28 - activeView?: "backlog" | "board"; 28 + activeView?: "backlog" | "board" | "gantt"; 29 29 }; 30 30 31 31 export default function ProjectLayout({ ··· 44 44 45 45 const resolvedView = 46 46 activeView ?? 47 - (location.pathname.includes("/backlog") ? "backlog" : "board"); 47 + (location.pathname.includes("/backlog") 48 + ? "backlog" 49 + : location.pathname.includes("/gantt") 50 + ? "gantt" 51 + : "board"); 48 52 49 53 const handleNavigateToBacklog = () => { 50 54 navigate({ ··· 60 64 }); 61 65 }; 62 66 63 - const handleProjectSwitch = (nextProjectId: string) => { 64 - const isBacklog = resolvedView === "backlog"; 67 + const handleNavigateToGantt = () => { 68 + navigate({ 69 + to: "/dashboard/workspace/$workspaceId/project/$projectId/gantt", 70 + params: { workspaceId, projectId }, 71 + }); 72 + }; 65 73 74 + const handleProjectSwitch = (nextProjectId: string) => { 66 75 navigate({ 67 - to: isBacklog 68 - ? "/dashboard/workspace/$workspaceId/project/$projectId/backlog" 69 - : "/dashboard/workspace/$workspaceId/project/$projectId/board", 76 + to: 77 + resolvedView === "backlog" 78 + ? "/dashboard/workspace/$workspaceId/project/$projectId/backlog" 79 + : resolvedView === "gantt" 80 + ? "/dashboard/workspace/$workspaceId/project/$projectId/gantt" 81 + : "/dashboard/workspace/$workspaceId/project/$projectId/board", 70 82 params: { 71 83 workspaceId, 72 84 projectId: nextProjectId, ··· 119 131 activeView={resolvedView} 120 132 onSelectBacklog={handleNavigateToBacklog} 121 133 onSelectBoard={handleNavigateToBoard} 134 + onSelectGantt={handleNavigateToGantt} 122 135 onSelectProject={handleProjectSwitch} 123 136 onAddProject={() => setIsCreateProjectModalOpen(true)} 124 137 /> ··· 149 162 > 150 163 <SquareKanban className="size-3.5" /> 151 164 Tasks 165 + </Button> 166 + <Button 167 + variant={resolvedView === "gantt" ? "secondary" : "ghost"} 168 + size="xs" 169 + onClick={handleNavigateToGantt} 170 + className={cn( 171 + "h-6 gap-1.5 rounded-md px-2 text-xs", 172 + resolvedView !== "gantt" && "text-muted-foreground", 173 + )} 174 + > 175 + <CalendarDays className="size-3.5" /> 176 + Gantt 152 177 </Button> 153 178 </div> 154 179 )}
+3
apps/web/src/components/kanban-board/column/index.tsx
··· 1 1 import { Plus } from "lucide-react"; 2 2 import { useState } from "react"; 3 3 import CreateTaskModal from "@/components/shared/modals/create-task-modal"; 4 + import useProjectStore from "@/store/project"; 4 5 import type { ProjectWithTasks } from "@/types/project"; 5 6 import { ColumnDropzone } from "./column-dropzone"; 6 7 import { ColumnHeader } from "./column-header"; ··· 12 13 function Column({ column }: ColumnProps) { 13 14 const [isTaskModalOpen, setIsTaskModalOpen] = useState(false); 14 15 const [isDropzoneOver, setIsDropzoneOver] = useState(false); 16 + const { project } = useProjectStore(); 15 17 16 18 return ( 17 19 <div ··· 24 26 <CreateTaskModal 25 27 open={isTaskModalOpen} 26 28 onClose={() => setIsTaskModalOpen(false)} 29 + projectId={project?.id} 27 30 status={column.id} 28 31 /> 29 32
+1
apps/web/src/components/list-view/index.tsx
··· 427 427 428 428 <CreateTaskModal 429 429 open={isTaskModalOpen} 430 + projectId={project.id} 430 431 onClose={() => setIsTaskModalOpen(false)} 431 432 status={activeColumn ?? "done"} 432 433 />
+1
apps/web/src/components/project/tasks-import-export.tsx
··· 190 190 "description": "Description text", 191 191 "status": "to-do", 192 192 "priority": "low", 193 + "startDate": "2025-04-18T00:00:00.000Z", 193 194 "dueDate": "2025-04-20T00:00:00.000Z", 194 195 "userId": "user@example.com" 195 196 }
+69 -7
apps/web/src/components/shared/modals/create-task-modal.tsx
··· 1 + import { useLocation } from "@tanstack/react-router"; 1 2 import { format } from "date-fns"; 2 3 import { produce } from "immer"; 3 4 import { ··· 51 52 open: boolean; 52 53 onClose: () => void; 53 54 status?: string; 55 + projectId?: string; 54 56 }; 55 57 56 58 type Priority = "no-priority" | "low" | "medium" | "high" | "urgent"; ··· 86 88 number: task.number ?? null, 87 89 description: task.description ?? null, 88 90 priority: task.priority ?? null, 91 + startDate: task.startDate ?? null, 89 92 dueDate: task.dueDate ?? null, 90 93 position: task.position ?? 0, 91 94 userId: task.userId ?? null, ··· 145 148 }, 146 149 ]; 147 150 148 - function CreateTaskModal({ open, onClose, status }: CreateTaskModalProps) { 151 + function CreateTaskModal({ 152 + open, 153 + onClose, 154 + status, 155 + projectId, 156 + }: CreateTaskModalProps) { 149 157 const { project, setProject } = useProjectStore(); 158 + const location = useLocation(); 150 159 const { data: workspace } = useActiveWorkspace(); 151 160 const { mutateAsync: createLabel } = useCreateLabel(); 152 161 const { data: workspaceLabels = [] } = useGetLabelsByWorkspace( ··· 157 166 const [description, setDescription] = useState(""); 158 167 const [priority, setPriority] = useState<Priority>("no-priority"); 159 168 const [assigneeId, setAssigneeId] = useState(""); 169 + const [startDate, setStartDate] = useState<Date | undefined>(undefined); 160 170 const [dueDate, setDueDate] = useState<Date | undefined>(undefined); 161 171 const [createMore, setCreateMore] = useState(false); 162 172 const [labels, setLabels] = useState<Label[]>([]); ··· 167 177 const [searchValue, setSearchValue] = useState(""); 168 178 const [selectedColor, setSelectedColor] = useState<LabelColor>("gray"); 169 179 const [newLabelName, setNewLabelName] = useState(""); 180 + 181 + const routeProjectId = 182 + location.pathname.match(/\/project\/([^/]+)/)?.[1] ?? null; 183 + const resolvedProjectId = projectId || project?.id || routeProjectId || ""; 170 184 171 185 const searchInputRef = useRef<HTMLInputElement>(null); 172 186 const draftCreationPromiseRef = useRef<Promise<Task> | null>(null); ··· 205 219 setDescription(""); 206 220 setPriority("no-priority"); 207 221 setAssigneeId(""); 222 + setStartDate(undefined); 208 223 setDueDate(undefined); 209 224 setCreateMore(false); 210 225 setLabels([]); ··· 287 302 return pendingTask.id; 288 303 } 289 304 290 - if (!project?.id) { 305 + if (!resolvedProjectId) { 291 306 toast.error("Choose a project before uploading images."); 292 307 return null; 293 308 } ··· 298 313 description: description.trim() || "", 299 314 userId: assigneeId, 300 315 priority, 301 - projectId: project.id, 316 + projectId: resolvedProjectId, 317 + startDate: startDate ? startDate.toISOString() : undefined, 302 318 dueDate: dueDate ? dueDate.toISOString() : undefined, 303 319 status: draftStatus, 304 320 }).then((task) => normalizeTask(task)); ··· 322 338 createTask, 323 339 description, 324 340 draftTask, 341 + startDate, 325 342 dueDate, 326 343 priority, 327 - project?.id, 344 + resolvedProjectId, 328 345 title, 329 346 ]); 330 347 331 348 const handleSubmit = async (e: React.FormEvent) => { 332 349 e.preventDefault(); 333 - if (!title.trim() || !project?.id || !workspace?.id) return; 350 + if (!title.trim() || !resolvedProjectId || !workspace?.id) return; 334 351 335 352 try { 336 353 const taskStatus = status ?? "to-do"; ··· 345 362 userId: assigneeId || null, 346 363 status: taskStatus, 347 364 priority, 365 + startDate: startDate ? startDate.toISOString() : null, 348 366 dueDate: dueDate ? dueDate.toISOString() : null, 349 - projectId: project.id, 367 + projectId: resolvedProjectId, 350 368 }), 351 369 ) 352 370 : normalizeTask( ··· 355 373 description: description.trim() || "", 356 374 userId: assigneeId, 357 375 priority, 358 - projectId: project.id, 376 + projectId: resolvedProjectId, 377 + startDate: startDate ? startDate.toISOString() : undefined, 359 378 dueDate: dueDate ? dueDate.toISOString() : undefined, 360 379 status: taskStatus, 361 380 }), ··· 385 404 setDescription(""); 386 405 setPriority("no-priority"); 387 406 setAssigneeId(""); 407 + setStartDate(undefined); 388 408 setDueDate(undefined); 389 409 setLabels([]); 390 410 setLabelsStep("select"); ··· 603 623 status.slice(1).replace("-", " ") 604 624 : "In Progress"} 605 625 </div> 626 + 627 + <Popover> 628 + <PopoverTrigger asChild> 629 + <button 630 + type="button" 631 + className={cn( 632 + "flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-md transition-colors border border-border hover:bg-accent/50", 633 + startDate 634 + ? "bg-accent/30 text-foreground" 635 + : "text-muted-foreground", 636 + )} 637 + > 638 + <CalendarIcon className="w-3.5 h-3.5" /> 639 + <span> 640 + {startDate 641 + ? format(startDate, "MMM d, yyyy") 642 + : "Start date"} 643 + </span> 644 + </button> 645 + </PopoverTrigger> 646 + <PopoverContent className="p-0" align="start"> 647 + <Calendar 648 + mode="single" 649 + selected={startDate} 650 + onSelect={setStartDate} 651 + className="w-full bg-popover" 652 + /> 653 + {startDate && ( 654 + <div className="p-2 border-t border-border"> 655 + <Button 656 + type="button" 657 + variant="outline" 658 + size="sm" 659 + className="w-full text-xs" 660 + onClick={() => setStartDate(undefined)} 661 + > 662 + Clear start date 663 + </Button> 664 + </div> 665 + )} 666 + </PopoverContent> 667 + </Popover> 606 668 607 669 <Popover> 608 670 <PopoverTrigger asChild>
+56
apps/web/src/components/task/task-properties-sidebar.tsx
··· 2 2 import { 3 3 Calendar, 4 4 CalendarClock, 5 + CalendarDays, 5 6 CalendarX, 6 7 Copy, 7 8 GitBranch, ··· 31 32 import TaskDueDatePopover from "./task-due-date-popover"; 32 33 import TaskLabelsPopover from "./task-labels-popover"; 33 34 import TaskPriorityPopover from "./task-priority-popover"; 35 + import TaskStartDatePopover from "./task-start-date-popover"; 34 36 import TaskStatusPopover from "./task-status-popover"; 35 37 36 38 function slugify(text: string | undefined): string { ··· 223 225 </TaskAssigneePopover> 224 226 )} 225 227 {task && ( 228 + <TaskStartDatePopover task={task}> 229 + <Button 230 + variant="ghost" 231 + size="sm" 232 + className="justify-start h-7 px-1.5 gap-1.5" 233 + > 234 + <CalendarDays className="w-3.5 h-3.5 text-muted-foreground" /> 235 + <span 236 + className={`text-xs font-semibold ${task.startDate ? "" : "text-muted-foreground"}`} 237 + > 238 + {task.startDate 239 + ? format(new Date(task.startDate), "MMM d") 240 + : "Start"} 241 + </span> 242 + </Button> 243 + </TaskStartDatePopover> 244 + )} 245 + {task && ( 226 246 <TaskDueDatePopover task={task}> 227 247 <Button 228 248 variant="ghost" ··· 379 399 </TaskAssigneePopover> 380 400 )} 381 401 {task && ( 402 + <TaskStartDatePopover task={task}> 403 + <Button 404 + variant="ghost" 405 + size="sm" 406 + className="justify-start h-7 px-1.5 gap-1.5" 407 + > 408 + <CalendarDays className="w-3.5 h-3.5 text-muted-foreground" /> 409 + <span 410 + className={`text-xs font-semibold ${task.startDate ? "" : "text-muted-foreground"}`} 411 + > 412 + {task.startDate 413 + ? format(new Date(task.startDate), "MMM d") 414 + : "Start"} 415 + </span> 416 + </Button> 417 + </TaskStartDatePopover> 418 + )} 419 + {task && ( 382 420 <TaskDueDatePopover task={task}> 383 421 <Button 384 422 variant="ghost" ··· 535 573 </span> 536 574 </Button> 537 575 </TaskAssigneePopover> 576 + )} 577 + {task && ( 578 + <TaskStartDatePopover task={task}> 579 + <Button 580 + variant="ghost" 581 + size="sm" 582 + className="justify-start h-7 px-1.5 gap-1.5 w-full" 583 + > 584 + <CalendarDays className="w-3.5 h-3.5 text-muted-foreground" /> 585 + <span 586 + className={`text-xs font-semibold ${task.startDate ? "" : "text-muted-foreground"}`} 587 + > 588 + {task.startDate 589 + ? format(new Date(task.startDate), "MMM d") 590 + : "Start date"} 591 + </span> 592 + </Button> 593 + </TaskStartDatePopover> 538 594 )} 539 595 {task && ( 540 596 <TaskDueDatePopover task={task}>
+69
apps/web/src/components/task/task-start-date-popover.tsx
··· 1 + import { X } from "lucide-react"; 2 + import { useState } from "react"; 3 + import { Button } from "@/components/ui/button"; 4 + import { Calendar } from "@/components/ui/calendar"; 5 + import { 6 + Popover, 7 + PopoverContent, 8 + PopoverTrigger, 9 + } from "@/components/ui/popover"; 10 + import { useUpdateTask } from "@/hooks/mutations/task/use-update-task"; 11 + import { toast } from "@/lib/toast"; 12 + import type Task from "@/types/task"; 13 + 14 + type TaskStartDatePopoverProps = { 15 + task: Task; 16 + children: React.ReactNode; 17 + }; 18 + 19 + export default function TaskStartDatePopover({ 20 + task, 21 + children, 22 + }: TaskStartDatePopoverProps) { 23 + const [open, setOpen] = useState(false); 24 + const { mutateAsync: updateTask } = useUpdateTask(); 25 + 26 + const handleDateChange = async (date: Date | undefined) => { 27 + try { 28 + await updateTask({ 29 + ...task, 30 + startDate: date?.toISOString() || null, 31 + }); 32 + toast.success("Task start date updated successfully"); 33 + setOpen(false); 34 + } catch (error) { 35 + toast.error( 36 + error instanceof Error 37 + ? error.message 38 + : "Failed to update task start date", 39 + ); 40 + } 41 + }; 42 + 43 + return ( 44 + <Popover open={open} onOpenChange={setOpen}> 45 + <PopoverTrigger asChild>{children}</PopoverTrigger> 46 + <PopoverContent className="p-0" align="start"> 47 + <Calendar 48 + mode="single" 49 + selected={task.startDate ? new Date(task.startDate) : undefined} 50 + onSelect={handleDateChange} 51 + className="w-full bg-popover" 52 + /> 53 + {task.startDate && ( 54 + <div className="p-0 border-t border-border"> 55 + <Button 56 + variant="ghost" 57 + size="sm" 58 + className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground rounded-none" 59 + onClick={() => handleDateChange(undefined)} 60 + > 61 + <X className="h-4 w-4" /> 62 + Clear start date 63 + </Button> 64 + </div> 65 + )} 66 + </PopoverContent> 67 + </Popover> 68 + ); 69 + }
+4 -1
apps/web/src/components/task/task-subtasks.tsx
··· 99 99 description: null, 100 100 status: subtask.task.status, 101 101 priority: subtask.task.priority, 102 + startDate: null, 102 103 dueDate: null, 103 104 position: null, 104 105 createdAt: "", ··· 119 120 120 121 const getAssignee = (userId: string | null) => { 121 122 if (!userId || !workspaceUsers?.members) return null; 122 - return workspaceUsers.members.find((member) => member.userId === userId); 123 + return ( 124 + workspaceUsers.members.find((member) => member.userId === userId) ?? null 125 + ); 123 126 }; 124 127 125 128 const getSelectionRadius = (index: number, isSelected: boolean) => {
+1
apps/web/src/constants/shortcuts.ts
··· 33 33 view: { 34 34 prefix: "v", 35 35 board: "b", 36 + gantt: "g", 36 37 list: "l", 37 38 backlog: "k", 38 39 },
+6
apps/web/src/fetchers/task/create-task.ts
··· 12 12 projectId: string, 13 13 userId: string, 14 14 status: string, 15 + startDate: Date | undefined, 15 16 dueDate: Date | undefined, 16 17 priority: string, 17 18 ) { 19 + if (!projectId) { 20 + throw new Error("No project selected for task creation"); 21 + } 22 + 18 23 const response = await client.task[":projectId"].$post({ 19 24 json: { 20 25 title, 21 26 description, 22 27 userId, 23 28 status, 29 + startDate: startDate?.toISOString() || undefined, 24 30 dueDate: dueDate?.toISOString() || undefined, 25 31 priority, 26 32 },
+1
apps/web/src/fetchers/task/import-tasks.ts
··· 5 5 description?: string; 6 6 status: string; 7 7 priority?: string; 8 + startDate?: string; 8 9 dueDate?: string; 9 10 userId?: string | null; 10 11 };
+1
apps/web/src/fetchers/task/update-task.ts
··· 10 10 description: task.description || "", 11 11 status: task.status, 12 12 priority: task.priority || "", 13 + startDate: task.startDate?.toString(), 13 14 dueDate: task.dueDate?.toString(), 14 15 position: task.position ?? 0, 15 16 projectId: task.projectId,
+2
apps/web/src/hooks/mutations/task/use-create-task.ts
··· 13 13 userId, 14 14 projectId, 15 15 status, 16 + startDate, 16 17 dueDate, 17 18 priority, 18 19 }: CreateTaskRequest) => ··· 22 23 projectId, 23 24 userId ?? "", 24 25 status, 26 + startDate ? new Date(startDate) : undefined, 25 27 dueDate ? new Date(dueDate) : undefined, 26 28 priority, 27 29 ),
+26
apps/web/src/routeTree.gen.ts
··· 44 44 import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdIntegrationsRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/integrations' 45 45 import { Route as LayoutAuthenticatedDashboardSettingsProjectsProjectIdGeneralRouteImport } from './routes/_layout/_authenticated/dashboard/settings/projects/$projectId/general' 46 46 import { Route as LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRouteImport } from './routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/index' 47 + import { Route as LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRouteImport } from './routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt' 47 48 import { Route as LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRouteImport } from './routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board' 48 49 import { Route as LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRouteImport } from './routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog' 49 50 import { Route as LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRouteImport } from './routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId_' ··· 257 258 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdRoute, 258 259 } as any, 259 260 ) 261 + const LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute = 262 + LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRouteImport.update( 263 + { 264 + id: '/project/$projectId/gantt', 265 + path: '/project/$projectId/gantt', 266 + getParentRoute: () => 267 + LayoutAuthenticatedDashboardWorkspaceWorkspaceIdRoute, 268 + } as any, 269 + ) 260 270 const LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute = 261 271 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRouteImport.update( 262 272 { ··· 320 330 '/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 321 331 '/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 322 332 '/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 333 + '/dashboard/workspace/$workspaceId/project/$projectId/gantt': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute 323 334 '/dashboard/workspace/$workspaceId/project/$projectId/': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute 324 335 '/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute 325 336 } ··· 356 367 '/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 357 368 '/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 358 369 '/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 370 + '/dashboard/workspace/$workspaceId/project/$projectId/gantt': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute 359 371 '/dashboard/workspace/$workspaceId/project/$projectId': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute 360 372 '/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute 361 373 } ··· 397 409 '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow': typeof LayoutAuthenticatedDashboardSettingsProjectsProjectIdWorkflowRoute 398 410 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 399 411 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 412 + '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute 400 413 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute 401 414 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId_': typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute 402 415 } ··· 437 450 | '/dashboard/settings/projects/$projectId/workflow' 438 451 | '/dashboard/workspace/$workspaceId/project/$projectId/backlog' 439 452 | '/dashboard/workspace/$workspaceId/project/$projectId/board' 453 + | '/dashboard/workspace/$workspaceId/project/$projectId/gantt' 440 454 | '/dashboard/workspace/$workspaceId/project/$projectId/' 441 455 | '/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId' 442 456 fileRoutesByTo: FileRoutesByTo ··· 473 487 | '/dashboard/settings/projects/$projectId/workflow' 474 488 | '/dashboard/workspace/$workspaceId/project/$projectId/backlog' 475 489 | '/dashboard/workspace/$workspaceId/project/$projectId/board' 490 + | '/dashboard/workspace/$workspaceId/project/$projectId/gantt' 476 491 | '/dashboard/workspace/$workspaceId/project/$projectId' 477 492 | '/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId' 478 493 id: ··· 513 528 | '/_layout/_authenticated/dashboard/settings/projects/$projectId/workflow' 514 529 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog' 515 530 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board' 531 + | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt' 516 532 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/' 517 533 | '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/task/$taskId_' 518 534 fileRoutesById: FileRoutesById ··· 773 789 preLoaderRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRouteImport 774 790 parentRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdRoute 775 791 } 792 + '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt': { 793 + id: '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt' 794 + path: '/project/$projectId/gantt' 795 + fullPath: '/dashboard/workspace/$workspaceId/project/$projectId/gantt' 796 + preLoaderRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRouteImport 797 + parentRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdRoute 798 + } 776 799 '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board': { 777 800 id: '/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board' 778 801 path: '/project/$projectId/board' ··· 884 907 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdIndexRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdIndexRoute 885 908 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute 886 909 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute 910 + LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute 887 911 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute 888 912 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute: typeof LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute 889 913 } ··· 900 924 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBacklogRoute, 901 925 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute: 902 926 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdBoardRoute, 927 + LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute: 928 + LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdGanttRoute, 903 929 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute: 904 930 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdIndexRoute, 905 931 LayoutAuthenticatedDashboardWorkspaceWorkspaceIdProjectProjectIdTaskTaskIdRoute:
+7
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/backlog.tsx
··· 93 93 params: { workspaceId, projectId }, 94 94 }); 95 95 }, 96 + [shortcuts.view.gantt]: () => { 97 + navigate({ 98 + to: "/dashboard/workspace/$workspaceId/project/$projectId/gantt", 99 + params: { workspaceId, projectId }, 100 + }); 101 + }, 96 102 [shortcuts.view.backlog]: () => {}, 97 103 }, 98 104 }, ··· 653 659 654 660 <CreateTaskModal 655 661 open={isTaskModalOpen} 662 + projectId={projectId} 656 663 onClose={() => setIsTaskModalOpen(false)} 657 664 status="planned" 658 665 />
+6
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/board.tsx
··· 67 67 [shortcuts.view.prefix]: { 68 68 [shortcuts.view.board]: () => setViewMode("board"), 69 69 [shortcuts.view.list]: () => setViewMode("list"), 70 + [shortcuts.view.gantt]: () => 71 + navigate({ 72 + to: "/dashboard/workspace/$workspaceId/project/$projectId/gantt", 73 + params: { workspaceId, projectId }, 74 + }), 70 75 [shortcuts.view.backlog]: () => 71 76 navigate({ 72 77 to: "/dashboard/workspace/$workspaceId/project/$projectId/backlog", ··· 210 215 211 216 <CreateTaskModal 212 217 open={isTaskModalOpen} 218 + projectId={projectId} 213 219 onClose={() => setIsTaskModalOpen(false)} 214 220 /> 215 221
+426
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt.tsx
··· 1 + import { createFileRoute, useNavigate } from "@tanstack/react-router"; 2 + import { 3 + differenceInCalendarDays, 4 + eachDayOfInterval, 5 + endOfWeek, 6 + format, 7 + isSameMonth, 8 + isToday, 9 + isWeekend, 10 + parseISO, 11 + startOfWeek, 12 + } from "date-fns"; 13 + import { ChevronLeft, ChevronRight, Search } from "lucide-react"; 14 + import { useEffect, useMemo, useState } from "react"; 15 + import ProjectLayout from "@/components/common/project-layout"; 16 + import PageTitle from "@/components/page-title"; 17 + import TaskDetailsSheet from "@/components/task/task-details-sheet"; 18 + import { Button } from "@/components/ui/button"; 19 + import { Input } from "@/components/ui/input"; 20 + import { useGetTasks } from "@/hooks/queries/task/use-get-tasks"; 21 + import { useIsMobile } from "@/hooks/use-mobile"; 22 + import { cn } from "@/lib/cn"; 23 + 24 + type GanttSearchParams = { 25 + taskId?: string; 26 + }; 27 + 28 + export const Route = createFileRoute( 29 + "/_layout/_authenticated/dashboard/workspace/$workspaceId/project/$projectId/gantt", 30 + )({ 31 + component: RouteComponent, 32 + validateSearch: (search: Record<string, unknown>): GanttSearchParams => ({ 33 + taskId: typeof search.taskId === "string" ? search.taskId : undefined, 34 + }), 35 + }); 36 + 37 + function toDisplayStatus(status: string) { 38 + return status 39 + .split("-") 40 + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) 41 + .join(" "); 42 + } 43 + 44 + function parseTaskDate(value: string | null) { 45 + if (!value) return null; 46 + const parsed = parseISO(value); 47 + return Number.isNaN(parsed.getTime()) ? null : parsed; 48 + } 49 + 50 + function RouteComponent() { 51 + const { projectId, workspaceId } = Route.useParams(); 52 + const { taskId } = Route.useSearch(); 53 + const navigate = useNavigate(); 54 + const { data: project } = useGetTasks(projectId); 55 + const [searchQuery, setSearchQuery] = useState(""); 56 + const isMobile = useIsMobile(); 57 + const [isTaskRailOpen, setIsTaskRailOpen] = useState(false); 58 + 59 + const dayColumnWidthRem = 2.75; 60 + const taskColumnWidthRem = 14; 61 + const showTaskRail = !isMobile || isTaskRailOpen; 62 + 63 + useEffect(() => { 64 + if (!isMobile) { 65 + setIsTaskRailOpen(true); 66 + return; 67 + } 68 + 69 + setIsTaskRailOpen(false); 70 + }, [isMobile]); 71 + 72 + const allTasks = useMemo( 73 + () => [ 74 + ...(project?.columns.flatMap((column) => column.tasks) ?? []), 75 + ...(project?.plannedTasks ?? []), 76 + ], 77 + [project], 78 + ); 79 + 80 + const parsedTasks = useMemo(() => { 81 + return allTasks 82 + .map((task) => { 83 + const parsedStart = 84 + parseTaskDate(task.startDate) ?? parseTaskDate(task.dueDate); 85 + const parsedEnd = 86 + parseTaskDate(task.dueDate) ?? parseTaskDate(task.startDate); 87 + 88 + if (!parsedStart || !parsedEnd) return null; 89 + 90 + const start = parsedStart <= parsedEnd ? parsedStart : parsedEnd; 91 + const end = parsedEnd >= parsedStart ? parsedEnd : parsedStart; 92 + 93 + return { 94 + ...task, 95 + scheduleStart: start, 96 + scheduleEnd: end, 97 + }; 98 + }) 99 + .filter((task): task is NonNullable<typeof task> => task !== null) 100 + .sort( 101 + (left, right) => 102 + left.scheduleStart.getTime() - right.scheduleStart.getTime(), 103 + ); 104 + }, [allTasks]); 105 + 106 + const scheduledTasks = useMemo(() => { 107 + const normalizedQuery = searchQuery.trim().toLowerCase(); 108 + if (!normalizedQuery) return parsedTasks; 109 + 110 + return parsedTasks.filter((task) => { 111 + return ( 112 + task.title.toLowerCase().includes(normalizedQuery) || 113 + `${project?.slug ?? ""}-${task.number ?? ""}` 114 + .toLowerCase() 115 + .includes(normalizedQuery) || 116 + task.status.toLowerCase().includes(normalizedQuery) 117 + ); 118 + }); 119 + }, [parsedTasks, project?.slug, searchQuery]); 120 + 121 + const timeline = useMemo(() => { 122 + if (parsedTasks.length === 0) return null; 123 + 124 + const earliest = parsedTasks.reduce( 125 + (current, task) => 126 + task.scheduleStart < current ? task.scheduleStart : current, 127 + parsedTasks[0].scheduleStart, 128 + ); 129 + const latest = parsedTasks.reduce( 130 + (current, task) => 131 + task.scheduleEnd > current ? task.scheduleEnd : current, 132 + parsedTasks[0].scheduleEnd, 133 + ); 134 + 135 + const rangeStart = startOfWeek(earliest, { weekStartsOn: 1 }); 136 + const rangeEnd = endOfWeek(latest, { weekStartsOn: 1 }); 137 + 138 + const days = eachDayOfInterval({ start: rangeStart, end: rangeEnd }); 139 + 140 + return { 141 + days, 142 + rangeStart, 143 + gridTemplateColumns: `repeat(${days.length}, minmax(${dayColumnWidthRem}rem, ${dayColumnWidthRem}rem))`, 144 + timelineMinWidthRem: days.length * dayColumnWidthRem, 145 + }; 146 + }, [parsedTasks]); 147 + 148 + return ( 149 + <ProjectLayout 150 + projectId={projectId} 151 + workspaceId={workspaceId} 152 + activeView="gantt" 153 + > 154 + <PageTitle title={`${project?.name} — Gantt`} hideAppName /> 155 + <div className="flex h-full min-h-0 flex-col bg-background"> 156 + <div className="border-b border-border/80 px-4 py-3"> 157 + <div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> 158 + <div className="space-y-1"> 159 + <h1 className="text-sm font-semibold text-foreground"> 160 + Gantt Timeline 161 + </h1> 162 + </div> 163 + 164 + <div className="relative w-full max-w-sm"> 165 + <Search className="pointer-events-none absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" /> 166 + <Input 167 + value={searchQuery} 168 + onChange={(event) => setSearchQuery(event.target.value)} 169 + placeholder="Search scheduled tickets..." 170 + className="h-8 [&_[data-slot=input]]:pl-8 [&_[data-slot=input]]:text-xs" 171 + /> 172 + </div> 173 + 174 + <Button 175 + variant="outline" 176 + size="xs" 177 + className="sm:hidden" 178 + onClick={() => setIsTaskRailOpen((current) => !current)} 179 + > 180 + {showTaskRail ? ( 181 + <ChevronLeft className="size-3.5" /> 182 + ) : ( 183 + <ChevronRight className="size-3.5" /> 184 + )} 185 + {showTaskRail ? "Hide tasks" : "Show tasks"} 186 + </Button> 187 + </div> 188 + </div> 189 + 190 + {!timeline || parsedTasks.length === 0 ? ( 191 + <div className="flex flex-1 items-center justify-center px-6"> 192 + <div className="max-w-sm text-center"> 193 + <h2 className="text-sm font-semibold text-foreground"> 194 + No scheduled tasks 195 + </h2> 196 + <p className="mt-1 text-sm text-muted-foreground"> 197 + Add a start date, due date, or both to tasks to place them on 198 + the project timeline. 199 + </p> 200 + </div> 201 + </div> 202 + ) : scheduledTasks.length === 0 ? ( 203 + <div className="flex flex-1 items-center justify-center px-6"> 204 + <div className="max-w-sm text-center"> 205 + <h2 className="text-sm font-semibold text-foreground"> 206 + No tasks found 207 + </h2> 208 + <p className="mt-1 text-sm text-muted-foreground"> 209 + No scheduled tasks match{" "} 210 + <span className="font-medium text-foreground"> 211 + "{searchQuery}" 212 + </span> 213 + </p> 214 + </div> 215 + </div> 216 + ) : ( 217 + <div className="min-h-0 flex-1 overflow-auto"> 218 + <div className="relative min-w-max"> 219 + <div className="sticky top-0 z-20 flex border-b border-border bg-background/95 backdrop-blur"> 220 + {showTaskRail ? ( 221 + <div 222 + className="sticky left-0 z-30 shrink-0 border-r border-border bg-background px-3 py-3 sm:w-80 sm:px-4" 223 + style={{ 224 + width: isMobile ? `${taskColumnWidthRem}rem` : undefined, 225 + }} 226 + > 227 + <p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground"> 228 + Task 229 + </p> 230 + </div> 231 + ) : null} 232 + <div 233 + className="grid shrink-0" 234 + style={{ 235 + gridTemplateColumns: timeline.gridTemplateColumns, 236 + minWidth: `${timeline.timelineMinWidthRem}rem`, 237 + }} 238 + > 239 + {timeline.days.map((day, index) => { 240 + const showMonth = 241 + index === 0 || 242 + !isSameMonth(day, timeline.days[index - 1] ?? day); 243 + 244 + return ( 245 + <div 246 + key={day.toISOString()} 247 + className={cn( 248 + "border-r border-border/70 px-1 py-2 text-center", 249 + isWeekend(day) && "bg-muted/25", 250 + )} 251 + > 252 + <div className="h-4 text-[10px] font-medium text-muted-foreground"> 253 + {showMonth ? format(day, "MMM") : ""} 254 + </div> 255 + <div 256 + className={cn( 257 + "mx-auto flex size-6 items-center justify-center rounded-full text-xs font-medium", 258 + isToday(day) && 259 + "bg-primary text-primary-foreground", 260 + )} 261 + > 262 + {format(day, "d")} 263 + </div> 264 + </div> 265 + ); 266 + })} 267 + </div> 268 + </div> 269 + 270 + <div className="relative"> 271 + <div 272 + className="absolute inset-y-0 z-0 grid" 273 + style={{ 274 + left: showTaskRail 275 + ? isMobile 276 + ? `${taskColumnWidthRem}rem` 277 + : "20rem" 278 + : "0rem", 279 + gridTemplateColumns: timeline.gridTemplateColumns, 280 + width: `${timeline.timelineMinWidthRem}rem`, 281 + }} 282 + > 283 + {timeline.days.map((day) => ( 284 + <div 285 + key={`bg-line-${day.toISOString()}`} 286 + className={cn( 287 + "h-full min-h-0 border-r border-border/60", 288 + isWeekend(day) && "bg-muted/25", 289 + )} 290 + /> 291 + ))} 292 + </div> 293 + 294 + <div className="relative z-10 flex flex-col"> 295 + {scheduledTasks.map((task) => { 296 + const startIndex = differenceInCalendarDays( 297 + task.scheduleStart, 298 + timeline.rangeStart, 299 + ); 300 + const endIndex = differenceInCalendarDays( 301 + task.scheduleEnd, 302 + timeline.rangeStart, 303 + ); 304 + const trackCount = timeline.days.length; 305 + const barInView = 306 + endIndex >= 0 && 307 + startIndex < trackCount && 308 + trackCount > 0; 309 + const lineStart = barInView 310 + ? Math.max(1, Math.min(startIndex + 1, trackCount)) 311 + : 1; 312 + const lineEnd = barInView 313 + ? Math.max( 314 + lineStart + 1, 315 + Math.min(endIndex + 2, trackCount + 1), 316 + ) 317 + : 1; 318 + 319 + return ( 320 + <div 321 + key={task.id} 322 + className="grid items-stretch border-b border-border/70" 323 + style={{ 324 + gridTemplateColumns: showTaskRail 325 + ? isMobile 326 + ? `${taskColumnWidthRem}rem max-content` 327 + : "20rem max-content" 328 + : "max-content", 329 + }} 330 + > 331 + {showTaskRail ? ( 332 + <div className="sticky left-0 z-[11] h-full border-r border-border bg-background"> 333 + <button 334 + type="button" 335 + className="flex h-full w-full min-w-0 flex-col items-start justify-center gap-0.5 px-3 py-1.5 text-left transition-colors hover:bg-muted" 336 + onClick={() => 337 + navigate({ 338 + to: ".", 339 + search: { taskId: task.id }, 340 + replace: true, 341 + }) 342 + } 343 + > 344 + <div className="flex w-full items-center gap-1.5"> 345 + <span className="max-w-[7rem] truncate rounded-full bg-secondary px-1.5 py-px text-[10px] font-medium uppercase tracking-wide text-secondary-foreground sm:max-w-none"> 346 + {toDisplayStatus(task.status)} 347 + </span> 348 + <span className="truncate text-[10px] text-muted-foreground"> 349 + {project?.slug}-{task.number} 350 + </span> 351 + </div> 352 + <p className="w-full line-clamp-1 text-xs font-medium leading-tight text-foreground"> 353 + {task.title} 354 + </p> 355 + <p className="w-full truncate text-[11px] leading-tight text-muted-foreground"> 356 + {format(task.scheduleStart, "MMM d")} -{" "} 357 + {format(task.scheduleEnd, "MMM d")} 358 + {task.assigneeName 359 + ? ` • ${task.assigneeName}` 360 + : ""} 361 + </p> 362 + </button> 363 + </div> 364 + ) : null} 365 + 366 + <div 367 + className="relative min-h-11 shrink-0" 368 + style={{ 369 + minWidth: `${timeline.timelineMinWidthRem}rem`, 370 + }} 371 + > 372 + {barInView && lineEnd > lineStart ? ( 373 + <div 374 + className="pointer-events-none absolute inset-0 z-[1] grid items-center" 375 + style={{ 376 + gridTemplateColumns: 377 + timeline.gridTemplateColumns, 378 + }} 379 + > 380 + <button 381 + type="button" 382 + style={{ 383 + gridColumn: `${lineStart} / ${lineEnd}`, 384 + }} 385 + className="group pointer-events-auto relative mx-1 flex h-11 min-w-0 items-center overflow-hidden rounded-md border border-primary/25 bg-background text-left text-sm font-medium leading-none text-foreground shadow-sm transition-colors hover:border-primary/40" 386 + onClick={() => 387 + navigate({ 388 + to: ".", 389 + search: { taskId: task.id }, 390 + replace: true, 391 + }) 392 + } 393 + > 394 + <div className="absolute inset-0 z-0 bg-primary/12 transition-colors group-hover:bg-primary/18" /> 395 + <span className="relative z-10 truncate px-3.5"> 396 + {task.title} 397 + </span> 398 + </button> 399 + </div> 400 + ) : null} 401 + </div> 402 + </div> 403 + ); 404 + })} 405 + </div> 406 + </div> 407 + </div> 408 + </div> 409 + )} 410 + 411 + <TaskDetailsSheet 412 + taskId={taskId} 413 + projectId={projectId} 414 + workspaceId={workspaceId} 415 + onClose={() => 416 + navigate({ 417 + to: ".", 418 + search: {}, 419 + replace: true, 420 + }) 421 + } 422 + /> 423 + </div> 424 + </ProjectLayout> 425 + ); 426 + }
+1
apps/web/src/types/task/index.ts
··· 22 22 description: string | null; 23 23 status: string; 24 24 priority: string | null; 25 + startDate: string | null; 25 26 dueDate: string | null; 26 27 position: number | null; 27 28 createdAt: string;