feat: disable UI elements if user does not have permissions
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ const PermissionsSettingsPage: NextPageWithLayout = () => {
|
|||||||
<>
|
<>
|
||||||
<SettingsLayout currentTab="permissions">
|
<SettingsLayout currentTab="permissions">
|
||||||
<PermissionsSettings />
|
<PermissionsSettings />
|
||||||
|
<Popup />
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
<Popup />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }) => (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" &&
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user