diff --git a/apps/web/src/components/CheckboxDropdown.tsx b/apps/web/src/components/CheckboxDropdown.tsx index d4e18525..18476f4d 100644 --- a/apps/web/src/components/CheckboxDropdown.tsx +++ b/apps/web/src/components/CheckboxDropdown.tsx @@ -31,6 +31,7 @@ interface CheckboxDropdownProps { handleEdit?: (key: string) => void; handleCreate?: () => void; asChild?: boolean; + disabled?: boolean; } export default function CheckboxDropdown({ @@ -44,6 +45,7 @@ export default function CheckboxDropdown({ handleEdit, handleCreate, asChild = true, + disabled = false, }: CheckboxDropdownProps) { const [selectedGroup, setSelectedGroup] = useState(null); @@ -58,13 +60,13 @@ export default function CheckboxDropdown({ {items.length > 0 ? ( items.map((item) => ( -
{ - e.preventDefault(); - handleSelect(groupKey, { key: item.key, value: item.value }); - }} - > +
{ + e.preventDefault(); + handleSelect(groupKey, { key: item.key, value: item.value }); + }} + > {children} diff --git a/apps/web/src/components/Dropdown.tsx b/apps/web/src/components/Dropdown.tsx index 4a4064a6..a5c612b5 100644 --- a/apps/web/src/components/Dropdown.tsx +++ b/apps/web/src/components/Dropdown.tsx @@ -6,7 +6,7 @@ export default function Dropdown({ children, disabled, }: { - items: { label: string; action: () => void; icon?: React.ReactNode }[]; + items: { label: string; action?: () => void; icon?: React.ReactNode; disabled?: boolean }[]; children: React.ReactNode; disabled?: boolean; }) { @@ -33,10 +33,11 @@ export default function Dropdown({
{items.map((item) => ( - + - )} -
- openNewCardForm(list.publicId), - icon: ( - - ), - }, - ] - : []), - ...(canDeleteList || isCreator - ? [ - { - label: t`Delete list`, - action: handleOpenDeleteListConfirmation, - icon: ( - - ), - }, - ] - : []), - ]} - > - - -
+ + {(() => { + const dropdownItems = [ + ...(canCreateCard + ? [ + { + label: t`Add a card`, + action: () => openNewCardForm(list.publicId), + icon: ( + + ), + }, + ] + : []), + ...(canDeleteList || isCreator + ? [ + { + label: t`Delete list`, + action: handleOpenDeleteListConfirmation, + icon: ( + + ), + }, + ] + : []), + ]; + + if (dropdownItems.length === 0) { + return null; + } + + return ( +
+ + + +
+ ); + })()}
{children} diff --git a/apps/web/src/views/board/components/UpdateBoardSlugButton.tsx b/apps/web/src/views/board/components/UpdateBoardSlugButton.tsx index 799cb464..ce33768c 100644 --- a/apps/web/src/views/board/components/UpdateBoardSlugButton.tsx +++ b/apps/web/src/views/board/components/UpdateBoardSlugButton.tsx @@ -1,17 +1,22 @@ import Link from "next/link"; +import { t } from "@lingui/core/macro"; import { env } from "next-runtime-env"; import { HiLink } from "react-icons/hi"; +import { Tooltip } from "~/components/Tooltip"; + const UpdateBoardSlugButton = ({ handleOnClick, workspaceSlug, boardSlug, isLoading, + canEdit, }: { handleOnClick: () => void; workspaceSlug: string; boardSlug: string; isLoading: boolean; + canEdit: boolean; }) => { if (!isLoading && (!workspaceSlug || !boardSlug)) return <>; @@ -22,10 +27,14 @@ const UpdateBoardSlugButton = ({ } return ( - + ); }; + export default UpdateBoardSlugButton; diff --git a/apps/web/src/views/board/components/VisibilityButton.tsx b/apps/web/src/views/board/components/VisibilityButton.tsx index f90c93d8..feb62c14 100644 --- a/apps/web/src/views/board/components/VisibilityButton.tsx +++ b/apps/web/src/views/board/components/VisibilityButton.tsx @@ -4,6 +4,8 @@ import { HiOutlineEye, HiOutlineEyeSlash } from "react-icons/hi2"; import Button from "~/components/Button"; import CheckboxDropdown from "~/components/CheckboxDropdown"; +import { Tooltip } from "~/components/Tooltip"; +import { usePermissions } from "~/hooks/usePermissions"; import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; @@ -29,6 +31,7 @@ const VisibilityButton = ({ isAdmin: boolean; }) => { const { showPopup } = usePopup(); + const { canEditBoard } = usePermissions(); const utils = api.useUtils(); const [stateVisibility, setStateVisibility] = useState<"public" | "private">( visibility, @@ -60,38 +63,47 @@ const VisibilityButton = ({ }, }); + const canEdit = canEditBoard || isAdmin; + return (
- { - setStateVisibility(isPublic ? "private" : "public"); - updateBoardVisibility.mutate({ - visibility: i.key as "public" | "private", - boardPublicId, - }); - }} - menuSpacing="md" + - - + + +
); }; diff --git a/apps/web/src/views/board/index.tsx b/apps/web/src/views/board/index.tsx index 46c1afec..7e9762ed 100644 --- a/apps/web/src/views/board/index.tsx +++ b/apps/web/src/views/board/index.tsx @@ -64,7 +64,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { direction: "horizontal", }); - const { canCreateList, canEditList, canEditCard } = usePermissions(); + const { canCreateList, canEditList, canEditCard, canEditBoard } = usePermissions(); const { tooltipContent: createListShortcutTooltipContent } = useKeyboardShortcut({ @@ -415,10 +415,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { id="name" type="text" {...register("name")} - onBlur={handleSubmit(onSubmit)} - 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]" + onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined} + 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" /> + )} {!boardData && !isLoading && (

@@ -441,6 +443,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { isLoading={isLoading} workspaceSlug={workspace.slug ?? ""} boardSlug={boardData?.slug ?? ""} + canEdit={canEditBoard} /> )} - {canCreateList && ( - - - - )} + + +

- {canCreateList && ( + - )} + ) : ( @@ -564,6 +576,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) { key={card.publicId} draggableId={card.publicId} index={index} + isDragDisabled={!canEditCard} > {(provided) => ( - + + + ); diff --git a/apps/web/src/views/boards/index.tsx b/apps/web/src/views/boards/index.tsx index 09e0a0e9..6730131c 100644 --- a/apps/web/src/views/boards/index.tsx +++ b/apps/web/src/views/boards/index.tsx @@ -41,22 +41,40 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
{!isTemplate && ( - + + )} - + - {isOpen && ( + {isOpen && !disabled && ( <>
{ addOrRemoveLabel.mutate({ cardPublicId, labelPublicId: label.key }); }} - handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)} - handleCreate={() => openModal("NEW_LABEL")} + handleEdit={disabled ? undefined : (labelPublicId) => openModal("EDIT_LABEL", labelPublicId)} + handleCreate={disabled ? undefined : () => openModal("NEW_LABEL")} createNewItemLabel={t`Create new label`} + disabled={disabled} asChild > {selectedLabels.length ? ( @@ -110,7 +113,7 @@ export default function LabelSelector({ } />
) : ( -
+
{t`Add label`}
diff --git a/apps/web/src/views/card/components/ListSelector.tsx b/apps/web/src/views/card/components/ListSelector.tsx index ba651550..bed786b4 100644 --- a/apps/web/src/views/card/components/ListSelector.tsx +++ b/apps/web/src/views/card/components/ListSelector.tsx @@ -13,12 +13,14 @@ interface ListSelectorProps { selected: boolean; }[]; isLoading: boolean; + disabled?: boolean; } export default function ListSelector({ cardPublicId, lists, isLoading, + disabled = false, }: ListSelectorProps) { const utils = api.useUtils(); @@ -77,9 +79,10 @@ export default function ListSelector({ index: 0, }); }} + disabled={disabled} asChild > -
+
{selectedList?.value}
diff --git a/apps/web/src/views/card/components/MemberSelector.tsx b/apps/web/src/views/card/components/MemberSelector.tsx index 110705fa..a2dcf00b 100644 --- a/apps/web/src/views/card/components/MemberSelector.tsx +++ b/apps/web/src/views/card/components/MemberSelector.tsx @@ -19,12 +19,14 @@ interface MemberSelectorProps { imageUrl: string | undefined; }[]; isLoading: boolean; + disabled?: boolean; } export default function MemberSelector({ cardPublicId, members, isLoading, + disabled = false, }: MemberSelectorProps) { const router = useRouter(); const utils = api.useUtils(); @@ -108,11 +110,12 @@ export default function MemberSelector({ workspaceMemberPublicId: member.key, }); }} - handleCreate={handleInviteMember} + handleCreate={disabled ? undefined : handleInviteMember} createNewItemLabel={t`Invite member`} + disabled={disabled} asChild > -
+
{selectedMembers.length ? (
{selectedMembers.map(({ value, imageUrl }) => ( diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index 700b34a4..7a06e99d 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -118,44 +118,44 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) { return (
- {canEdit && ( - <> -
-

{t`List`}

- -
-
-

{t`Labels`}

- -
- {!isTemplate && ( -
-

{t`Members`}

- -
- )} -
-

{t`Due date`}

- -
- +
+

{t`List`}

+ +
+
+

{t`Labels`}

+ +
+ {!isTemplate && ( +
+

{t`Members`}

+ +
)} +
+

{t`Due date`}

+ +
); } diff --git a/apps/web/src/views/settings/WorkspaceSettings.tsx b/apps/web/src/views/settings/WorkspaceSettings.tsx index 24039c3c..175eeb84 100644 --- a/apps/web/src/views/settings/WorkspaceSettings.tsx +++ b/apps/web/src/views/settings/WorkspaceSettings.tsx @@ -12,6 +12,7 @@ import FeedbackModal from "~/components/FeedbackModal"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; +import { usePermissions } from "~/hooks/usePermissions"; import { useModal } from "~/providers/modal"; import { useWorkspace } from "~/providers/workspace"; import { api } from "~/utils/api"; @@ -25,6 +26,7 @@ import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation" export default function WorkspaceSettings() { const { modalContentType, openModal, isOpen } = useModal(); const { workspace } = useWorkspace(); + const { canEditWorkspace } = usePermissions(); const router = useRouter(); const { data } = api.user.getUser.useQuery(); const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false); @@ -61,6 +63,7 @@ export default function WorkspaceSettings() {

@@ -70,6 +73,7 @@ export default function WorkspaceSettings() { workspacePublicId={workspace.publicId} workspaceUrl={workspace.slug ?? ""} workspacePlan={workspace.plan ?? "free"} + disabled={!canEditWorkspace} />

@@ -78,6 +82,7 @@ export default function WorkspaceSettings() {

@@ -88,6 +93,7 @@ export default function WorkspaceSettings() { showEmailsToMembers={Boolean( workspaceData?.showEmailsToMembers ?? false, )} + disabled={!canEditWorkspace} /> {env("NEXT_PUBLIC_KAN_ENV") === "cloud" && diff --git a/apps/web/src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx b/apps/web/src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx index 393beff6..72cb3efc 100644 --- a/apps/web/src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx +++ b/apps/web/src/views/settings/components/UpdateWorkspaceDescriptionForm.tsx @@ -11,9 +11,11 @@ import { api } from "~/utils/api"; const UpdateWorkspaceDescriptionForm = ({ workspacePublicId, workspaceDescription, + disabled = false, }: { workspacePublicId: string; workspaceDescription: string; + disabled?: boolean; }) => { const utils = api.useUtils(); const { showPopup } = usePopup(); @@ -78,9 +80,10 @@ const UpdateWorkspaceDescriptionForm = ({

- {isDirty && ( + {isDirty && !disabled && (
); diff --git a/apps/web/src/views/settings/components/UpdateWorkspaceNameForm.tsx b/apps/web/src/views/settings/components/UpdateWorkspaceNameForm.tsx index 9996d3c4..6a2f7f94 100644 --- a/apps/web/src/views/settings/components/UpdateWorkspaceNameForm.tsx +++ b/apps/web/src/views/settings/components/UpdateWorkspaceNameForm.tsx @@ -20,9 +20,11 @@ type FormValues = z.infer; const UpdateWorkspaceNameForm = ({ workspacePublicId, workspaceName, + disabled = false, }: { workspacePublicId: string; workspaceName: string; + disabled?: boolean; }) => { const utils = api.useUtils(); const { showPopup } = usePopup(); @@ -70,9 +72,13 @@ const UpdateWorkspaceNameForm = ({ return (
- +
- {isDirty && ( + {isDirty && !disabled && (
- {isDirty && ( + {isDirty && !disabled && (