Skip to content

Commit

Permalink
feat: allow copying projects (#3393) (#3408) (#3448) (#3427)
Browse files Browse the repository at this point in the history
  • Loading branch information
ciyer authored Jan 6, 2025
1 parent bf3ec8b commit 772f0a2
Show file tree
Hide file tree
Showing 24 changed files with 1,747 additions and 54 deletions.
1 change: 1 addition & 0 deletions client/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.css
*.svg
# Generated files should not be linted
src/features/projectsV2/api/projectV2.api.ts
src/features/projectsV2/api/storagesV2.api.ts
src/features/dataConnectorsV2/api/data-connectors.api.ts
src/features/usersV2/api/users.generated-api.ts
8 changes: 8 additions & 0 deletions client/src/components/PrimaryAlert.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@import "~bootstrap/scss/functions";
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/variables-dark";
@import "../styles/renku_bootstrap_customization.scss";

.primaryAlert {
--bs-primary-bg-subtle: #{tint-color($primary, 90%)};
}
47 changes: 47 additions & 0 deletions client/src/components/PrimaryAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import cx from "classnames";
import { Alert } from "reactstrap";
import styles from "./PrimaryAlert.module.scss";

interface PrimaryAlertProps {
children: React.ReactNode;
"data-cy"?: string;
icon?: React.ReactNode;
className?: string;
}
export default function PrimaryAlert({
children,
icon,
...props
}: PrimaryAlertProps) {
return (
<Alert
color="primary"
isOpen
data-cy={props["data-cy"]}
className={cx(styles.primaryAlert, props.className, "overflow-y-auto")}
>
<div className={cx("d-flex", "gap-3")}>
{icon && <div>{icon}</div>}
<div className={cx("my-auto", "w-100")}>{children}</div>
</div>
</Alert>
);
}
50 changes: 45 additions & 5 deletions client/src/components/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import cx from "classnames";
import { Fragment, ReactNode, useRef, useState } from "react";
import { ArrowRight, ChevronDown, Pencil, PlusLg } from "react-bootstrap-icons";
import {
ArrowRight,
ChevronDown,
Pencil,
PlusLg,
ThreeDotsVertical,
} from "react-bootstrap-icons";
import { Link } from "react-router-dom";
import {
Button,
Expand Down Expand Up @@ -114,7 +120,7 @@ function ButtonWithMenu(props: ButtonWithMenuProps) {
);
}

interface BButtonWithMenuV2Props {
interface ButtonWithMenuV2Props {
children?: React.ReactNode;
className?: string;
color?: string;
Expand All @@ -125,7 +131,9 @@ interface BButtonWithMenuV2Props {
preventPropagation?: boolean;
size?: string;
}
export function ButtonWithMenuV2({
export const ButtonWithMenuV2 = SplitButtonWithMenu;

export function SplitButtonWithMenu({
children,
className,
color,
Expand All @@ -135,15 +143,15 @@ export function ButtonWithMenuV2({
id,
preventPropagation,
size,
}: BButtonWithMenuV2Props) {
}: ButtonWithMenuV2Props) {
// ! Temporary workaround to quickly implement a design solution -- to be removed ASAP #3250
const additionalProps = preventPropagation
? { onClick: (e: React.MouseEvent) => e.stopPropagation() }
: {};
return (
<UncontrolledDropdown
{...additionalProps}
className={cx(className)}
className={className}
color={color ?? "primary"}
direction={direction ?? "down"}
disabled={disabled}
Expand All @@ -165,6 +173,38 @@ export function ButtonWithMenuV2({
);
}

export function SingleButtonWithMenu({
children,
className,
color,
direction,
disabled,
id,
size,
}: Omit<ButtonWithMenuV2Props, "default" | "preventPropagation">) {
return (
<UncontrolledDropdown
className={className}
color={color ?? "primary"}
direction={direction ?? "down"}
disabled={disabled}
id={id}
size={size ?? "md"}
>
<DropdownToggle
caret={false}
data-bs-toggle="dropdown"
color={color ?? "primary"}
data-cy="button-with-menu-dropdown"
disabled={disabled}
>
<ThreeDotsVertical />
</DropdownToggle>
<DropdownMenu end>{children}</DropdownMenu>
</UncontrolledDropdown>
);
}

type RefreshButtonProps = {
action: () => void;
updating?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,115 @@
* limitations under the License.
*/

import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
import { useMemo } from "react";
import {
Bookmarks,
Clock,
Diagram3Fill,
Eye,
InfoCircle,
JournalAlbum,
People,
} from "react-bootstrap-icons";
import { Link, generatePath } from "react-router-dom-v5-compat";
import { Badge, Card, CardBody, CardHeader } from "reactstrap";

import { Loader } from "../../../../components/Loader";
import { TimeCaption } from "../../../../components/TimeCaption";
import {
EditButtonLink,
UnderlineArrowLink,
} from "../../../../components/buttons/Button";
import { UnderlineArrowLink } from "../../../../components/buttons/Button";
import { ABSOLUTE_ROUTES } from "../../../../routing/routes.constants";
import projectPreviewImg from "../../../../styles/assets/projectImagePreview.svg";
import PermissionsGuard from "../../../permissionsV2/PermissionsGuard";
import type {
ProjectMemberListResponse,
ProjectMemberResponse,
} from "../../../projectsV2/api/projectV2.api";
import {
useGetNamespacesByNamespaceSlugQuery,
useGetProjectsByProjectIdQuery,
useGetProjectsByProjectIdMembersQuery,
} from "../../../projectsV2/api/projectV2.enhanced-api";
import type { Project } from "../../../projectsV2/api/projectV2.api";
import { useProject } from "../../ProjectPageContainer/ProjectPageContainer";
import { getMemberNameToDisplay, toSortedMembers } from "../../utils/roleUtils";
import useProjectPermissions from "../../utils/useProjectPermissions.hook";

import ProjectInformationButton from "./ProjectInformationButton";
import styles from "./ProjectInformation.module.scss";

const MAX_MEMBERS_DISPLAYED = 5;

function ProjectCopyTemplateInformationBox({ project }: { project: Project }) {
const { data: templateProject, isLoading: isLoadingTemplateInformation } =
useGetProjectsByProjectIdQuery(
project.template_id
? {
projectId: project.template_id,
}
: skipToken
);
const { data: templateProjectNamespace } =
useGetNamespacesByNamespaceSlugQuery(
templateProject
? {
namespaceSlug: templateProject.namespace,
}
: skipToken
);

if (!project.template_id) return null;
if (isLoadingTemplateInformation) return <Loader />;
if (!templateProject || !templateProjectNamespace) {
const projectUrl = generatePath(ABSOLUTE_ROUTES.v2.projects.showById, {
id: project.template_id,
});
return (
<ProjectInformationBox
icon={<Diagram3Fill className="bi" />}
title="Copied from:"
>
<div className="mb-0">
<div>
<Link
color="outline-secondary"
className={cx("d-flex", "align-items-center")}
data-cy="copy-project-template-link"
to={projectUrl}
>
{project.template_id}
</Link>
</div>
</div>
</ProjectInformationBox>
);
}
const projectUrl = generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, {
namespace: templateProject.namespace,
slug: templateProject.slug,
});
return (
<ProjectInformationBox
icon={<Diagram3Fill className="bi" />}
title="Copied from:"
>
<div className="mb-0">
<div>
<Link
color="outline-secondary"
className={cx("d-flex", "align-items-center")}
data-cy="copy-project-template-link"
to={projectUrl}
>
{templateProjectNamespace.name ?? templateProjectNamespace.slug} /{" "}
{templateProject.name}
</Link>
</div>
</div>
</ProjectInformationBox>
);
}

interface ProjectInformationProps {
output?: "plain" | "card";
}
Expand Down Expand Up @@ -140,6 +214,7 @@ export default function ProjectInformation({
</p>
))}
</ProjectInformationBox>
<ProjectCopyTemplateInformationBox project={project} />
</div>
);
return output === "plain" ? (
Expand All @@ -160,23 +235,9 @@ export default function ProjectInformation({
</h4>

<div>
<PermissionsGuard
disabled={
<EditButtonLink
disabled={true}
to={settingsUrl}
tooltip="Your role does not allow modifying project information"
/>
}
enabled={
<EditButtonLink
data-cy="project-settings-edit"
to={settingsUrl}
tooltip="Modify project information"
/>
}
requestedPermission="write"
<ProjectInformationButton
userPermissions={permissions}
project={project}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* Copyright 2024 - Swiss Data Science Center (SDSC)
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
* Eidgenössische Technische Hochschule Zürich (ETHZ).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import cx from "classnames";
import { useCallback, useState } from "react";

import { DropdownItem } from "reactstrap";

import { SingleButtonWithMenu } from "../../../../components/buttons/Button";
import BootstrapCopyIcon from "../../../../components/icons/BootstrapCopyIcon";
import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook";

import type { Project } from "../../../projectsV2/api/projectV2.api";
import { useGetUserQuery } from "../../../usersV2/api/users.api";

import useProjectPermissions from "../../utils/useProjectPermissions.hook";
import ProjectCopyModal from "../../ProjectPageHeader/ProjectCopyModal";

export default function ProjectInformationButton({
project,
}: {
userPermissions: ReturnType<typeof useProjectPermissions>;
project: Project;
}) {
const { data: currentUser } = useGetUserQuery();
const [isCopyModalOpen, setCopyModalOpen] = useState(false);
const toggleCopyModal = useCallback(() => {
setCopyModalOpen((open) => !open);
}, []);
const userLogged = useLegacySelector<boolean>(
(state) => state.stateModel.user.logged
);
if (!userLogged) return null;
return (
<>
<SingleButtonWithMenu color="outline-primary" size="sm">
<DropdownItem
data-cy="project-copy-menu-item"
onClick={toggleCopyModal}
>
<BootstrapCopyIcon className={cx("bi")} />
<span className={cx("ms-2")}>Copy project</span>
</DropdownItem>
</SingleButtonWithMenu>
{
<ProjectCopyModal
currentUser={currentUser}
isOpen={isCopyModalOpen}
project={project}
toggle={toggleCopyModal}
/>
}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ export default function ProjectOverviewPage() {
</Row>
</Col>
<Col xs={12} md={4} xl={3}>
<ProjectInformation output="card" />
<div className="mb-3">
<ProjectInformation output="card" />
</div>
</Col>
</Row>
);
Expand Down
Loading

0 comments on commit 772f0a2

Please sign in to comment.