From d0424e087590581d3513b07d7834132437ef51b2 Mon Sep 17 00:00:00 2001
From: Lorenzo
Date: Thu, 5 Jun 2025 17:17:16 +0200
Subject: [PATCH 1/2] TMP keywords
---
.../src/components/keywords/KeywordBadge.tsx | 65 ++++++
.../components/keywords/KeywordContainer.tsx | 47 ++++
.../src/components/renkuBadge/RenkuBadge.tsx | 59 ++++++
.../ProjectInformation/ProjectInformation.tsx | 26 ++-
.../Settings/ProjectKeywordsFormField.tsx | 122 +++++++++++
.../Settings/ProjectSettings.tsx | 47 ++--
.../settings/projectSettings.types.ts | 4 +
.../DataConnectorModalBody.tsx | 200 +++++++++++++++---
.../components/DataConnectorModal/index.tsx | 7 +-
.../components/DataConnectorView.tsx | 27 +--
.../components/dataConnector.utils.ts | 4 +
.../storybook/bootstrap/BadgeInfo.stories.tsx | 48 ++---
tests/cypress/e2e/projectV2.spec.ts | 48 +++++
.../projectV2/update-projectV2-metadata.json | 3 +-
14 files changed, 594 insertions(+), 113 deletions(-)
create mode 100644 client/src/components/keywords/KeywordBadge.tsx
create mode 100644 client/src/components/keywords/KeywordContainer.tsx
create mode 100644 client/src/components/renkuBadge/RenkuBadge.tsx
create mode 100644 client/src/features/ProjectPageV2/ProjectPageContent/Settings/ProjectKeywordsFormField.tsx
diff --git a/client/src/components/keywords/KeywordBadge.tsx b/client/src/components/keywords/KeywordBadge.tsx
new file mode 100644
index 0000000000..a4227172ac
--- /dev/null
+++ b/client/src/components/keywords/KeywordBadge.tsx
@@ -0,0 +1,65 @@
+/*!
+ * Copyright 2025 - 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 RenkuBadge from "../renkuBadge/RenkuBadge";
+import { XCircle } from "react-bootstrap-icons";
+
+interface KeywordBadgeProps {
+ children?: React.ReactNode;
+ className?: string;
+ "data-cy"?: string;
+ highlighted?: boolean;
+ removable?: boolean;
+ removeHandler?: () => void;
+}
+
+export default function KeywordBadge({
+ children,
+ className,
+ "data-cy": dataCy = "keyword",
+ highlighted,
+ removable = true,
+ removeHandler,
+}: KeywordBadgeProps) {
+ const remove =
+ removable && removeHandler ? (
+
+ ) : null;
+
+ return (
+
+ {children}
+ {remove}
+
+ );
+}
diff --git a/client/src/components/keywords/KeywordContainer.tsx b/client/src/components/keywords/KeywordContainer.tsx
new file mode 100644
index 0000000000..50ca1dc51f
--- /dev/null
+++ b/client/src/components/keywords/KeywordContainer.tsx
@@ -0,0 +1,47 @@
+/*!
+ * Copyright 2025 - 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";
+
+interface KeywordContainerProps {
+ children?: React.ReactNode;
+ className?: string;
+ "data-cy"?: string;
+}
+
+export default function KeywordContainer({
+ children,
+ className,
+ "data-cy": dataCy,
+}: KeywordContainerProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/components/renkuBadge/RenkuBadge.tsx b/client/src/components/renkuBadge/RenkuBadge.tsx
new file mode 100644
index 0000000000..aa232e9a62
--- /dev/null
+++ b/client/src/components/renkuBadge/RenkuBadge.tsx
@@ -0,0 +1,59 @@
+/*!
+ * Copyright 2025 - 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";
+
+interface RenkuBadgeProps {
+ children?: React.ReactNode;
+ className?: string;
+ color?: "success" | "danger" | "warning" | "light";
+ "data-cy"?: string;
+ pills?: boolean;
+}
+
+export default function RenkuBadge({
+ children,
+ className,
+ color = "light",
+ "data-cy": dataCy,
+ pills = false,
+}: RenkuBadgeProps) {
+ const colorClasses =
+ color === "success"
+ ? ["border-success", "bg-success-subtle", "text-success-emphasis"]
+ : color === "danger"
+ ? ["border-danger", "bg-danger-subtle", "text-danger-emphasis"]
+ : color === "warning"
+ ? ["border-warning", "bg-warning-subtle", "text-warning-emphasis"]
+ : ["border-dark-subtle", "bg-light", "text-dark-emphasis"];
+
+ const baseClasses = [
+ "border",
+ "badge",
+ pills ? "rounded-pill" : "",
+ ...colorClasses,
+ ];
+
+ const finalClasses = className ? cx(className, baseClasses) : cx(baseClasses);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx
index d2591d9da5..263edecf63 100644
--- a/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx
+++ b/client/src/features/ProjectPageV2/ProjectPageContent/ProjectInformation/ProjectInformation.tsx
@@ -30,28 +30,28 @@ import {
} from "react-bootstrap-icons";
import { Link, generatePath } from "react-router";
import { Badge, Card, CardBody, CardHeader } from "reactstrap";
-
+import KeywordBadge from "~/components/keywords/KeywordBadge";
+import KeywordContainer from "~/components/keywords/KeywordContainer";
import { Loader } from "../../../../components/Loader";
import { TimeCaption } from "../../../../components/TimeCaption";
import { UnderlineArrowLink } from "../../../../components/buttons/Button";
import { ABSOLUTE_ROUTES } from "../../../../routing/routes.constants";
import projectPreviewImg from "../../../../styles/assets/projectImagePreview.svg";
import type {
+ Project,
ProjectMemberListResponse,
ProjectMemberResponse,
} from "../../../projectsV2/api/projectV2.api";
import {
useGetNamespacesByNamespaceSlugQuery,
- useGetProjectsByProjectIdQuery,
useGetProjectsByProjectIdMembersQuery,
+ useGetProjectsByProjectIdQuery,
} 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";
+import ProjectInformationButton from "./ProjectInformationButton";
const MAX_MEMBERS_DISPLAYED = 5;
@@ -164,6 +164,12 @@ export default function ProjectInformation({
}),
[namespace?.namespace_kind, project.namespace]
);
+ const keywordsSorted = useMemo(() => {
+ if (!project.keywords) return [];
+ return project.keywords
+ .map((keyword) => keyword.trim())
+ .sort((a, b) => a.localeCompare(b));
+ }, [project.keywords]);
const information = (
+ Keywords help orginizing your work and are available to search. You
+ can use them to group elements that belong together or to create
+ specific topics. You can add multiple keywords.
+
+
+
{flatDataConnector.dataConnectorId == null &&
hasPasswordFieldWithInput &&
validationResult?.isSuccess && (
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorModal/index.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorModal/index.tsx
index 2e8c15eaf5..c653354fe1 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorModal/index.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorModal/index.tsx
@@ -21,7 +21,6 @@ import cx from "classnames";
import { useCallback, useEffect } from "react";
import { Database, XLg } from "react-bootstrap-icons";
import { Button, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
-
import { ErrorAlert } from "../../../../components/Alert";
import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert";
import { Loader } from "../../../../components/Loader";
@@ -43,11 +42,10 @@ import {
dataConnectorToFlattened,
getDataConnectorScope,
} from "../dataConnector.utils";
+import styles from "./DataConnectorModal.module.scss";
import DataConnectorModalBody from "./DataConnectorModalBody";
import DataConnectorModalFooter from "./DataConnectorModalFooter";
-import styles from "./DataConnectorModal.module.scss";
-
export function DataConnectorModalBodyAndFooter({
dataConnector = null,
isOpen,
@@ -108,8 +106,9 @@ export function DataConnectorModalBodyAndFooter({
) : (
)}
diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
index 55dc1933b0..8332eb736c 100644
--- a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
+++ b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
import { useMemo, useRef } from "react";
@@ -29,12 +30,9 @@ import {
PersonBadge,
} from "react-bootstrap-icons";
import { Link, generatePath } from "react-router";
-import {
- Badge,
- Offcanvas,
- OffcanvasBody,
- UncontrolledTooltip,
-} from "reactstrap";
+import { Offcanvas, OffcanvasBody, UncontrolledTooltip } from "reactstrap";
+import KeywordBadge from "~/components/keywords/KeywordBadge";
+import KeywordContainer from "~/components/keywords/KeywordContainer";
import { WarnAlert } from "../../../components/Alert";
import { Clipboard } from "../../../components/clipboard/Clipboard";
import { Loader } from "../../../components/Loader";
@@ -432,6 +430,13 @@ function DataConnectorViewMetadata({
[dataConnector.storage.configuration, scope]
);
+ const sortedKeywords = useMemo(() => {
+ if (!dataConnector.keywords) return [];
+ return dataConnector.keywords
+ .map((keyword) => keyword.trim())
+ .sort((a, b) => a.localeCompare(b));
+ }, [dataConnector.keywords]);
+
const dataConnectorSource = useGetDataConnectorSource(dataConnector);
return (
@@ -544,13 +549,11 @@ function DataConnectorViewMetadata({
{dataConnector.keywords && dataConnector.keywords.length > 0 && (
-