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;
|
||||
canEditBoard: boolean;
|
||||
canDeleteBoard: boolean;
|
||||
canArchiveBoard: boolean;
|
||||
canViewComment: boolean;
|
||||
canCreateComment: boolean;
|
||||
canEditComment: boolean;
|
||||
@@ -33,7 +34,7 @@ interface UsePermissionsResult {
|
||||
export function usePermissions(): UsePermissionsResult {
|
||||
// Check if WorkspaceProvider is available (for public board views, it may not be)
|
||||
const workspaceContext = useContext(WorkspaceContext);
|
||||
|
||||
|
||||
// If WorkspaceProvider is not available, return safe defaults
|
||||
if (!workspaceContext) {
|
||||
const emptyPermissions: UsePermissionsResult = {
|
||||
@@ -51,6 +52,7 @@ export function usePermissions(): UsePermissionsResult {
|
||||
canCreateBoard: false,
|
||||
canEditBoard: false,
|
||||
canDeleteBoard: false,
|
||||
canArchiveBoard: false,
|
||||
canViewComment: false,
|
||||
canCreateComment: false,
|
||||
canEditComment: false,
|
||||
@@ -95,6 +97,7 @@ export function usePermissions(): UsePermissionsResult {
|
||||
canCreateBoard: hasPermission("board:create"),
|
||||
canEditBoard: hasPermission("board:edit"),
|
||||
canDeleteBoard: hasPermission("board:delete"),
|
||||
canArchiveBoard: hasPermission("board:edit"),
|
||||
canViewComment: hasPermission("comment:view"),
|
||||
canCreateComment: hasPermission("comment:create"),
|
||||
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",
|
||||
"ru",
|
||||
"pl",
|
||||
"pt-BR"
|
||||
"ptbr"
|
||||
] as const;
|
||||
|
||||
export type Locale = (typeof locales)[number];
|
||||
@@ -23,5 +23,5 @@ export const localeNames: Record<Locale, string> = {
|
||||
nl: "Nederlands",
|
||||
ru: "Русский",
|
||||
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,
|
||||
HiStar,
|
||||
} from "react-icons/hi2";
|
||||
|
||||
import { IoArchiveOutline } from "react-icons/io5";
|
||||
import Dropdown from "~/components/Dropdown";
|
||||
import { usePermissions } from "~/hooks/usePermissions";
|
||||
import { useModal } from "~/providers/modal";
|
||||
@@ -17,6 +17,7 @@ import { api } from "~/utils/api";
|
||||
export default function BoardDropdown({
|
||||
isTemplate,
|
||||
isLoading,
|
||||
isArchived,
|
||||
boardPublicId,
|
||||
isFavorite,
|
||||
boardName,
|
||||
@@ -24,28 +25,29 @@ export default function BoardDropdown({
|
||||
isTemplate: boolean;
|
||||
isLoading: boolean;
|
||||
boardPublicId: string;
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
boardName?: string;
|
||||
}) {
|
||||
const { openModal } = useModal();
|
||||
const { canEditBoard, canDeleteBoard, canCreateBoard } = usePermissions();
|
||||
const { showPopup } = usePopup();
|
||||
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
|
||||
usePermissions();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
updateBoard.mutate({
|
||||
boardPublicId,
|
||||
favorite: !isFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
const updateBoard = api.board.update.useMutation({
|
||||
onSuccess: (data, variables) => {
|
||||
onSuccess: (_data, variables) => {
|
||||
void utils.board.all.invalidate();
|
||||
void utils.board.byId.invalidate();
|
||||
|
||||
// Show popup notification
|
||||
if (variables.favorite !== undefined) {
|
||||
if (variables.isArchived !== undefined) {
|
||||
showPopup({
|
||||
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({
|
||||
header: variables.favorite
|
||||
? t`Added to favorites`
|
||||
@@ -65,27 +67,54 @@ export default function BoardDropdown({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
updateBoard.mutate({
|
||||
boardPublicId,
|
||||
favorite: !isFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
const handleArchiveOrUnarchive = () => {
|
||||
updateBoard.mutate({
|
||||
boardPublicId,
|
||||
isArchived: !isArchived,
|
||||
});
|
||||
};
|
||||
|
||||
const isArchiveActionPending = updateBoard.isPending;
|
||||
|
||||
const items = [
|
||||
...(isTemplate && canCreateBoard
|
||||
? [
|
||||
{
|
||||
label: t`Make template`,
|
||||
action: () => openModal("CREATE_TEMPLATE"),
|
||||
icon: (
|
||||
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
]
|
||||
{
|
||||
label: t`Make template`,
|
||||
action: () => openModal("CREATE_TEMPLATE"),
|
||||
icon: (
|
||||
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isTemplate && canEditBoard
|
||||
? [
|
||||
{
|
||||
label: t`Edit board URL`,
|
||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]
|
||||
{
|
||||
label: t`Edit board URL`,
|
||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isTemplate && canArchiveBoard
|
||||
? [
|
||||
{
|
||||
label: isArchived ? t`Unarchive board` : t`Archive board`,
|
||||
action: handleArchiveOrUnarchive,
|
||||
icon: (
|
||||
<IoArchiveOutline className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: isFavorite
|
||||
@@ -100,22 +129,26 @@ export default function BoardDropdown({
|
||||
},
|
||||
...(canDeleteBoard
|
||||
? [
|
||||
{
|
||||
label: isTemplate ? t`Delete template` : t`Delete board`,
|
||||
action: () => openModal("DELETE_BOARD"),
|
||||
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]
|
||||
{
|
||||
label: isTemplate ? t`Delete template` : t`Delete board`,
|
||||
action: () => openModal("DELETE_BOARD"),
|
||||
icon: (
|
||||
<HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown disabled={isLoading} items={items}>
|
||||
<Dropdown
|
||||
disabled={isLoading || isArchiveActionPending}
|
||||
items={items}
|
||||
>
|
||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -494,7 +494,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
isTemplate={!!isTemplate}
|
||||
isLoading={!boardData}
|
||||
boardPublicId={boardId ?? ""}
|
||||
workspacePublicId={workspace.publicId}
|
||||
isArchived={boardData?.isArchived ?? false}
|
||||
isFavorite={boardData?.favorite}
|
||||
boardName={boardData?.name}
|
||||
/>
|
||||
@@ -598,13 +598,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
? `/templates/${boardId}/cards/${card.publicId}`
|
||||
: `/cards/${card.publicId}`
|
||||
}
|
||||
className={`mb-2 flex !cursor-pointer flex-col ${
|
||||
card.publicId.startsWith(
|
||||
"PLACEHOLDER",
|
||||
)
|
||||
? "pointer-events-none"
|
||||
: ""
|
||||
}`}
|
||||
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
|
||||
"PLACEHOLDER",
|
||||
)
|
||||
? "pointer-events-none"
|
||||
: ""
|
||||
}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
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 { openModal } = useModal();
|
||||
const { canCreateBoard } = usePermissions();
|
||||
@@ -26,6 +26,7 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
{
|
||||
workspacePublicId: workspace.publicId,
|
||||
type: isTemplate ? "template" : "regular",
|
||||
archived: archived,
|
||||
},
|
||||
{ enabled: workspace.publicId ? true : false },
|
||||
);
|
||||
@@ -59,10 +60,10 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
<div className="flex flex-col items-center">
|
||||
<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">
|
||||
{t`No ${isTemplate ? "templates" : "boards"}`}
|
||||
{archived ? t`No archived boards` : t`No ${isTemplate ? "templates" : "boards"}`}
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
<Tooltip
|
||||
@@ -109,18 +110,18 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
|
||||
>
|
||||
<div className="group relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
||||
<PatternedBackground />
|
||||
<button
|
||||
onClick={(e) => handleToggleFavorite(e, board.publicId, board.favorite)}
|
||||
className={`absolute right-3 top-3 z-10 rounded p-1 transition-all hover:bg-light-300 dark:hover:bg-dark-200 ${board.favorite ? "" : "md:opacity-0 md:group-hover:opacity-100"
|
||||
}`}
|
||||
aria-label={board.favorite ? "Remove from favorites" : "Add to favorites"}
|
||||
>
|
||||
{board.favorite ? (
|
||||
<HiStar className="h-5 w-5 text-neutral-700 dark:text-dark-1000" />
|
||||
) : (
|
||||
<HiOutlineStar className="h-5 w-5 text-neutral-700 dark:text-dark-800" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleToggleFavorite(e, board.publicId, board.favorite)}
|
||||
className={`absolute right-3 top-3 z-10 rounded p-1 transition-all hover:bg-light-300 dark:hover:bg-dark-200 ${board.favorite ? "" : "md:opacity-0 md:group-hover:opacity-100"
|
||||
}`}
|
||||
aria-label={board.favorite ? "Remove from favorites" : "Add to favorites"}
|
||||
>
|
||||
{board.favorite ? (
|
||||
<HiStar className="h-5 w-5 text-neutral-700 dark:text-dark-1000" />
|
||||
) : (
|
||||
<HiOutlineStar className="h-5 w-5 text-neutral-700 dark:text-dark-800" />
|
||||
)}
|
||||
</button>
|
||||
<p className="px-4 text-[14px] font-bold text-neutral-700 dark:text-dark-1000">
|
||||
{board.name}
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/react";
|
||||
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 FeedbackModal from "~/components/FeedbackModal";
|
||||
@@ -15,9 +22,15 @@ import { BoardsList } from "./components/BoardsList";
|
||||
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
||||
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 }) {
|
||||
const { openModal, modalContentType, isOpen } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
const [activeTab, setActiveTab] = useState<"boards" | "archived">("boards");
|
||||
const { canCreateBoard } = usePermissions();
|
||||
|
||||
const { tooltipContent: createModalShortcutTooltipContent } =
|
||||
@@ -34,7 +47,7 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
<PageHead
|
||||
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">
|
||||
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||
{t`${isTemplate ? "Templates" : "Boards"}`}
|
||||
@@ -115,9 +128,79 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
<div className="flex h-full flex-row">
|
||||
<BoardsList isTemplate={!!isTemplate} />
|
||||
</div>
|
||||
{!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">
|
||||
<BoardsList isTemplate={!!isTemplate} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ export const boardRouter = createTRPCRouter({
|
||||
z.object({
|
||||
workspacePublicId: z.string().min(12),
|
||||
type: z.enum(["regular", "template"]).optional(),
|
||||
archived: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
@@ -65,7 +66,10 @@ export const boardRouter = createTRPCRouter({
|
||||
ctx.db,
|
||||
workspace.id,
|
||||
userId,
|
||||
{ type: input.type }
|
||||
{
|
||||
type: input.type,
|
||||
archived: input.archived ?? false,
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -153,24 +157,24 @@ export const boardRouter = createTRPCRouter({
|
||||
// Generate presigned URLs for workspace member avatars
|
||||
const workspaceWithAvatarUrls = result.workspace
|
||||
? {
|
||||
...result.workspace,
|
||||
members: await Promise.all(
|
||||
result.workspace.members.map(async (member) => {
|
||||
if (!member.user?.image) {
|
||||
return member;
|
||||
}
|
||||
...result.workspace,
|
||||
members: await Promise.all(
|
||||
result.workspace.members.map(async (member) => {
|
||||
if (!member.user?.image) {
|
||||
return member;
|
||||
}
|
||||
|
||||
const avatarUrl = await generateAvatarUrl(member.user.image);
|
||||
return {
|
||||
...member,
|
||||
user: {
|
||||
...member.user,
|
||||
image: avatarUrl,
|
||||
},
|
||||
};
|
||||
}),
|
||||
),
|
||||
}
|
||||
const avatarUrl = await generateAvatarUrl(member.user.image);
|
||||
return {
|
||||
...member,
|
||||
user: {
|
||||
...member.user,
|
||||
image: avatarUrl,
|
||||
},
|
||||
};
|
||||
}),
|
||||
),
|
||||
}
|
||||
: result.workspace;
|
||||
|
||||
// Generate presigned URLs for card member avatars
|
||||
@@ -461,7 +465,8 @@ export const boardRouter = createTRPCRouter({
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
|
||||
.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>>>()))
|
||||
@@ -503,7 +508,7 @@ export const boardRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Only favorite was updated, return success
|
||||
@@ -530,6 +535,7 @@ export const boardRouter = createTRPCRouter({
|
||||
slug: input.slug,
|
||||
boardPublicId: input.boardPublicId,
|
||||
visibility: input.visibility,
|
||||
isArchived: input.isArchived,
|
||||
});
|
||||
|
||||
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,16 +187,16 @@
|
||||
{
|
||||
"idx": 26,
|
||||
"version": "7",
|
||||
"when": 1770521594167,
|
||||
"tag": "20260208033314_AddAttachmentToActivity",
|
||||
"when": 1771192000000,
|
||||
"tag": "20260129210000_AddWorkspaceWebhooks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1771192000000,
|
||||
"tag": "20260129210000_AddWorkspaceWebhooks",
|
||||
"when": 1772049587901,
|
||||
"tag": "20260225195947_AddIsArchivedToBoard",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const getAllByWorkspaceId = async (
|
||||
db: dbClient,
|
||||
workspaceId: number,
|
||||
userId: string,
|
||||
opts?: { type?: "regular" | "template" },
|
||||
opts?: { type?: "regular" | "template"; archived?: boolean },
|
||||
) => {
|
||||
const boardsData = await db.query.boards.findMany({
|
||||
columns: {
|
||||
@@ -78,6 +78,7 @@ export const getAllByWorkspaceId = async (
|
||||
eq(boards.workspaceId, workspaceId),
|
||||
isNull(boards.deletedAt),
|
||||
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: {
|
||||
id: true,
|
||||
type: true,
|
||||
isArchived: true,
|
||||
},
|
||||
where: eq(boards.publicId, boardPublicId),
|
||||
});
|
||||
@@ -195,6 +197,7 @@ export const getByPublicId = async (
|
||||
name: true,
|
||||
slug: true,
|
||||
visibility: true,
|
||||
isArchived: true,
|
||||
},
|
||||
with: {
|
||||
userFavorites: {
|
||||
@@ -628,6 +631,7 @@ export const update = async (
|
||||
slug: string | undefined;
|
||||
visibility: BoardVisibilityStatus | undefined;
|
||||
boardPublicId: string;
|
||||
isArchived?: boolean;
|
||||
},
|
||||
) => {
|
||||
const [result] = await db
|
||||
@@ -637,6 +641,7 @@ export const update = async (
|
||||
slug: boardInput.slug,
|
||||
visibility: boardInput.visibility,
|
||||
updatedAt: new Date(),
|
||||
...(boardInput.isArchived !== undefined && { isArchived: boardInput.isArchived })
|
||||
})
|
||||
.where(eq(boards.publicId, boardInput.boardPublicId))
|
||||
.returning({
|
||||
|
||||
@@ -30,22 +30,22 @@ const SYSTEM_ROLES: {
|
||||
description: string;
|
||||
hierarchyLevel: number;
|
||||
}[] = [
|
||||
{
|
||||
name: "admin",
|
||||
description: "Full access to all workspace features",
|
||||
hierarchyLevel: 100,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description: "Standard member with create and edit permissions",
|
||||
hierarchyLevel: 50,
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
description: "View-only access",
|
||||
hierarchyLevel: 10,
|
||||
},
|
||||
];
|
||||
{
|
||||
name: "admin",
|
||||
description: "Full access to all workspace features",
|
||||
hierarchyLevel: 100,
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
description: "Standard member with create and edit permissions",
|
||||
hierarchyLevel: 50,
|
||||
},
|
||||
{
|
||||
name: "guest",
|
||||
description: "View-only access",
|
||||
hierarchyLevel: 10,
|
||||
},
|
||||
];
|
||||
|
||||
export const getCount = async (db: dbClient) => {
|
||||
const result = await db
|
||||
@@ -250,7 +250,7 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
|
||||
slug: 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)]
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
boolean,
|
||||
index,
|
||||
pgEnum,
|
||||
pgTable,
|
||||
@@ -12,8 +11,8 @@ import {
|
||||
uniqueIndex,
|
||||
uuid,
|
||||
varchar,
|
||||
boolean,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
import { imports } from "./imports";
|
||||
import { labels } from "./labels";
|
||||
import { lists } from "./lists";
|
||||
@@ -56,9 +55,11 @@ export const boards = pgTable(
|
||||
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||
visibility: boardVisibilityEnum("visibility").notNull().default("private"),
|
||||
type: boardTypeEnum("type").notNull().default("regular"),
|
||||
isArchived: boolean("isArchived").notNull().default(false),
|
||||
sourceBoardId: bigint("sourceBoardId", { mode: "number" }),
|
||||
},
|
||||
(table) => [
|
||||
index("board_is_archived_idx").on(table.isArchived),
|
||||
index("board_visibility_idx").on(table.visibility),
|
||||
index("board_type_idx").on(table.type),
|
||||
index("board_source_idx").on(table.sourceBoardId),
|
||||
|
||||
Reference in New Issue
Block a user