feat: disable UI elements if user does not have permissions

This commit is contained in:
Henry
2026-01-29 22:05:00 +00:00
parent 2cb7c55d13
commit a2ef6609f2
27 changed files with 357 additions and 208 deletions

View File

@@ -31,6 +31,7 @@ interface CheckboxDropdownProps {
handleEdit?: (key: string) => void; handleEdit?: (key: string) => void;
handleCreate?: () => void; handleCreate?: () => void;
asChild?: boolean; asChild?: boolean;
disabled?: boolean;
} }
export default function CheckboxDropdown({ export default function CheckboxDropdown({
@@ -44,6 +45,7 @@ export default function CheckboxDropdown({
handleEdit, handleEdit,
handleCreate, handleCreate,
asChild = true, asChild = true,
disabled = false,
}: CheckboxDropdownProps) { }: CheckboxDropdownProps) {
const [selectedGroup, setSelectedGroup] = useState<string | null>(null); const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
@@ -58,13 +60,13 @@ export default function CheckboxDropdown({
{items.length > 0 ? ( {items.length > 0 ? (
items.map((item) => ( items.map((item) => (
<Menu.Item key={item.key}> <Menu.Item key={item.key}>
<div <div
className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300" className="group flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
handleSelect(groupKey, { key: item.key, value: item.value }); handleSelect(groupKey, { key: item.key, value: item.value });
}} }}
> >
<input <input
id={item.key} id={item.key}
name={item.key} name={item.key}
@@ -132,7 +134,8 @@ export default function CheckboxDropdown({
<> <>
<Menu.Button <Menu.Button
as={asChild ? "div" : undefined} as={asChild ? "div" : undefined}
className="h-full w-full cursor-pointer focus-visible:outline-none" disabled={disabled}
className="h-full w-full cursor-pointer focus-visible:outline-none disabled:cursor-not-allowed"
> >
{children} {children}
</Menu.Button> </Menu.Button>

View File

@@ -6,7 +6,7 @@ export default function Dropdown({
children, children,
disabled, disabled,
}: { }: {
items: { label: string; action: () => void; icon?: React.ReactNode }[]; items: { label: string; action?: () => void; icon?: React.ReactNode; disabled?: boolean }[];
children: React.ReactNode; children: React.ReactNode;
disabled?: boolean; disabled?: boolean;
}) { }) {
@@ -33,10 +33,11 @@ export default function Dropdown({
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300"> <Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<div className="flex flex-col"> <div className="flex flex-col">
{items.map((item) => ( {items.map((item) => (
<Menu.Item key={item.label}> <Menu.Item key={item.label} disabled={item.disabled}>
<button <button
onClick={item.action} onClick={item.action}
className="flex w-auto items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-950 dark:hover:bg-dark-400" disabled={item.disabled ?? !item.action}
className="flex w-auto items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-left text-sm text-neutral-900 hover:bg-light-200 disabled:cursor-not-allowed disabled:opacity-60 dark:text-dark-950 dark:hover:bg-dark-400"
> >
{item.icon} {item.icon}
{item.label} {item.label}

View File

@@ -17,6 +17,7 @@ import {
HiOutlineShieldCheck, HiOutlineShieldCheck,
HiOutlineUser, HiOutlineUser,
} from "react-icons/hi2"; } from "react-icons/hi2";
import { usePermissions } from "~/hooks/usePermissions";
import { useWorkspace } from "~/providers/workspace"; import { useWorkspace } from "~/providers/workspace";
interface SettingsLayoutProps { interface SettingsLayoutProps {
@@ -27,6 +28,7 @@ interface SettingsLayoutProps {
export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) { export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) {
const router = useRouter(); const router = useRouter();
const { workspace } = useWorkspace(); const { workspace } = useWorkspace();
const { canViewWorkspace, canEditWorkspace } = usePermissions();
const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const isAdmin = workspace.role === "admin"; const isAdmin = workspace.role === "admin";
@@ -42,7 +44,7 @@ export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) {
key: "workspace", key: "workspace",
icon: <HiOutlineRectangleGroup />, icon: <HiOutlineRectangleGroup />,
label: t`Workspace`, label: t`Workspace`,
condition: true, condition: canViewWorkspace,
}, },
{ {
key: "permissions", key: "permissions",
@@ -66,7 +68,7 @@ export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) {
key: "integrations", key: "integrations",
icon: <HiOutlineCodeBracketSquare />, icon: <HiOutlineCodeBracketSquare />,
label: t`Integrations`, label: t`Integrations`,
condition: true, condition: canEditWorkspace,
}, },
]; ];
@@ -108,7 +110,7 @@ export function SettingsLayout({ children, currentTab }: SettingsLayoutProps) {
> >
<div className="relative mb-4"> <div className="relative mb-4">
<ListboxButton className="w-full appearance-none rounded-lg border-0 bg-light-50 py-2 pl-3 pr-10 text-left text-sm text-light-1000 shadow-sm ring-1 ring-inset ring-light-300 focus:ring-2 focus:ring-inset focus:ring-light-400 dark:bg-dark-50 dark:text-dark-1000 dark:ring-dark-300 dark:focus:ring-dark-500"> <ListboxButton className="w-full appearance-none rounded-lg border-0 bg-light-50 py-2 pl-3 pr-10 text-left text-sm text-light-1000 shadow-sm ring-1 ring-inset ring-light-300 focus:ring-2 focus:ring-inset focus:ring-light-400 dark:bg-dark-50 dark:text-dark-1000 dark:ring-dark-300 dark:focus:ring-dark-500">
{availableTabs[selectedTabIndex]?.label || "Select a tab"} {availableTabs[selectedTabIndex]?.label ?? "Select a tab"}
<HiChevronDown <HiChevronDown
aria-hidden="true" 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" 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"

View File

@@ -77,7 +77,7 @@ export default function UserMenu({
) : ( ) : (
<Menu.Button <Menu.Button
className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000" className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000"
title={isCollapsed ? displayName ?? email : undefined} title={isCollapsed ? (displayName || email) : undefined}
> >
{avatarUrl ? ( {avatarUrl ? (
<Image <Image
@@ -104,7 +104,7 @@ export default function UserMenu({
isCollapsed && "md:hidden", isCollapsed && "md:hidden",
)} )}
> >
{displayName ?? email} {displayName || email}
</span> </span>
</Menu.Button> </Menu.Button>
)} )}

View File

@@ -25,6 +25,8 @@ interface UsePermissionsResult {
canInviteMember: boolean; canInviteMember: boolean;
canEditMember: boolean; canEditMember: boolean;
canRemoveMember: boolean; canRemoveMember: boolean;
canViewWorkspace: boolean;
canEditWorkspace: boolean;
} }
export function usePermissions(): UsePermissionsResult { export function usePermissions(): UsePermissionsResult {
@@ -66,6 +68,8 @@ export function usePermissions(): UsePermissionsResult {
canInviteMember: hasPermission("member:invite"), canInviteMember: hasPermission("member:invite"),
canEditMember: hasPermission("member:edit"), canEditMember: hasPermission("member:edit"),
canRemoveMember: hasPermission("member:remove"), canRemoveMember: hasPermission("member:remove"),
canViewWorkspace: hasPermission("workspace:view"),
canEditWorkspace: hasPermission("workspace:edit"),
}; };
} }

View File

@@ -2,11 +2,13 @@ import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard"; import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout"; import { SettingsLayout } from "~/components/SettingsLayout";
import ApiSettings from "~/views/settings/ApiSettings"; import ApiSettings from "~/views/settings/ApiSettings";
import Popup from "~/components/Popup";
const ApiSettingsPage: NextPageWithLayout = () => { const ApiSettingsPage: NextPageWithLayout = () => {
return ( return (
<SettingsLayout currentTab="api"> <SettingsLayout currentTab="api">
<ApiSettings /> <ApiSettings />
<Popup />
</SettingsLayout> </SettingsLayout>
); );
}; };

View File

@@ -2,11 +2,13 @@ import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard"; import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout"; import { SettingsLayout } from "~/components/SettingsLayout";
import BillingSettings from "~/views/settings/BillingSettings"; import BillingSettings from "~/views/settings/BillingSettings";
import Popup from "~/components/Popup";
const BillingSettingsPage: NextPageWithLayout = () => { const BillingSettingsPage: NextPageWithLayout = () => {
return ( return (
<SettingsLayout currentTab="billing"> <SettingsLayout currentTab="billing">
<BillingSettings /> <BillingSettings />
<Popup />
</SettingsLayout> </SettingsLayout>
); );
}; };

View File

@@ -2,11 +2,13 @@ import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard"; import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout"; import { SettingsLayout } from "~/components/SettingsLayout";
import IntegrationsSettings from "~/views/settings/IntegrationsSettings"; import IntegrationsSettings from "~/views/settings/IntegrationsSettings";
import Popup from "~/components/Popup";
const IntegrationsSettingsPage: NextPageWithLayout = () => { const IntegrationsSettingsPage: NextPageWithLayout = () => {
return ( return (
<SettingsLayout currentTab="integrations"> <SettingsLayout currentTab="integrations">
<IntegrationsSettings /> <IntegrationsSettings />
<Popup />
</SettingsLayout> </SettingsLayout>
); );
}; };

View File

@@ -9,8 +9,9 @@ const PermissionsSettingsPage: NextPageWithLayout = () => {
<> <>
<SettingsLayout currentTab="permissions"> <SettingsLayout currentTab="permissions">
<PermissionsSettings /> <PermissionsSettings />
<Popup />
</SettingsLayout> </SettingsLayout>
<Popup />
</> </>
); );
}; };

View File

@@ -2,11 +2,13 @@ import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard"; import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout"; import { SettingsLayout } from "~/components/SettingsLayout";
import WorkspaceSettings from "~/views/settings/WorkspaceSettings"; import WorkspaceSettings from "~/views/settings/WorkspaceSettings";
import Popup from "~/components/Popup";
const WorkspaceSettingsPage: NextPageWithLayout = () => { const WorkspaceSettingsPage: NextPageWithLayout = () => {
return ( return (
<SettingsLayout currentTab="workspace"> <SettingsLayout currentTab="workspace">
<WorkspaceSettings /> <WorkspaceSettings />
<Popup />
</SettingsLayout> </SettingsLayout>
); );
}; };

View File

@@ -24,43 +24,48 @@ export default function BoardDropdown({
workspacePublicId: string; workspacePublicId: string;
}) { }) {
const { openModal } = useModal(); const { openModal } = useModal();
const { canEditBoard, canDeleteBoard } = usePermissions(); const { canEditBoard, canDeleteBoard, canCreateBoard } = usePermissions();
return (
<Dropdown const items = [
disabled={isLoading} ...(isTemplate
items={[ ? []
...(isTemplate : [
? [] {
: [ label: t`Make template`,
...(canEditBoard action: canCreateBoard ? () => openModal("CREATE_TEMPLATE") : undefined,
? [ icon: (
{ <HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
label: t`Make template`, ),
action: () => openModal("CREATE_TEMPLATE"), disabled: !canCreateBoard,
icon: ( },
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" /> ...(canEditBoard
), ? [
}, {
{ label: t`Edit board URL`,
label: t`Edit board URL`, action: () => openModal("UPDATE_BOARD_SLUG"),
action: () => openModal("UPDATE_BOARD_SLUG"), icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />, },
}, ]
] : []),
: []), ]),
]),
...(canDeleteBoard ...(canDeleteBoard
? [ ? [
{ {
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) {
return null;
}
return (
<Dropdown disabled={isLoading} items={items}>
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" /> <HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Dropdown> </Dropdown>
); );

View File

@@ -12,6 +12,7 @@ import {
import { authClient } from "@kan/auth/client"; import { authClient } from "@kan/auth/client";
import Dropdown from "~/components/Dropdown"; import Dropdown from "~/components/Dropdown";
import { Tooltip } from "~/components/Tooltip";
import { usePermissions } from "~/hooks/usePermissions"; import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal"; import { useModal } from "~/providers/modal";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@@ -47,6 +48,7 @@ export default function List({
const { data: session } = authClient.useSession(); const { data: session } = authClient.useSession();
const isCreator = list.createdBy && session?.user.id === list.createdBy; const isCreator = list.createdBy && session?.user.id === list.createdBy;
const canEdit = canEditList || isCreator; const canEdit = canEditList || isCreator;
const canDrag = canEditList || isCreator;
const openNewCardForm = (publicListId: PublicListId) => { const openNewCardForm = (publicListId: PublicListId) => {
if (!canCreateCard) return; if (!canCreateCard) return;
@@ -81,7 +83,12 @@ export default function List({
}; };
return ( return (
<Draggable key={list.publicId} draggableId={list.publicId} index={index}> <Draggable
key={list.publicId}
draggableId={list.publicId}
index={index}
isDragDisabled={!canDrag}
>
{(provided) => ( {(provided) => (
<div <div
key={list.publicId} key={list.publicId}
@@ -105,47 +112,60 @@ export default function List({
/> />
</form> </form>
<div className="flex items-center"> <div className="flex items-center">
{canCreateCard && ( <Tooltip
content={
!canCreateCard ? t`You don't have permission` : undefined
}
>
<button <button
className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-400 dark:hover:bg-dark-200" className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-400 disabled:opacity-60 disabled:cursor-not-allowed dark:hover:bg-dark-200"
onClick={() => openNewCardForm(list.publicId)} onClick={() => openNewCardForm(list.publicId)}
disabled={!canCreateCard}
> >
<HiOutlinePlusSmall <HiOutlinePlusSmall
className="h-5 w-5 text-dark-900" className="h-5 w-5 text-dark-900"
aria-hidden="true" aria-hidden="true"
/> />
</button> </button>
)} </Tooltip>
<div className="relative mr-1 inline-block"> {(() => {
<Dropdown const dropdownItems = [
items={[ ...(canCreateCard
...(canCreateCard ? [
? [ {
{ label: t`Add a card`,
label: t`Add a card`, action: () => openNewCardForm(list.publicId),
action: () => openNewCardForm(list.publicId), icon: (
icon: ( <HiOutlineSquaresPlus className="h-[18px] w-[18px] text-dark-900" />
<HiOutlineSquaresPlus className="h-[18px] w-[18px] text-dark-900" /> ),
), },
}, ]
] : []),
: []), ...(canDeleteList || isCreator
...(canDeleteList || isCreator ? [
? [ {
{ label: t`Delete list`,
label: t`Delete list`, action: handleOpenDeleteListConfirmation,
action: handleOpenDeleteListConfirmation, icon: (
icon: ( <HiOutlineTrash className="h-[18px] w-[18px] text-dark-900" />
<HiOutlineTrash className="h-[18px] w-[18px] text-dark-900" /> ),
), },
}, ]
] : []),
: []), ];
]}
> if (dropdownItems.length === 0) {
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" /> return null;
</Dropdown> }
</div>
return (
<div className="relative mr-1 inline-block">
<Dropdown items={dropdownItems}>
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Dropdown>
</div>
);
})()}
</div> </div>
</div> </div>
{children} {children}

View File

@@ -1,17 +1,22 @@
import Link from "next/link"; import Link from "next/link";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env"; import { env } from "next-runtime-env";
import { HiLink } from "react-icons/hi"; import { HiLink } from "react-icons/hi";
import { Tooltip } from "~/components/Tooltip";
const UpdateBoardSlugButton = ({ const UpdateBoardSlugButton = ({
handleOnClick, handleOnClick,
workspaceSlug, workspaceSlug,
boardSlug, boardSlug,
isLoading, isLoading,
canEdit,
}: { }: {
handleOnClick: () => void; handleOnClick: () => void;
workspaceSlug: string; workspaceSlug: string;
boardSlug: string; boardSlug: string;
isLoading: boolean; isLoading: boolean;
canEdit: boolean;
}) => { }) => {
if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>; if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>;
@@ -22,10 +27,14 @@ const UpdateBoardSlugButton = ({
} }
return ( return (
<button <Tooltip
onClick={handleOnClick} content={!canEdit && !isLoading ? t`You don't have permission` : undefined}
className="hidden cursor-pointer items-center gap-2 rounded-full border-[1px] bg-light-50 p-1 pl-4 pr-1 text-sm text-light-950 hover:bg-light-100 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900 dark:hover:bg-dark-100 xl:flex"
> >
<button
onClick={canEdit ? handleOnClick : undefined}
disabled={!canEdit || isLoading}
className="hidden cursor-pointer items-center gap-2 rounded-full border-[1px] bg-light-50 p-1 pl-4 pr-1 text-sm text-light-950 hover:bg-light-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900 dark:hover:bg-dark-100 xl:flex"
>
<div className="flex items-center"> <div className="flex items-center">
<span> <span>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" {env("NEXT_PUBLIC_KAN_ENV") === "cloud"
@@ -41,13 +50,20 @@ const UpdateBoardSlugButton = ({
href={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/${boardSlug}`} href={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/${boardSlug}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onClick={(e) => {
e.stopPropagation();
if (!canEdit) {
e.preventDefault();
}
}}
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200" className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
> >
<HiLink className="h-[13px] w-[13px]" /> <HiLink className="h-[13px] w-[13px]" />
</Link> </Link>
</button> </button>
</Tooltip>
); );
}; };
export default UpdateBoardSlugButton; export default UpdateBoardSlugButton;

View File

@@ -4,6 +4,8 @@ import { HiOutlineEye, HiOutlineEyeSlash } from "react-icons/hi2";
import Button from "~/components/Button"; import Button from "~/components/Button";
import CheckboxDropdown from "~/components/CheckboxDropdown"; import CheckboxDropdown from "~/components/CheckboxDropdown";
import { Tooltip } from "~/components/Tooltip";
import { usePermissions } from "~/hooks/usePermissions";
import { usePopup } from "~/providers/popup"; import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@@ -29,6 +31,7 @@ const VisibilityButton = ({
isAdmin: boolean; isAdmin: boolean;
}) => { }) => {
const { showPopup } = usePopup(); const { showPopup } = usePopup();
const { canEditBoard } = usePermissions();
const utils = api.useUtils(); const utils = api.useUtils();
const [stateVisibility, setStateVisibility] = useState<"public" | "private">( const [stateVisibility, setStateVisibility] = useState<"public" | "private">(
visibility, visibility,
@@ -60,38 +63,47 @@ const VisibilityButton = ({
}, },
}); });
const canEdit = canEditBoard || isAdmin;
return ( return (
<div className="relative"> <div className="relative">
<CheckboxDropdown <Tooltip
items={[ content={
{ !canEdit && !isLoading ? t`You don't have permission` : undefined
key: "public", }
value: t`Public`,
selected: isPublic,
},
{
key: "private",
value: t`Private`,
selected: !isPublic,
},
]}
handleSelect={(_g, i) => {
setStateVisibility(isPublic ? "private" : "public");
updateBoardVisibility.mutate({
visibility: i.key as "public" | "private",
boardPublicId,
});
}}
menuSpacing="md"
> >
<Button <CheckboxDropdown
variant="secondary" items={[
iconLeft={isPublic ? <HiOutlineEye /> : <HiOutlineEyeSlash />} {
disabled={isLoading || !isAdmin} key: "public",
value: t`Public`,
selected: isPublic,
},
{
key: "private",
value: t`Private`,
selected: !isPublic,
},
]}
handleSelect={(_g, i) => {
if (!canEdit) return;
setStateVisibility(isPublic ? "private" : "public");
updateBoardVisibility.mutate({
visibility: i.key as "public" | "private",
boardPublicId,
});
}}
menuSpacing="md"
> >
{t`Visibility`} <Button
</Button> variant="secondary"
</CheckboxDropdown> iconLeft={isPublic ? <HiOutlineEye /> : <HiOutlineEyeSlash />}
disabled={isLoading || !canEdit}
>
{t`Visibility`}
</Button>
</CheckboxDropdown>
</Tooltip>
</div> </div>
); );
}; };

View File

@@ -64,7 +64,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
direction: "horizontal", direction: "horizontal",
}); });
const { canCreateList, canEditList, canEditCard } = usePermissions(); const { canCreateList, canEditList, canEditCard, canEditBoard } = usePermissions();
const { tooltipContent: createListShortcutTooltipContent } = const { tooltipContent: createListShortcutTooltipContent } =
useKeyboardShortcut({ useKeyboardShortcut({
@@ -415,10 +415,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
id="name" id="name"
type="text" type="text"
{...register("name")} {...register("name")}
onBlur={handleSubmit(onSubmit)} onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined}
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000 sm:text-[1.2rem]" readOnly={!canEditBoard}
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000 sm:text-[1.2rem] disabled:cursor-not-allowed"
/> />
</form> </form>
)} )}
{!boardData && !isLoading && ( {!boardData && !isLoading && (
<p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1"> <p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1">
@@ -441,6 +443,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
isLoading={isLoading} isLoading={isLoading}
workspaceSlug={workspace.slug ?? ""} workspaceSlug={workspace.slug ?? ""}
boardSlug={boardData?.slug ?? ""} boardSlug={boardData?.slug ?? ""}
canEdit={canEditBoard}
/> />
<VisibilityButton <VisibilityButton
visibility={boardData?.visibility ?? "private"} visibility={boardData?.visibility ?? "private"}
@@ -463,24 +466,28 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
)} )}
</> </>
)} )}
{canCreateList && ( <Tooltip
<Tooltip content={createListShortcutTooltipContent}> content={
<Button !canCreateList
iconLeft={ ? t`You don't have permission`
<HiOutlinePlusSmall : createListShortcutTooltipContent
className="-mr-0.5 h-5 w-5" }
aria-hidden="true" >
/> <Button
} iconLeft={
onClick={() => { <HiOutlinePlusSmall
if (boardId) openNewListForm(boardId); className="-mr-0.5 h-5 w-5"
}} aria-hidden="true"
disabled={!boardData} />
> }
{t`New list`} onClick={() => {
</Button> if (boardId && canCreateList) openNewListForm(boardId);
</Tooltip> }}
)} disabled={!boardData || !canCreateList}
>
{t`New list`}
</Button>
</Tooltip>
<BoardDropdown <BoardDropdown
isTemplate={!!isTemplate} isTemplate={!!isTemplate}
isLoading={!boardData} isLoading={!boardData}
@@ -516,15 +523,20 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
: t`No lists have been created yet`} : t`No lists have been created yet`}
</p> </p>
</div> </div>
{canCreateList && ( <Tooltip
content={
!canCreateList ? t`You don't have permission` : undefined
}
>
<Button <Button
onClick={() => { onClick={() => {
if (boardId) openNewListForm(boardId); if (boardId && canCreateList) openNewListForm(boardId);
}} }}
disabled={!canCreateList}
> >
{t`Create new list`} {t`Create new list`}
</Button> </Button>
)} </Tooltip>
</div> </div>
) : ( ) : (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
@@ -564,6 +576,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
key={card.publicId} key={card.publicId}
draggableId={card.publicId} draggableId={card.publicId}
index={index} index={index}
isDragDisabled={!canEditCard}
> >
{(provided) => ( {(provided) => (
<Link <Link

View File

@@ -4,6 +4,8 @@ import { HiOutlineRectangleStack } from "react-icons/hi2";
import Button from "~/components/Button"; import Button from "~/components/Button";
import PatternedBackground from "~/components/PatternedBackground"; import PatternedBackground from "~/components/PatternedBackground";
import { Tooltip } from "~/components/Tooltip";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal"; 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";
@@ -11,6 +13,7 @@ import { api } from "~/utils/api";
export function BoardsList({ isTemplate }: { isTemplate?: boolean }) { export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
const { workspace } = useWorkspace(); const { workspace } = useWorkspace();
const { openModal } = useModal(); const { openModal } = useModal();
const { canCreateBoard } = usePermissions();
const { data, isLoading } = api.board.all.useQuery( const { data, isLoading } = api.board.all.useQuery(
{ {
@@ -41,9 +44,20 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
{t`Get started by creating a new ${isTemplate ? "template" : "board"}`} {t`Get started by creating a new ${isTemplate ? "template" : "board"}`}
</p> </p>
</div> </div>
<Button onClick={() => openModal("NEW_BOARD")}> <Tooltip
{t`Create new ${isTemplate ? "template" : "board"}`} content={
</Button> !canCreateBoard ? t`You don't have permission` : undefined
}
>
<Button
onClick={() => {
if (canCreateBoard) openModal("NEW_BOARD");
}}
disabled={!canCreateBoard}
>
{t`Create new ${isTemplate ? "template" : "board"}`}
</Button>
</Tooltip>
</div> </div>
); );

View File

@@ -41,22 +41,40 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
</h1> </h1>
<div className="flex gap-2"> <div className="flex gap-2">
{!isTemplate && ( {!isTemplate && (
<Button <Tooltip
type="button" content={
variant="secondary" !canCreateBoard ? t`You don't have permission` : undefined
onClick={() => openModal("IMPORT_BOARDS")}
iconLeft={
<HiArrowDownTray aria-hidden="true" className="h-4 w-4" />
} }
> >
{t`Import`} <Button
</Button> type="button"
variant="secondary"
onClick={() => {
if (canCreateBoard) openModal("IMPORT_BOARDS");
}}
disabled={!canCreateBoard}
iconLeft={
<HiArrowDownTray aria-hidden="true" className="h-4 w-4" />
}
>
{t`Import`}
</Button>
</Tooltip>
)} )}
<Tooltip content={createModalShortcutTooltipContent}> <Tooltip
content={
!canCreateBoard
? t`You don't have permission`
: createModalShortcutTooltipContent
}
>
<Button <Button
type="button" type="button"
variant="primary" variant="primary"
onClick={() => openModal("NEW_BOARD")} onClick={() => {
if (canCreateBoard) openModal("NEW_BOARD");
}}
disabled={!canCreateBoard}
iconLeft={ iconLeft={
<HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" /> <HiOutlinePlusSmall aria-hidden="true" className="h-4 w-4" />
} }

View File

@@ -12,12 +12,14 @@ interface DueDateSelectorProps {
cardPublicId: string; cardPublicId: string;
dueDate: Date | null | undefined; dueDate: Date | null | undefined;
isLoading?: boolean; isLoading?: boolean;
disabled?: boolean;
} }
export function DueDateSelector({ export function DueDateSelector({
cardPublicId, cardPublicId,
dueDate, dueDate,
isLoading = false, isLoading = false,
disabled = false,
}: DueDateSelectorProps) { }: DueDateSelectorProps) {
const { showPopup } = usePopup(); const { showPopup } = usePopup();
const utils = api.useUtils(); const utils = api.useUtils();
@@ -105,9 +107,9 @@ export function DueDateSelector({
<div className="relative flex w-full items-center text-left"> <div className="relative flex w-full items-center text-left">
<button <button
type="button" type="button"
onClick={() => setIsOpen(!isOpen)} onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={isLoading} disabled={isLoading || disabled}
className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100" className={`flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 dark:border-dark-50 dark:text-dark-1000 ${disabled ? "cursor-not-allowed opacity-60" : "hover:border-light-300 hover:bg-light-200 dark:hover:border-dark-200 dark:hover:bg-dark-100"}`}
> >
{dueDate ? ( {dueDate ? (
<span>{format(dueDate, "MMM d, yyyy")}</span> <span>{format(dueDate, "MMM d, yyyy")}</span>
@@ -118,7 +120,7 @@ export function DueDateSelector({
</> </>
)} )}
</button> </button>
{isOpen && ( {isOpen && !disabled && (
<> <>
<div className="fixed inset-0 z-10" onClick={handleBackdropClick} /> <div className="fixed inset-0 z-10" onClick={handleBackdropClick} />
<div <div

View File

@@ -17,12 +17,14 @@ interface LabelSelectorProps {
leftIcon: React.ReactNode; leftIcon: React.ReactNode;
}[]; }[];
isLoading: boolean; isLoading: boolean;
disabled?: boolean;
} }
export default function LabelSelector({ export default function LabelSelector({
cardPublicId, cardPublicId,
labels, labels,
isLoading, isLoading,
disabled = false,
}: LabelSelectorProps) { }: LabelSelectorProps) {
const utils = api.useUtils(); const utils = api.useUtils();
const { openModal } = useModal(); const { openModal } = useModal();
@@ -93,9 +95,10 @@ export default function LabelSelector({
handleSelect={(_, label) => { handleSelect={(_, label) => {
addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key }); addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key });
}} }}
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)} handleEdit={disabled ? undefined : (labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
handleCreate={() => openModal("NEW_LABEL")} handleCreate={disabled ? undefined : () => openModal("NEW_LABEL")}
createNewItemLabel={t`Create new label`} createNewItemLabel={t`Create new label`}
disabled={disabled}
asChild asChild
> >
{selectedLabels.length ? ( {selectedLabels.length ? (
@@ -110,7 +113,7 @@ export default function LabelSelector({
<Badge value={t`Add label`} iconLeft={<HiMiniPlus size={14} />} /> <Badge value={t`Add label`} iconLeft={<HiMiniPlus size={14} />} />
</div> </div>
) : ( ) : (
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100"> <div className={`flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 pl-2 text-left text-sm text-neutral-900 dark:border-dark-50 dark:text-dark-1000 ${disabled ? "cursor-not-allowed opacity-60" : "hover:border-light-300 hover:bg-light-200 dark:hover:border-dark-200 dark:hover:bg-dark-100"}`}>
<HiMiniPlus size={22} className="pr-2" /> <HiMiniPlus size={22} className="pr-2" />
{t`Add label`} {t`Add label`}
</div> </div>

View File

@@ -13,12 +13,14 @@ interface ListSelectorProps {
selected: boolean; selected: boolean;
}[]; }[];
isLoading: boolean; isLoading: boolean;
disabled?: boolean;
} }
export default function ListSelector({ export default function ListSelector({
cardPublicId, cardPublicId,
lists, lists,
isLoading, isLoading,
disabled = false,
}: ListSelectorProps) { }: ListSelectorProps) {
const utils = api.useUtils(); const utils = api.useUtils();
@@ -77,9 +79,10 @@ export default function ListSelector({
index: 0, index: 0,
}); });
}} }}
disabled={disabled}
asChild asChild
> >
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100"> <div className={`flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 dark:border-dark-50 dark:text-dark-1000 ${disabled ? "cursor-not-allowed opacity-60" : "hover:border-light-300 hover:bg-light-200 dark:hover:border-dark-200 dark:hover:bg-dark-100"}`}>
{selectedList?.value} {selectedList?.value}
</div> </div>
</CheckboxDropdown> </CheckboxDropdown>

View File

@@ -19,12 +19,14 @@ interface MemberSelectorProps {
imageUrl: string | undefined; imageUrl: string | undefined;
}[]; }[];
isLoading: boolean; isLoading: boolean;
disabled?: boolean;
} }
export default function MemberSelector({ export default function MemberSelector({
cardPublicId, cardPublicId,
members, members,
isLoading, isLoading,
disabled = false,
}: MemberSelectorProps) { }: MemberSelectorProps) {
const router = useRouter(); const router = useRouter();
const utils = api.useUtils(); const utils = api.useUtils();
@@ -108,11 +110,12 @@ export default function MemberSelector({
workspaceMemberPublicId: member.key, workspaceMemberPublicId: member.key,
}); });
}} }}
handleCreate={handleInviteMember} handleCreate={disabled ? undefined : handleInviteMember}
createNewItemLabel={t`Invite member`} createNewItemLabel={t`Invite member`}
disabled={disabled}
asChild asChild
> >
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 hover:border-light-300 hover:bg-light-200 dark:border-dark-50 dark:text-dark-1000 dark:hover:border-dark-200 dark:hover:bg-dark-100"> <div className={`flex h-full w-full items-center rounded-[5px] border-[1px] border-light-50 py-1 pl-2 text-left text-xs text-neutral-900 dark:border-dark-50 dark:text-dark-1000 ${disabled ? "cursor-not-allowed opacity-60" : "hover:border-light-300 hover:bg-light-200 dark:hover:border-dark-200 dark:hover:bg-dark-100"}`}>
{selectedMembers.length ? ( {selectedMembers.length ? (
<div className="isolate flex justify-end -space-x-1 overflow-hidden"> <div className="isolate flex justify-end -space-x-1 overflow-hidden">
{selectedMembers.map(({ value, imageUrl }) => ( {selectedMembers.map(({ value, imageUrl }) => (

View File

@@ -118,44 +118,44 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
return ( return (
<div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900"> <div className="h-full w-[360px] border-l-[1px] border-light-300 bg-light-50 p-8 text-light-900 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
{canEdit && ( <div className="mb-4 flex w-full flex-row pt-[18px]">
<> <p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p>
<div className="mb-4 flex w-full flex-row pt-[18px]"> <ListSelector
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`List`}</p> cardPublicId={cardId ?? ""}
<ListSelector lists={formattedLists}
cardPublicId={cardId ?? ""} isLoading={!card}
lists={formattedLists} disabled={!canEdit}
isLoading={!card} />
/> </div>
</div> <div className="mb-4 flex w-full flex-row">
<div className="mb-4 flex w-full flex-row"> <p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Labels`}</p>
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Labels`}</p> <LabelSelector
<LabelSelector cardPublicId={cardId ?? ""}
cardPublicId={cardId ?? ""} labels={formattedLabels}
labels={formattedLabels} isLoading={!card}
isLoading={!card} disabled={!canEdit}
/> />
</div> </div>
{!isTemplate && ( {!isTemplate && (
<div className="mb-4 flex w-full flex-row"> <div className="mb-4 flex w-full flex-row">
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Members`}</p> <p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Members`}</p>
<MemberSelector <MemberSelector
cardPublicId={cardId ?? ""} cardPublicId={cardId ?? ""}
members={formattedMembers} members={formattedMembers}
isLoading={!card} isLoading={!card}
/> disabled={!canEdit}
</div> />
)} </div>
<div className="mb-4 flex w-full flex-row">
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Due date`}</p>
<DueDateSelector
cardPublicId={cardId ?? ""}
dueDate={card?.dueDate}
isLoading={!card}
/>
</div>
</>
)} )}
<div className="mb-4 flex w-full flex-row">
<p className="my-2 mb-2 w-[100px] text-sm font-medium">{t`Due date`}</p>
<DueDateSelector
cardPublicId={cardId ?? ""}
dueDate={card?.dueDate}
isLoading={!card}
disabled={!canEdit}
/>
</div>
</div> </div>
); );
} }

View File

@@ -12,6 +12,7 @@ import FeedbackModal from "~/components/FeedbackModal";
import Modal from "~/components/modal"; import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead"; import { PageHead } from "~/components/PageHead";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal"; 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";
@@ -25,6 +26,7 @@ import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation"
export default function WorkspaceSettings() { export default function WorkspaceSettings() {
const { modalContentType, openModal, isOpen } = useModal(); const { modalContentType, openModal, isOpen } = useModal();
const { workspace } = useWorkspace(); const { workspace } = useWorkspace();
const { canEditWorkspace } = usePermissions();
const router = useRouter(); const router = useRouter();
const { data } = api.user.getUser.useQuery(); const { data } = api.user.getUser.useQuery();
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false); const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
@@ -61,6 +63,7 @@ export default function WorkspaceSettings() {
<UpdateWorkspaceNameForm <UpdateWorkspaceNameForm
workspacePublicId={workspace.publicId} workspacePublicId={workspace.publicId}
workspaceName={workspace.name} workspaceName={workspace.name}
disabled={!canEditWorkspace}
/> />
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000"> <h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
@@ -70,6 +73,7 @@ export default function WorkspaceSettings() {
workspacePublicId={workspace.publicId} workspacePublicId={workspace.publicId}
workspaceUrl={workspace.slug ?? ""} workspaceUrl={workspace.slug ?? ""}
workspacePlan={workspace.plan ?? "free"} workspacePlan={workspace.plan ?? "free"}
disabled={!canEditWorkspace}
/> />
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000"> <h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
@@ -78,6 +82,7 @@ export default function WorkspaceSettings() {
<UpdateWorkspaceDescriptionForm <UpdateWorkspaceDescriptionForm
workspacePublicId={workspace.publicId} workspacePublicId={workspace.publicId}
workspaceDescription={workspace.description ?? ""} workspaceDescription={workspace.description ?? ""}
disabled={!canEditWorkspace}
/> />
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000"> <h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
@@ -88,6 +93,7 @@ export default function WorkspaceSettings() {
showEmailsToMembers={Boolean( showEmailsToMembers={Boolean(
workspaceData?.showEmailsToMembers ?? false, workspaceData?.showEmailsToMembers ?? false,
)} )}
disabled={!canEditWorkspace}
/> />
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && {env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&

View File

@@ -11,9 +11,11 @@ import { api } from "~/utils/api";
const UpdateWorkspaceDescriptionForm = ({ const UpdateWorkspaceDescriptionForm = ({
workspacePublicId, workspacePublicId,
workspaceDescription, workspaceDescription,
disabled = false,
}: { }: {
workspacePublicId: string; workspacePublicId: string;
workspaceDescription: string; workspaceDescription: string;
disabled?: boolean;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { showPopup } = usePopup(); const { showPopup } = usePopup();
@@ -78,9 +80,10 @@ const UpdateWorkspaceDescriptionForm = ({
<Input <Input
{...register("description")} {...register("description")}
errorMessage={errors.description?.message} errorMessage={errors.description?.message}
disabled={disabled}
/> />
</div> </div>
{isDirty && ( {isDirty && !disabled && (
<div> <div>
<Button <Button
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}

View File

@@ -7,9 +7,11 @@ import { api } from "~/utils/api";
export default function UpdateWorkspaceEmailVisibilityForm({ export default function UpdateWorkspaceEmailVisibilityForm({
workspacePublicId, workspacePublicId,
showEmailsToMembers, showEmailsToMembers,
disabled = false,
}: { }: {
workspacePublicId: string; workspacePublicId: string;
showEmailsToMembers: boolean; showEmailsToMembers: boolean;
disabled?: boolean;
}) { }) {
const utils = api.useUtils(); const utils = api.useUtils();
const [isChecked, setIsChecked] = useState(showEmailsToMembers); const [isChecked, setIsChecked] = useState(showEmailsToMembers);
@@ -27,6 +29,7 @@ export default function UpdateWorkspaceEmailVisibilityForm({
}); });
const handleToggle = () => { const handleToggle = () => {
if (disabled) return;
const newValue = !isChecked; const newValue = !isChecked;
setIsChecked(newValue); setIsChecked(newValue);
updateWorkspace.mutate({ updateWorkspace.mutate({
@@ -46,7 +49,7 @@ export default function UpdateWorkspaceEmailVisibilityForm({
isChecked={isChecked} isChecked={isChecked}
onChange={handleToggle} onChange={handleToggle}
label="" label=""
disabled={updateWorkspace.isPending} disabled={disabled || updateWorkspace.isPending}
/> />
</div> </div>
); );

View File

@@ -20,9 +20,11 @@ type FormValues = z.infer<typeof schema>;
const UpdateWorkspaceNameForm = ({ const UpdateWorkspaceNameForm = ({
workspacePublicId, workspacePublicId,
workspaceName, workspaceName,
disabled = false,
}: { }: {
workspacePublicId: string; workspacePublicId: string;
workspaceName: string; workspaceName: string;
disabled?: boolean;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { showPopup } = usePopup(); const { showPopup } = usePopup();
@@ -70,9 +72,13 @@ const UpdateWorkspaceNameForm = ({
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
<div className="mb-4 flex w-full max-w-[325px] items-center gap-2"> <div className="mb-4 flex w-full max-w-[325px] items-center gap-2">
<Input {...register("name")} errorMessage={errors.name?.message} /> <Input
{...register("name")}
errorMessage={errors.name?.message}
disabled={disabled}
/>
</div> </div>
{isDirty && ( {isDirty && !disabled && (
<div> <div>
<Button <Button
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}

View File

@@ -16,10 +16,12 @@ const UpdateWorkspaceUrlForm = ({
workspacePublicId, workspacePublicId,
workspaceUrl, workspaceUrl,
workspacePlan, workspacePlan,
disabled = false,
}: { }: {
workspacePublicId: string; workspacePublicId: string;
workspaceUrl: string; workspaceUrl: string;
workspacePlan: "free" | "pro" | "enterprise"; workspacePlan: "free" | "pro" | "enterprise";
disabled?: boolean;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { showPopup } = usePopup(); const { showPopup } = usePopup();
@@ -136,9 +138,10 @@ const UpdateWorkspaceUrlForm = ({
<HiCheck className="h-4 w-4 dark:text-dark-1000" /> <HiCheck className="h-4 w-4 dark:text-dark-1000" />
) : null ) : null
} }
disabled={disabled}
/> />
</div> </div>
{isDirty && ( {isDirty && !disabled && (
<div> <div>
<Button <Button
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}