snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
198
apps/web/core/components/project/multi-select-modal.tsx
Normal file
198
apps/web/core/components/project/multi-select-modal.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { xor } from "lodash-es";
|
||||
import { observer } from "mobx-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
// plane ui
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { Logo } from "@plane/propel/emoji-icon-picker";
|
||||
import { CloseOutline, SearchOutline } from "@makeplane/propel/icons";
|
||||
import { Checkbox } from "@makeplane/propel/components/checkbox";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// assets
|
||||
import darkProjectAsset from "@/app/assets/empty-state/search/project-dark.webp?url";
|
||||
import lightProjectAsset from "@/app/assets/empty-state/search/project-light.webp?url";
|
||||
// components
|
||||
import { SimpleEmptyState } from "@/components/empty-state/simple-empty-state-root";
|
||||
// hooks
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedProjectIds: string[];
|
||||
projectIds: string[];
|
||||
onSubmit: (projectIds: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ProjectMultiSelectModal = observer(function ProjectMultiSelectModal(props: Props) {
|
||||
const { isOpen, onClose, selectedProjectIds: selectedProjectIdsProp, projectIds, onSubmit } = props;
|
||||
// states
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// refs
|
||||
const moveButtonRef = useRef<HTMLButtonElement>(null);
|
||||
// theme hook
|
||||
const { resolvedTheme } = useTheme();
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
// derived values
|
||||
const projectDetailsMap = useMemo(
|
||||
() => new Map(projectIds.map((id) => [id, getProjectById(id)])),
|
||||
[projectIds, getProjectById]
|
||||
);
|
||||
const areSelectedProjectsChanged = xor(selectedProjectIds, selectedProjectIdsProp).length > 0;
|
||||
const filteredProjectIds = projectIds.filter((id) => {
|
||||
const project = projectDetailsMap.get(id);
|
||||
const projectQuery = `${project?.identifier} ${project?.name}`.toLowerCase();
|
||||
return projectQuery.includes(searchTerm.toLowerCase());
|
||||
});
|
||||
const filteredProjectResolvedPath = resolvedTheme === "light" ? lightProjectAsset : darkProjectAsset;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setSelectedProjectIds(selectedProjectIdsProp);
|
||||
}, [isOpen, selectedProjectIdsProp]);
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setTimeout(() => {
|
||||
setSearchTerm("");
|
||||
setSelectedProjectIds([]);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
await onSubmit(selectedProjectIds);
|
||||
setIsSubmitting(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleSelectedProjectChange = (val: string[]) => {
|
||||
setSelectedProjectIds(val);
|
||||
setSearchTerm("");
|
||||
moveButtonRef.current?.focus();
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<ModalCore isOpen={isOpen} width={EModalWidth.LG} position={EModalPosition.TOP} handleClose={handleClose}>
|
||||
<Combobox as="div" multiple value={selectedProjectIds} onChange={handleSelectedProjectChange}>
|
||||
<div className="flex items-center gap-2 border-b border-subtle px-4">
|
||||
<SearchOutline className="size-4 flex-shrink-0 text-placeholder" aria-hidden="true" />
|
||||
<Combobox.Input
|
||||
className="h-12 w-full border-0 bg-transparent text-13 text-primary outline-none placeholder:text-placeholder focus:ring-0"
|
||||
placeholder="Search for projects"
|
||||
displayValue={() => ""}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{selectedProjectIds.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 px-4 pt-2">
|
||||
{selectedProjectIds.map((projectId) => {
|
||||
const projectDetails = projectDetailsMap.get(projectId);
|
||||
if (!projectDetails) return null;
|
||||
return (
|
||||
<div
|
||||
key={projectDetails.id}
|
||||
className="group flex cursor-pointer items-center gap-1.5 rounded-sm bg-surface-2 px-2 py-1"
|
||||
onClick={() => {
|
||||
handleSelectedProjectChange(selectedProjectIds.filter((id) => id !== projectDetails.id));
|
||||
}}
|
||||
>
|
||||
<Logo logo={projectDetails.logo_props} size={14} />
|
||||
<p className="truncate text-11 text-tertiary transition-colors group-hover:text-secondary">
|
||||
{projectDetails.identifier}
|
||||
</p>
|
||||
<CloseOutline className="size-3 flex-shrink-0 text-placeholder transition-colors group-hover:text-secondary" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Combobox.Options
|
||||
as="ul"
|
||||
static
|
||||
className="vertical-scrollbar scrollbar-md max-h-80 scroll-py-2 overflow-y-auto py-2 transition-[height] duration-200 ease-in-out"
|
||||
>
|
||||
{filteredProjectIds.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center px-3 py-8 text-center">
|
||||
<SimpleEmptyState
|
||||
title={t("workspace_projects.empty_state.filter.title")}
|
||||
description={t("workspace_projects.empty_state.filter.description")}
|
||||
assetPath={filteredProjectResolvedPath}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ul
|
||||
className={cn("text-primary", {
|
||||
"px-2": filteredProjectIds.length > 0,
|
||||
})}
|
||||
>
|
||||
{filteredProjectIds.map((projectId) => {
|
||||
const projectDetails = projectDetailsMap.get(projectId);
|
||||
if (!projectDetails) return null;
|
||||
const isProjectSelected = selectedProjectIds.includes(projectDetails.id);
|
||||
return (
|
||||
<Combobox.Option
|
||||
as="li"
|
||||
key={projectDetails.id}
|
||||
value={projectDetails.id}
|
||||
className={({ active }) =>
|
||||
cn(
|
||||
"flex w-full cursor-pointer items-center justify-between gap-2 truncate rounded-md p-2 text-secondary transition-colors select-none",
|
||||
{
|
||||
"bg-layer-1": active,
|
||||
"text-primary": isProjectSelected,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<span className="flex flex-shrink-0 items-center gap-2.5">
|
||||
<span className="pointer-events-none">
|
||||
<Checkbox checked={isProjectSelected} aria-label={projectDetails.name} />
|
||||
</span>
|
||||
<Logo logo={projectDetails.logo_props} size={16} />
|
||||
</span>
|
||||
<span className="flex-shrink-0 text-10">{projectDetails.identifier}</span>
|
||||
<p className="truncate text-13">{projectDetails.name}</p>
|
||||
</div>
|
||||
</Combobox.Option>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
<div className="flex items-center justify-end gap-2 border-t border-subtle p-3">
|
||||
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
ref={moveButtonRef}
|
||||
variant="primary"
|
||||
size="lg"
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting}
|
||||
disabled={!areSelectedProjectsChanged}
|
||||
>
|
||||
{isSubmitting ? t("confirming") : t("confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalCore>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user