feat: archived boards (#386)
* init: schema migration, isArchived added to board table. refactor: board repo for isArchived filtering * init: archived, unarchived API procedures. refactor: all query * fix: migration error * feat: add tabbed navigation for boards view * Implemented a Listbox for mobile and a tabbed navigation for desktop to switch between "Boards" and "Archived" views. * Introduced state management for active tab selection. * Updated UI components to reflect the new navigation structure. * init: frontend/boards lists & tabs * chore: fixed font styling and spacing * init:boardDropdown / boardView. * chore:added translations * Remove .cursor plan file from repo * fix:build erros * revert: remove locales changes * fix:reverted changes under locales, replaced the archive and unarchive endpoints. Reorder migrations * fix:migration issue * fix: update journal.json --------- Co-authored-by: Henry <henry_ball@hotmail.co.uk>
This commit is contained in:
committed by
GitHub
parent
9cb1fb5218
commit
af36fb133a
@@ -19,6 +19,7 @@ interface UsePermissionsResult {
|
|||||||
canCreateBoard: boolean;
|
canCreateBoard: boolean;
|
||||||
canEditBoard: boolean;
|
canEditBoard: boolean;
|
||||||
canDeleteBoard: boolean;
|
canDeleteBoard: boolean;
|
||||||
|
canArchiveBoard: boolean;
|
||||||
canViewComment: boolean;
|
canViewComment: boolean;
|
||||||
canCreateComment: boolean;
|
canCreateComment: boolean;
|
||||||
canEditComment: boolean;
|
canEditComment: boolean;
|
||||||
@@ -51,6 +52,7 @@ export function usePermissions(): UsePermissionsResult {
|
|||||||
canCreateBoard: false,
|
canCreateBoard: false,
|
||||||
canEditBoard: false,
|
canEditBoard: false,
|
||||||
canDeleteBoard: false,
|
canDeleteBoard: false,
|
||||||
|
canArchiveBoard: false,
|
||||||
canViewComment: false,
|
canViewComment: false,
|
||||||
canCreateComment: false,
|
canCreateComment: false,
|
||||||
canEditComment: false,
|
canEditComment: false,
|
||||||
@@ -95,6 +97,7 @@ export function usePermissions(): UsePermissionsResult {
|
|||||||
canCreateBoard: hasPermission("board:create"),
|
canCreateBoard: hasPermission("board:create"),
|
||||||
canEditBoard: hasPermission("board:edit"),
|
canEditBoard: hasPermission("board:edit"),
|
||||||
canDeleteBoard: hasPermission("board:delete"),
|
canDeleteBoard: hasPermission("board:delete"),
|
||||||
|
canArchiveBoard: hasPermission("board:edit"),
|
||||||
canViewComment: hasPermission("comment:view"),
|
canViewComment: hasPermission("comment:view"),
|
||||||
canCreateComment: hasPermission("comment:create"),
|
canCreateComment: hasPermission("comment:create"),
|
||||||
canEditComment: hasPermission("comment:edit"),
|
canEditComment: hasPermission("comment:edit"),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ export const locales = [
|
|||||||
"nl",
|
"nl",
|
||||||
"ru",
|
"ru",
|
||||||
"pl",
|
"pl",
|
||||||
"pt-BR"
|
"ptbr"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Locale = (typeof locales)[number];
|
export type Locale = (typeof locales)[number];
|
||||||
@@ -23,5 +23,5 @@ export const localeNames: Record<Locale, string> = {
|
|||||||
nl: "Nederlands",
|
nl: "Nederlands",
|
||||||
ru: "Русский",
|
ru: "Русский",
|
||||||
pl: "Polski",
|
pl: "Polski",
|
||||||
"pt-BR": "Português",
|
ptbr: "Português",
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2899
apps/web/src/locales/ptbr/messages.po
Normal file
2899
apps/web/src/locales/ptbr/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
1
apps/web/src/locales/ptbr/messages.ts
Normal file
1
apps/web/src/locales/ptbr/messages.ts
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,7 +7,7 @@ import {
|
|||||||
HiOutlineStar,
|
HiOutlineStar,
|
||||||
HiStar,
|
HiStar,
|
||||||
} from "react-icons/hi2";
|
} from "react-icons/hi2";
|
||||||
|
import { IoArchiveOutline } from "react-icons/io5";
|
||||||
import Dropdown from "~/components/Dropdown";
|
import Dropdown from "~/components/Dropdown";
|
||||||
import { usePermissions } from "~/hooks/usePermissions";
|
import { usePermissions } from "~/hooks/usePermissions";
|
||||||
import { useModal } from "~/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
@@ -17,6 +17,7 @@ import { api } from "~/utils/api";
|
|||||||
export default function BoardDropdown({
|
export default function BoardDropdown({
|
||||||
isTemplate,
|
isTemplate,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isArchived,
|
||||||
boardPublicId,
|
boardPublicId,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
boardName,
|
boardName,
|
||||||
@@ -24,28 +25,29 @@ export default function BoardDropdown({
|
|||||||
isTemplate: boolean;
|
isTemplate: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
boardPublicId: string;
|
boardPublicId: string;
|
||||||
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
boardName?: string;
|
boardName?: string;
|
||||||
}) {
|
}) {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const { canEditBoard, canDeleteBoard, canCreateBoard } = usePermissions();
|
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
||||||
|
usePermissions();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const handleToggleFavorite = () => {
|
|
||||||
updateBoard.mutate({
|
|
||||||
boardPublicId,
|
|
||||||
favorite: !isFavorite,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBoard = api.board.update.useMutation({
|
const updateBoard = api.board.update.useMutation({
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
void utils.board.all.invalidate();
|
void utils.board.all.invalidate();
|
||||||
void utils.board.byId.invalidate();
|
void utils.board.byId.invalidate();
|
||||||
|
if (variables.isArchived !== undefined) {
|
||||||
// Show popup notification
|
showPopup({
|
||||||
if (variables.favorite !== undefined) {
|
header: variables.isArchived ? t`Board archived` : t`Board unarchived`,
|
||||||
|
message: variables.isArchived
|
||||||
|
? t`The board has been archived.`
|
||||||
|
: t`The board has been unarchived.`,
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
|
} else if (variables.favorite !== undefined) {
|
||||||
showPopup({
|
showPopup({
|
||||||
header: variables.favorite
|
header: variables.favorite
|
||||||
? t`Added to favorites`
|
? t`Added to favorites`
|
||||||
@@ -66,6 +68,22 @@ export default function BoardDropdown({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleToggleFavorite = () => {
|
||||||
|
updateBoard.mutate({
|
||||||
|
boardPublicId,
|
||||||
|
favorite: !isFavorite,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArchiveOrUnarchive = () => {
|
||||||
|
updateBoard.mutate({
|
||||||
|
boardPublicId,
|
||||||
|
isArchived: !isArchived,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isArchiveActionPending = updateBoard.isPending;
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
...(isTemplate && canCreateBoard
|
...(isTemplate && canCreateBoard
|
||||||
? [
|
? [
|
||||||
@@ -87,6 +105,17 @@ export default function BoardDropdown({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(!isTemplate && canArchiveBoard
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: isArchived ? t`Unarchive board` : t`Archive board`,
|
||||||
|
action: handleArchiveOrUnarchive,
|
||||||
|
icon: (
|
||||||
|
<IoArchiveOutline className="h-[16px] w-[16px] text-dark-900" />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: isFavorite
|
label: isFavorite
|
||||||
? t`Remove from favorites`
|
? t`Remove from favorites`
|
||||||
@@ -103,19 +132,23 @@ export default function BoardDropdown({
|
|||||||
{
|
{
|
||||||
label: isTemplate ? t`Delete template` : t`Delete board`,
|
label: isTemplate ? t`Delete template` : t`Delete board`,
|
||||||
action: () => openModal("DELETE_BOARD"),
|
action: () => openModal("DELETE_BOARD"),
|
||||||
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
icon: (
|
||||||
|
<HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown disabled={isLoading} items={items}>
|
<Dropdown
|
||||||
|
disabled={isLoading || isArchiveActionPending}
|
||||||
|
items={items}
|
||||||
|
>
|
||||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
isTemplate={!!isTemplate}
|
isTemplate={!!isTemplate}
|
||||||
isLoading={!boardData}
|
isLoading={!boardData}
|
||||||
boardPublicId={boardId ?? ""}
|
boardPublicId={boardId ?? ""}
|
||||||
workspacePublicId={workspace.publicId}
|
isArchived={boardData?.isArchived ?? false}
|
||||||
isFavorite={boardData?.favorite}
|
isFavorite={boardData?.favorite}
|
||||||
boardName={boardData?.name}
|
boardName={boardData?.name}
|
||||||
/>
|
/>
|
||||||
@@ -598,8 +598,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
? `/templates/${boardId}/cards/${card.publicId}`
|
? `/templates/${boardId}/cards/${card.publicId}`
|
||||||
: `/cards/${card.publicId}`
|
: `/cards/${card.publicId}`
|
||||||
}
|
}
|
||||||
className={`mb-2 flex !cursor-pointer flex-col ${
|
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
|
||||||
card.publicId.startsWith(
|
|
||||||
"PLACEHOLDER",
|
"PLACEHOLDER",
|
||||||
)
|
)
|
||||||
? "pointer-events-none"
|
? "pointer-events-none"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useModal } from "~/providers/modal";
|
|||||||
import { useWorkspace } from "~/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
export function BoardsList({ isTemplate, archived = false }: { isTemplate?: boolean; archived?: boolean }) {
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
const { canCreateBoard } = usePermissions();
|
const { canCreateBoard } = usePermissions();
|
||||||
@@ -26,6 +26,7 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
{
|
{
|
||||||
workspacePublicId: workspace.publicId,
|
workspacePublicId: workspace.publicId,
|
||||||
type: isTemplate ? "template" : "regular",
|
type: isTemplate ? "template" : "regular",
|
||||||
|
archived: archived,
|
||||||
},
|
},
|
||||||
{ enabled: workspace.publicId ? true : false },
|
{ enabled: workspace.publicId ? true : false },
|
||||||
);
|
);
|
||||||
@@ -59,10 +60,10 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<HiOutlineRectangleStack className="h-10 w-10 text-light-800 dark:text-dark-800" />
|
<HiOutlineRectangleStack className="h-10 w-10 text-light-800 dark:text-dark-800" />
|
||||||
<p className="mb-2 mt-4 text-[14px] font-bold text-light-1000 dark:text-dark-950">
|
<p className="mb-2 mt-4 text-[14px] font-bold text-light-1000 dark:text-dark-950">
|
||||||
{t`No ${isTemplate ? "templates" : "boards"}`}
|
{archived ? t`No archived boards` : t`No ${isTemplate ? "templates" : "boards"}`}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[14px] text-light-900 dark:text-dark-900">
|
<p className="text-[14px] text-light-900 dark:text-dark-900">
|
||||||
{t`Get started by creating a new ${isTemplate ? "template" : "board"}`}
|
{archived ? t`Boards you archive will appear here.` : t`Get started by creating a new ${isTemplate ? "template" : "board"}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from "@headlessui/react";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiArrowDownTray, HiChevronDown, HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
import FeedbackModal from "~/components/FeedbackModal";
|
import FeedbackModal from "~/components/FeedbackModal";
|
||||||
@@ -15,9 +22,15 @@ import { BoardsList } from "./components/BoardsList";
|
|||||||
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
||||||
import { NewBoardForm } from "./components/NewBoardForm";
|
import { NewBoardForm } from "./components/NewBoardForm";
|
||||||
|
|
||||||
|
const boardsTabs = [
|
||||||
|
{ key: "boards" as const, label: t`Active` },
|
||||||
|
{ key: "archived" as const, label: t`Archived` },
|
||||||
|
];
|
||||||
|
|
||||||
export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||||
const { openModal, modalContentType, isOpen } = useModal();
|
const { openModal, modalContentType, isOpen } = useModal();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
|
const [activeTab, setActiveTab] = useState<"boards" | "archived">("boards");
|
||||||
const { canCreateBoard } = usePermissions();
|
const { canCreateBoard } = usePermissions();
|
||||||
|
|
||||||
const { tooltipContent: createModalShortcutTooltipContent } =
|
const { tooltipContent: createModalShortcutTooltipContent } =
|
||||||
@@ -34,7 +47,7 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
<PageHead
|
<PageHead
|
||||||
title={t`${isTemplate ? "Templates" : "Boards"} | ${workspace.name ?? t`Workspace`}`}
|
title={t`${isTemplate ? "Templates" : "Boards"} | ${workspace.name ?? t`Workspace`}`}
|
||||||
/>
|
/>
|
||||||
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
|
<div className="m-auto h-full max-w-[1100px] p-8 px-5 md:px-28 md:py-12">
|
||||||
<div className="relative z-10 mb-8 flex w-full items-center justify-between">
|
<div className="relative z-10 mb-8 flex w-full items-center justify-between">
|
||||||
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||||
{t`${isTemplate ? "Templates" : "Boards"}`}
|
{t`${isTemplate ? "Templates" : "Boards"}`}
|
||||||
@@ -115,9 +128,79 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
{!isTemplate ? (
|
||||||
|
<div className="flex h-full w-full flex-col">
|
||||||
|
<div className="focus:outline-none">
|
||||||
|
<div className="sm:hidden">
|
||||||
|
<Listbox
|
||||||
|
value={activeTab}
|
||||||
|
onChange={(tab) => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<ListboxButton className="w-full appearance-none rounded-md border-0 bg-light-50 py-3 pl-3 pr-10 text-left text-sm font-semibold text-light-1000 shadow-sm ring-1 ring-inset ring-light-300 dark:bg-dark-50 dark:text-dark-1000 dark:ring-dark-300 dark:focus:ring-dark-500">
|
||||||
|
{boardsTabs.find((tab) => tab.key === activeTab)?.label ??
|
||||||
|
"Select a tab"}
|
||||||
|
<HiChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-light-900 dark:text-dark-900"
|
||||||
|
/>
|
||||||
|
</ListboxButton>
|
||||||
|
<ListboxOptions className="absolute z-10 mt-1 w-full rounded-md bg-light-50 py-1 text-sm shadow-lg ring-1 ring-inset ring-light-300 dark:bg-dark-50 dark:ring-dark-300">
|
||||||
|
{boardsTabs.map((tab) => (
|
||||||
|
<ListboxOption
|
||||||
|
key={tab.key}
|
||||||
|
value={tab.key}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`relative cursor-pointer select-none py-2 pl-3 pr-9 ${selected
|
||||||
|
? "font-bold text-light-1000 dark:text-dark-1000"
|
||||||
|
: "font-normal text-light-1000 dark:text-dark-1000"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</ListboxOption>
|
||||||
|
))}
|
||||||
|
</ListboxOptions>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<div>
|
||||||
|
<nav
|
||||||
|
aria-label="Tabs"
|
||||||
|
className="-mb-px flex space-x-8 focus:outline-none"
|
||||||
|
>
|
||||||
|
{boardsTabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab(tab.key)}
|
||||||
|
className={`whitespace-nowrap px-1 py-0 mt-2 mb-8 text-sm font-semibold transition-colors focus:outline-none ${activeTab === tab.key
|
||||||
|
? "border-light-1000 text-light-1000 dark:border-dark-1000 dark:text-dark-1000"
|
||||||
|
: "border-transparent text-light-900 hover:border-light-950 hover:text-light-950 dark:text-dark-900 dark:hover:border-white/20 dark:hover:text-dark-950"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full flex-row focus:outline-none">
|
||||||
|
{activeTab === "boards" && (
|
||||||
|
<BoardsList isTemplate={false} archived={false} />
|
||||||
|
)}
|
||||||
|
{activeTab === "archived" && (
|
||||||
|
<BoardsList isTemplate={false} archived={true} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex h-full flex-row">
|
<div className="flex h-full flex-row">
|
||||||
<BoardsList isTemplate={!!isTemplate} />
|
<BoardsList isTemplate={!!isTemplate} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
workspacePublicId: z.string().min(12),
|
workspacePublicId: z.string().min(12),
|
||||||
type: z.enum(["regular", "template"]).optional(),
|
type: z.enum(["regular", "template"]).optional(),
|
||||||
|
archived: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(
|
.output(
|
||||||
@@ -65,7 +66,10 @@ export const boardRouter = createTRPCRouter({
|
|||||||
ctx.db,
|
ctx.db,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
userId,
|
userId,
|
||||||
{ type: input.type }
|
{
|
||||||
|
type: input.type,
|
||||||
|
archived: input.archived ?? false,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -461,7 +465,8 @@ export const boardRouter = createTRPCRouter({
|
|||||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
|
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
|
||||||
.optional(),
|
.optional(),
|
||||||
visibility: z.enum(["public", "private"]).optional(),
|
visibility: z.enum(["public", "private"]).optional(),
|
||||||
favorite: z.boolean().optional()
|
favorite: z.boolean().optional(),
|
||||||
|
isArchived: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
|
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
|
||||||
@@ -503,7 +508,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle other updates (name, slug, visibility)
|
// Handle other updates (name, slug, visibility)
|
||||||
const hasOtherUpdates = input.name || input.slug || input.visibility !== undefined;
|
const hasOtherUpdates = input.name || input.slug || input.visibility !== undefined || input.isArchived !== undefined;
|
||||||
|
|
||||||
if (!hasOtherUpdates) {
|
if (!hasOtherUpdates) {
|
||||||
// Only favorite was updated, return success
|
// Only favorite was updated, return success
|
||||||
@@ -530,6 +535,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
boardPublicId: input.boardPublicId,
|
boardPublicId: input.boardPublicId,
|
||||||
visibility: input.visibility,
|
visibility: input.visibility,
|
||||||
|
isArchived: input.isArchived,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "board" ADD COLUMN "isArchived" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
|
CREATE INDEX IF NOT EXISTS "board_is_archived_idx" ON "board" USING btree ("isArchived");
|
||||||
3709
packages/db/migrations/meta/20260225195947_snapshot.json
Normal file
3709
packages/db/migrations/meta/20260225195947_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -187,15 +187,15 @@
|
|||||||
{
|
{
|
||||||
"idx": 26,
|
"idx": 26,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1770521594167,
|
"when": 1771192000000,
|
||||||
"tag": "20260208033314_AddAttachmentToActivity",
|
"tag": "20260129210000_AddWorkspaceWebhooks",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 27,
|
"idx": 27,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1771192000000,
|
"when": 1772049587901,
|
||||||
"tag": "20260129210000_AddWorkspaceWebhooks",
|
"tag": "20260225195947_AddIsArchivedToBoard",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const getAllByWorkspaceId = async (
|
|||||||
db: dbClient,
|
db: dbClient,
|
||||||
workspaceId: number,
|
workspaceId: number,
|
||||||
userId: string,
|
userId: string,
|
||||||
opts?: { type?: "regular" | "template" },
|
opts?: { type?: "regular" | "template"; archived?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const boardsData = await db.query.boards.findMany({
|
const boardsData = await db.query.boards.findMany({
|
||||||
columns: {
|
columns: {
|
||||||
@@ -78,6 +78,7 @@ export const getAllByWorkspaceId = async (
|
|||||||
eq(boards.workspaceId, workspaceId),
|
eq(boards.workspaceId, workspaceId),
|
||||||
isNull(boards.deletedAt),
|
isNull(boards.deletedAt),
|
||||||
opts?.type ? eq(boards.type, opts.type) : undefined,
|
opts?.type ? eq(boards.type, opts.type) : undefined,
|
||||||
|
opts?.archived !== undefined ? eq(boards.isArchived, opts.archived) : undefined,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ export const getIdByPublicId = async (db: dbClient, boardPublicId: string) => {
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
type: true,
|
type: true,
|
||||||
|
isArchived: true,
|
||||||
},
|
},
|
||||||
where: eq(boards.publicId, boardPublicId),
|
where: eq(boards.publicId, boardPublicId),
|
||||||
});
|
});
|
||||||
@@ -195,6 +197,7 @@ export const getByPublicId = async (
|
|||||||
name: true,
|
name: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
visibility: true,
|
visibility: true,
|
||||||
|
isArchived: true,
|
||||||
},
|
},
|
||||||
with: {
|
with: {
|
||||||
userFavorites: {
|
userFavorites: {
|
||||||
@@ -628,6 +631,7 @@ export const update = async (
|
|||||||
slug: string | undefined;
|
slug: string | undefined;
|
||||||
visibility: BoardVisibilityStatus | undefined;
|
visibility: BoardVisibilityStatus | undefined;
|
||||||
boardPublicId: string;
|
boardPublicId: string;
|
||||||
|
isArchived?: boolean;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
@@ -637,6 +641,7 @@ export const update = async (
|
|||||||
slug: boardInput.slug,
|
slug: boardInput.slug,
|
||||||
visibility: boardInput.visibility,
|
visibility: boardInput.visibility,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
...(boardInput.isArchived !== undefined && { isArchived: boardInput.isArchived })
|
||||||
})
|
})
|
||||||
.where(eq(boards.publicId, boardInput.boardPublicId))
|
.where(eq(boards.publicId, boardInput.boardPublicId))
|
||||||
.returning({
|
.returning({
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const SYSTEM_ROLES: {
|
|||||||
description: "View-only access",
|
description: "View-only access",
|
||||||
hierarchyLevel: 10,
|
hierarchyLevel: 10,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getCount = async (db: dbClient) => {
|
export const getCount = async (db: dbClient) => {
|
||||||
const result = await db
|
const result = await db
|
||||||
@@ -250,7 +250,7 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
|
|||||||
slug: true,
|
slug: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public")),
|
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public"), eq(boards.isArchived, false)),
|
||||||
orderBy: [asc(boards.name)]
|
orderBy: [asc(boards.name)]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { relations, sql } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
bigint,
|
bigint,
|
||||||
bigserial,
|
bigserial,
|
||||||
boolean,
|
|
||||||
index,
|
index,
|
||||||
pgEnum,
|
pgEnum,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -12,8 +11,8 @@ import {
|
|||||||
uniqueIndex,
|
uniqueIndex,
|
||||||
uuid,
|
uuid,
|
||||||
varchar,
|
varchar,
|
||||||
|
boolean,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import { imports } from "./imports";
|
import { imports } from "./imports";
|
||||||
import { labels } from "./labels";
|
import { labels } from "./labels";
|
||||||
import { lists } from "./lists";
|
import { lists } from "./lists";
|
||||||
@@ -56,9 +55,11 @@ export const boards = pgTable(
|
|||||||
.references(() => workspaces.id, { onDelete: "cascade" }),
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||||
visibility: boardVisibilityEnum("visibility").notNull().default("private"),
|
visibility: boardVisibilityEnum("visibility").notNull().default("private"),
|
||||||
type: boardTypeEnum("type").notNull().default("regular"),
|
type: boardTypeEnum("type").notNull().default("regular"),
|
||||||
|
isArchived: boolean("isArchived").notNull().default(false),
|
||||||
sourceBoardId: bigint("sourceBoardId", { mode: "number" }),
|
sourceBoardId: bigint("sourceBoardId", { mode: "number" }),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
|
index("board_is_archived_idx").on(table.isArchived),
|
||||||
index("board_visibility_idx").on(table.visibility),
|
index("board_visibility_idx").on(table.visibility),
|
||||||
index("board_type_idx").on(table.type),
|
index("board_type_idx").on(table.type),
|
||||||
index("board_source_idx").on(table.sourceBoardId),
|
index("board_source_idx").on(table.sourceBoardId),
|
||||||
|
|||||||
Reference in New Issue
Block a user