From 65c93c92ab7011403db9dd5785e5b0acaef396da Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Fri, 29 Mar 2024 12:15:45 -0400 Subject: [PATCH 1/2] :ghost: Refactor tags table to remove deprecated legacy table dep Signed-off-by: Ian Bolton --- client/public/locales/en/translation.json | 4 +- .../labels/item-tag-label/item-tag-label.tsx | 2 +- .../controls/tags/components/tag-table.tsx | 17 +- client/src/app/pages/controls/tags/tags.tsx | 571 +++++++++--------- 4 files changed, 289 insertions(+), 305 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 379a37d354..2b7b436d0d 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -220,7 +220,9 @@ "toTagApplication": "Either no tags exist yet or you may not have permission to view any. If you have permission, try creating a new custom tag.", "unsavedChanges": "Are you sure you want to close the assessment? Any unsaved changes will be lost.", "noAnswers": "Are you sure you want to close the assessment? There are no answers to save.", - "unlinkTicket": "Unlink from Jira" + "unlinkTicket": "Unlink from Jira", + "noTagsAvailable": "No tags available", + "noAssociatedTags": "This tag category has no associated tags." }, "proposedActions": { "refactor": "Refactor", diff --git a/client/src/app/components/labels/item-tag-label/item-tag-label.tsx b/client/src/app/components/labels/item-tag-label/item-tag-label.tsx index ef6ad228a4..55b673f402 100644 --- a/client/src/app/components/labels/item-tag-label/item-tag-label.tsx +++ b/client/src/app/components/labels/item-tag-label/item-tag-label.tsx @@ -3,7 +3,7 @@ import { Tag, TagCategory } from "@app/api/models"; import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants"; import { LabelCustomColor } from "@app/components/LabelCustomColor"; -export const getTagCategoryFallbackColor = (category?: TagCategory) => { +export const getTagCategoryFallbackColor = (category?: TagCategory | null) => { if (!category?.id) return COLOR_HEX_VALUES_BY_NAME.gray; const colorValues = Object.values(COLOR_HEX_VALUES_BY_NAME); return colorValues[category?.id % colorValues.length]; diff --git a/client/src/app/pages/controls/tags/components/tag-table.tsx b/client/src/app/pages/controls/tags/components/tag-table.tsx index ee9fcfc413..4f27cc3108 100644 --- a/client/src/app/pages/controls/tags/components/tag-table.tsx +++ b/client/src/app/pages/controls/tags/components/tag-table.tsx @@ -9,8 +9,6 @@ import { Td, ActionsColumn, IAction, - cellWidth, - ICell, IRow, IRowData, } from "@patternfly/react-table"; @@ -36,17 +34,6 @@ export const TagTable: React.FC = ({ }) => { const { t } = useTranslation(); - const columns: ICell[] = [ - { - title: t("terms.tagName"), - transforms: [cellWidth(100)], - cellFormatters: [], - props: { - className: "columnPadding", - }, - }, - ]; - const rows: IRow[] = []; (tagCategory.tags || []) .sort((a, b) => a.name.localeCompare(b.name)) @@ -94,9 +81,7 @@ export const TagTable: React.FC = ({ const rowActions = defaultActions(row); return ( - {row.cells?.map((cell: any) => ( - {cell.title} - ))} + {row.cells?.map((cell: any) => {cell.title})} {rowActions && } diff --git a/client/src/app/pages/controls/tags/tags.tsx b/client/src/app/pages/controls/tags/tags.tsx index ca545555a4..c2bbad551e 100644 --- a/client/src/app/pages/controls/tags/tags.tsx +++ b/client/src/app/pages/controls/tags/tags.tsx @@ -1,36 +1,34 @@ import React from "react"; import { AxiosError } from "axios"; import { useTranslation } from "react-i18next"; -import { useSelectionState } from "@migtools/lib-ui"; import { Button, ButtonVariant, + EmptyState, + EmptyStateBody, + EmptyStateIcon, Modal, ModalVariant, + Title, + Toolbar, + ToolbarContent, ToolbarGroup, ToolbarItem, } from "@patternfly/react-core"; import { - expandable, - ICell, - IExtraData, - IRow, - IRowData, - sortable, + ExpandableRowContent, + Table, + Tbody, + Td, + Th, + Thead, + Tr, } from "@patternfly/react-table"; +import { CubesIcon } from "@patternfly/react-icons"; import { dedupeFunction, getAxiosErrorMessage } from "@app/utils/utils"; import { Tag, TagCategory } from "@app/api/models"; -import { TagTable } from "./components/tag-table"; -import { useLegacyPaginationState } from "@app/hooks/useLegacyPaginationState"; -import { - FilterCategory, - FilterToolbar, - FilterType, -} from "@app/components/FilterToolbar"; -import { useLegacyFilterState } from "@app/hooks/useLegacyFilterState"; -import { useLegacySortState } from "@app/hooks/useLegacySortState"; -import { controlsWriteScopes, RBAC, RBAC_TYPE } from "@app/rbac"; +import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { useDeleteTagMutation, useDeleteTagCategoryMutation, @@ -45,15 +43,16 @@ import { AppTableActionButtons } from "@app/components/AppTableActionButtons"; import { Color } from "@app/components/Color"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { AppTableWithControls } from "@app/components/AppTableWithControls"; -import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; - -const ENTITY_FIELD = "entity"; - -const getRow = (rowData: IRowData): TagCategory => { - return rowData[ENTITY_FIELD]; -}; +import { SimplePagination } from "@app/components/SimplePagination"; +import { + TableHeaderContentWithControls, + ConditionalTableBody, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { useLocalTableControls } from "@app/hooks/table-controls"; +import { RBAC, controlsWriteScopes, RBAC_TYPE } from "@app/rbac"; +import { TagTable } from "./components/tag-table"; export const Tags: React.FC = () => { const { t } = useTranslation(); @@ -70,10 +69,6 @@ export const Tags: React.FC = () => { const tagCategoryToUpdate = tagCategoryModalState !== "create" ? tagCategoryModalState : null; - // const [isNewTagCategoryModalOpen, setIsNewTagCategoryModalOpen] = - // useState(false); - // const [rowToUpdate, setRowToUpdate] = useState(); - const [tagModalState, setTagModalState] = React.useState< "create" | Tag | null >(null); @@ -157,281 +152,283 @@ export const Tags: React.FC = () => { refetch, } = useFetchTagCategories(); - const { - isItemSelected: isItemExpanded, - toggleItemSelected: toggleItemExpanded, - } = useSelectionState({ - items: tagCategories || [], - isEqual: (a, b) => a.id === b.id, - }); - - const filterCategories: FilterCategory< - TagCategory, - "tags" | "rank" | "color" - >[] = [ - { - categoryKey: "tags", - title: t("terms.name"), - type: FilterType.multiselect, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", - getItemValue: (item) => { - const tagCategoryNames = item.name?.toString() || ""; - const tagNames = item?.tags - ?.map((tag) => tag.name) - .concat(tagCategoryNames) - .join(""); - - return tagNames || ""; - }, - selectOptions: dedupeFunction( - tagCategories - ?.map((tagCategory) => tagCategory?.tags) - .flat() - .filter((tag) => tag && tag.name) - .map((tag) => ({ key: tag?.name, value: tag?.name })) - .concat( - tagCategories?.map((tagCategory) => ({ - key: tagCategory?.name, - value: tagCategory?.name, - })) - ) - .sort((a, b) => { - if (a.value && b.value) { - return a?.value.localeCompare(b?.value); - } else { - return 0; - } - }) - ), - }, - { - categoryKey: "rank", - title: t("terms.rank"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.rank").toLowerCase(), - }) + "...", - getItemValue: (item) => { - return item.rank?.toString() || ""; - }, - }, - { - categoryKey: "color", - title: t("terms.color"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.color").toLowerCase(), - }) + "...", - getItemValue: (item) => { - const hex = item?.colour || ""; - const colorLabel = COLOR_NAMES_BY_HEX_VALUE[hex.toLowerCase()]; - return colorLabel || hex; - }, - }, - ]; - const { filterValues, setFilterValues, filteredItems } = useLegacyFilterState( - tagCategories || [], - filterCategories - ); - - const getSortValues = (item: TagCategory) => [ - "", - item?.name || "", - typeof item?.rank === "number" ? item.rank : Number.MAX_VALUE, - - "", - item?.tags?.length || 0, - "", // Action column - ]; - - const { sortBy, onSort, sortedItems } = useLegacySortState( - filteredItems, - getSortValues - ); - - const { currentPageItems, setPageNumber, paginationProps } = - useLegacyPaginationState(sortedItems, 10); - - const deleteTagFromTable = (row: Tag) => { - setTagToDelete(row); + const deleteTagFromTable = (tag: Tag) => { + setTagToDelete(tag); }; - const columns: ICell[] = [ - { - title: t("terms.tagCategory"), - transforms: [sortable], - cellFormatters: [expandable], - }, - { title: t("terms.rank"), transforms: [sortable] }, - { - title: t("terms.color"), - transforms: [], - }, - { - title: t("terms.tagCount"), - transforms: [sortable], + const tableControls = useLocalTableControls({ + tableName: "business-services-table", + idProperty: "name", + items: tagCategories, + columnNames: { + name: t("terms.name"), + rank: t("terms.rank"), + color: t("terms.color"), + tagCount: t("terms.tagCount"), }, - { - title: "", - props: { - className: "pf-v5-u-text-align-right", - }, - }, - ]; - - const rows: IRow[] = []; - currentPageItems.forEach((item) => { - const isExpanded = isItemExpanded(item) && !!item?.tags?.length; - const categoryColor = item.colour || getTagCategoryFallbackColor(item); - rows.push({ - [ENTITY_FIELD]: item, - isOpen: (item.tags || []).length > 0 ? isExpanded : undefined, - cells: [ - { - title: item.name, - }, - { - title: item.rank, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + hasActionsColumn: true, + isExpansionEnabled: true, + expandableVariant: "single", + filterCategories: [ + { + categoryKey: "tags", + title: t("terms.name"), + type: FilterType.multiselect, + placeholderText: + t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }) + "...", + getItemValue: (item) => { + const tagCategoryNames = item.name?.toString() || ""; + const tagNames = item?.tags + ?.map((tag) => tag.name) + .concat(tagCategoryNames) + .join(""); + return tagNames || ""; }, - { - title: , - }, - { - title: item.tags ? item.tags.length : 0, + selectOptions: dedupeFunction( + tagCategories + ?.map((tagCategory) => tagCategory?.tags) + .flat() + .filter((tag) => tag && tag.name) + .map((tag) => ({ key: tag?.name, value: tag?.name })) + .concat( + tagCategories?.map((tagCategory) => ({ + key: tagCategory?.name, + value: tagCategory?.name, + })) + ) + .sort((a, b) => { + if (a.value && b.value) { + return a?.value.localeCompare(b?.value); + } else { + return 0; + } + }) + ), + }, + { + categoryKey: "rank", + title: t("terms.rank"), + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.rank").toLowerCase(), + }) + "...", + getItemValue: (item) => { + return item.rank?.toString() || ""; }, - { - title: ( - setTagCategoryModalState(item)} - onDelete={() => deleteRow(item)} - /> - ), + }, + { + categoryKey: "color", + title: t("terms.color"), + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.color").toLowerCase(), + }) + "...", + getItemValue: (item) => { + const hex = item?.colour || ""; + const colorLabel = COLOR_NAMES_BY_HEX_VALUE[hex.toLowerCase()]; + return colorLabel || hex; }, - ], - }); - - if (isExpanded) { - rows.push({ - parent: rows.length - 1, - fullWidth: true, - noPadding: true, - cells: [ - { - title: ( -
- -
- ), - }, - ], - }); - } + }, + ], + initialItemsPerPage: 10, + sortableColumns: ["name", "rank", "tagCount"], + initialSort: { columnKey: "name", direction: "asc" }, + getSortValues: (item) => ({ + name: item?.name || "", + rank: typeof item?.rank === "number" ? item.rank : Number.MAX_VALUE, + tagCount: item?.tags?.length || 0, + }), + isLoading: isFetching, }); - // Rows - - const collapseRow = ( - event: React.MouseEvent, - rowIndex: number, - isOpen: boolean, - rowData: IRowData, - extraData: IExtraData - ) => { - const row = getRow(rowData); - toggleItemExpanded(row); - }; - - const deleteRow = (row: TagCategory) => { - setTagCategoryToDelete(row); - }; - - // Advanced filters - - const handleOnClearAllFilters = () => { - setFilterValues({}); - }; + const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTrProps, + getTdProps, + }, + expansionDerivedState: { isCellExpanded }, + } = tableControls; + const categoryColor = + tagCategoryToUpdate?.colour || + getTagCategoryFallbackColor(tagCategoryToUpdate); return ( <> } > - - } - toolbarActions={ - - - - - - - + + + + + + + + + + + + + + + + + + + + + {t("composed.noDataStateTitle", { + what: t("terms.tags").toLowerCase(), + })} + + + {t("composed.noDataStateBody", { + what: t("terms.tags").toLowerCase(), + })} + + + } + numRenderedColumns={numRenderedColumns} + > + {currentPageItems?.map((tagCategory, rowIndex) => { + const hasTags = tagCategory.tags && tagCategory.tags.length > 0; + const categoryColor = + tagCategory.colour || + getTagCategoryFallbackColor(tagCategory); + + return ( + - {t("actions.createTagCategory")} - - - - - } - noDataState={ - + + + + + + + + + {isCellExpanded(tagCategory) && ( + + + + )} + + ); })} - // t('terms.stakeholderGroup') - description={t("composed.noDataStateBody", { - what: t("terms.tagCategory").toLowerCase(), - })} - /> - } - /> + +
+ + + + +
+ {tagCategory.name} + + {tagCategory.rank} + + + + {tagCategory.tags?.length || 0} + + setTagCategoryModalState(tagCategory)} + onDelete={() => setTagCategoryToDelete(tagCategory)} + /> +
+ + {hasTags ? ( + + ) : ( + + + + {t("message.noTagsAvailable")} + + + {t("message.noAssociatedTags")} + + + )} + +
+ +
Date: Thu, 4 Apr 2024 15:04:30 -0400 Subject: [PATCH 2/2] PR suggestions Signed-off-by: Ian Bolton --- client/public/locales/en/translation.json | 1 + .../controls/tags/components/tag-table.tsx | 73 +++++-------------- client/src/app/pages/controls/tags/tags.tsx | 38 +++++----- 3 files changed, 39 insertions(+), 73 deletions(-) diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 2b7b436d0d..edcbefc65d 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -178,6 +178,7 @@ "blockedDeleteTarget": "Cannot delete {{what}} because it is associated with a target.", "defaultBlockedDelete": "Cannot delete {{what}} because it is associated with another object.", "cannotDeleteApplicationsAssignedToMigrationWave": "Cannot delete applications that are assigned to a migration wave.", + "cannotDeleteNonEmptyTagCategory": "Cannot delete a tag category that contains tags.", "continueConfirmation": "Yes, continue", "copyAssessmentAndReviewBody": "Some of the selected target applications have an in-progress or complete assessment/review. By continuing, the existing assessment(s)/review(s) will be replaced by the copied assessment/review. Do you wish to continue?", "copyAssessmentAndReviewQuestion": "Copy assessment and review?", diff --git a/client/src/app/pages/controls/tags/components/tag-table.tsx b/client/src/app/pages/controls/tags/components/tag-table.tsx index 4f27cc3108..6bc612288a 100644 --- a/client/src/app/pages/controls/tags/components/tag-table.tsx +++ b/client/src/app/pages/controls/tags/components/tag-table.tsx @@ -8,19 +8,10 @@ import { Tbody, Td, ActionsColumn, - IAction, - IRow, - IRowData, } from "@patternfly/react-table"; import { Tag, TagCategory } from "@app/api/models"; import "./tag-table.css"; -const ENTITY_FIELD = "entity"; - -const getRow = (rowData: IRowData): Tag => { - return rowData[ENTITY_FIELD]; -}; - export interface TabTableProps { tagCategory: TagCategory; onEdit: (tag: Tag) => void; @@ -28,66 +19,42 @@ export interface TabTableProps { } export const TagTable: React.FC = ({ - tagCategory: tagCategory, + tagCategory, onEdit, onDelete, }) => { const { t } = useTranslation(); - const rows: IRow[] = []; - (tagCategory.tags || []) - .sort((a, b) => a.name.localeCompare(b.name)) - .forEach((item) => { - rows.push({ - [ENTITY_FIELD]: item, - noPadding: true, - cells: [ - { - title: item.name, - }, - ], - }); - }); - - const editRow = (row: Tag) => { - onEdit(row); - }; - - const deleteRow = (row: Tag) => { - onDelete(row); - }; - - const defaultActions = (tag: IRowData): IAction[] => [ - { - title: t("actions.edit"), - onClick: () => editRow(getRow(tag)), - }, - { - title: t("actions.delete"), - onClick: () => deleteRow(getRow(tag)), - }, - ]; - return ( - + - {rows.map((row: IRow) => { - const rowActions = defaultActions(row); - return ( - - {row.cells?.map((cell: any) => )} + {(tagCategory.tags || []) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((tag) => ( + + - ); - })} + ))}
{t("terms.tagName")}
{cell.title}
{tag.name} - {rowActions && } + onEdit(tag), + }, + { + title: t("actions.delete"), + onClick: () => onDelete(tag), + }, + ]} + />
); diff --git a/client/src/app/pages/controls/tags/tags.tsx b/client/src/app/pages/controls/tags/tags.tsx index c2bbad551e..19361cf990 100644 --- a/client/src/app/pages/controls/tags/tags.tsx +++ b/client/src/app/pages/controls/tags/tags.tsx @@ -26,7 +26,11 @@ import { } from "@patternfly/react-table"; import { CubesIcon } from "@patternfly/react-icons"; -import { dedupeFunction, getAxiosErrorMessage } from "@app/utils/utils"; +import { + dedupeFunction, + getAxiosErrorMessage, + localeNumericCompare, +} from "@app/utils/utils"; import { Tag, TagCategory } from "@app/api/models"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { @@ -53,6 +57,7 @@ import { import { useLocalTableControls } from "@app/hooks/table-controls"; import { RBAC, controlsWriteScopes, RBAC_TYPE } from "@app/rbac"; import { TagTable } from "./components/tag-table"; +import i18n from "@app/i18n"; export const Tags: React.FC = () => { const { t } = useTranslation(); @@ -177,10 +182,9 @@ export const Tags: React.FC = () => { categoryKey: "tags", title: t("terms.name"), type: FilterType.multiselect, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", + placeholderText: t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }), getItemValue: (item) => { const tagCategoryNames = item.name?.toString() || ""; const tagNames = item?.tags @@ -191,23 +195,18 @@ export const Tags: React.FC = () => { }, selectOptions: dedupeFunction( tagCategories - ?.map((tagCategory) => tagCategory?.tags) - .flat() + ?.flatMap((tagCategory) => tagCategory?.tags ?? []) .filter((tag) => tag && tag.name) - .map((tag) => ({ key: tag?.name, value: tag?.name })) + .map((tag) => ({ key: tag.name, value: tag.name })) .concat( tagCategories?.map((tagCategory) => ({ key: tagCategory?.name, value: tagCategory?.name, - })) + })) ?? [] + ) + .sort((a, b) => + localeNumericCompare(a.value, b.value, i18n.language) ) - .sort((a, b) => { - if (a.value && b.value) { - return a?.value.localeCompare(b?.value); - } else { - return 0; - } - }) ), }, { @@ -264,9 +263,6 @@ export const Tags: React.FC = () => { expansionDerivedState: { isCellExpanded }, } = tableControls; - const categoryColor = - tagCategoryToUpdate?.colour || - getTagCategoryFallbackColor(tagCategoryToUpdate); return ( <> { setTagCategoryModalState(tagCategory)} onDelete={() => setTagCategoryToDelete(tagCategory)} />