diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTable.tsx b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTable.tsx new file mode 100644 index 0000000..a4121fa --- /dev/null +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTable.tsx @@ -0,0 +1,93 @@ +import { Table } from "optimizely-oui"; +import React, { useCallback } from "react"; +import { translations } from "../../../translations"; +import { ContentUsageDto } from "../../../dtos"; +import { TableColumn } from "../../../types"; +import ContentTypeUsageTableRow from "./ContentTypeUsageTableRow/ContentTypeUsageTableRow"; +import { navigateTo } from "../../../routes"; + +enum ContentTypeUsageTableColumn { + ID = "id", + Name = "name", + LanguageBranch = "languageBranch", + PageUrl = "pageUrl", + Actions = "actions", +} + +interface ContentTypeUsagesTableProps { + tableColumns: TableColumn[]; + rows: ContentUsageDto[]; + onSortChange: (column: TableColumn) => void; + sortDirection: string; +} + +const ContentTypeUsagesTable = ({ + tableColumns, + rows, + onSortChange, + sortDirection, +}: ContentTypeUsagesTableProps) => { + const onTableRowClick = useCallback( + (url?: string | null, alwaysTriggerClick = false) => + (event: React.PointerEvent) => { + if (!url) return; + + const target = event.target as HTMLTableCellElement | undefined; + + if ((target && target.tagName === "TD") || alwaysTriggerClick) { + navigateTo(url, true); + } + }, + [navigateTo] + ); + + return ( + + + + {tableColumns + .filter((column) => column.visible) + .map((column) => ( + onSortChange(column), + order: sortDirection, + }} + key={column.id} + > + {column.name} + + ))} + + + + + + {rows.length > 0 ? ( + rows.map( + (row) => ( + + ) + ) + ) : ( + + {translations.noResults} + + )} + +
+ ); +}; + +export default ContentTypeUsagesTable; +export { ContentTypeUsageTableColumn }; diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTableRow/ContentTypeUsageTableRow.tsx b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTableRow/ContentTypeUsageTableRow.tsx new file mode 100644 index 0000000..b0341c6 --- /dev/null +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTableRow/ContentTypeUsageTableRow.tsx @@ -0,0 +1,51 @@ +import { Table } from "optimizely-oui"; +import { useTranslations } from "../../../../Contexts/TranslationsProvider"; +import { ContentUsageDto } from "../../../../dtos"; +import { TableColumn } from "../../../../types"; +import PageUrlCell from "../../PageUrlCell/PageUrlCell"; +import React from "react"; +import { ContentTypeUsageTableColumn } from "../ContentTypeUsageTable"; +import { useHoverTrackingHandlers } from "../../../../Lib/hooks/useHoverTrackingHandlers"; + +interface ContentTypeUsageTableRowProps extends ContentUsageDto { + tableColumns: TableColumn[]; + onRowClick?: (event: React.PointerEvent) => void; +} + +const ContentTypeUsageTableRow = ({ + tableColumns, + onRowClick, + ...row + }: ContentTypeUsageTableRowProps) => { + const { + views: { + contentUsagesView: { + table: { actions }, + }, + }, + } = useTranslations(); + + const [isUrlHovered, urlHoveredHandlers] = useHoverTrackingHandlers(); + const actionLabel = isUrlHovered ? actions.view : actions.edit; + + return ( + + { tableColumns + .filter((column) => column.visible) + .map((column) => + + { column.id.toString() === ContentTypeUsageTableColumn.PageUrl && + row.pageUrls.length > 0 ? ( + + ) : ( + row[column.id] + )} + + )} + {actionLabel + " >"} + + ); + }; + +export default ContentTypeUsageTableRow; + \ No newline at end of file diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlCell.tsx b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlCell.tsx new file mode 100644 index 0000000..1a273f1 --- /dev/null +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlCell.tsx @@ -0,0 +1,25 @@ +import { Disclose } from "optimizely-oui"; +import React from "react"; +import PageUrlLink from "./PageUrlLink/PageUrlLink"; +import { HoverHandlers } from "../../../Lib/hooks/useHoverTrackingHandlers"; + +interface PageUrlCellProps { + pageUrls: string[]; + urlHoveredHandlers: HoverHandlers; +} + +const PageUrlCell = ({pageUrls, urlHoveredHandlers}: PageUrlCellProps) => { + const isManyUrls = pageUrls.length > 1; + + return isManyUrls ? ( + + {pageUrls.map((pageUrl, index) => ( + + ))} + + ) : ( + + ); + } + + export default PageUrlCell; \ No newline at end of file diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlLink/PageUrlLink.tsx b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlLink/PageUrlLink.tsx new file mode 100644 index 0000000..9e7ed03 --- /dev/null +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Components/Tables/PageUrlCell/PageUrlLink/PageUrlLink.tsx @@ -0,0 +1,23 @@ +import { Link } from "optimizely-oui"; +import React from "react"; +import { HoverHandlers } from "../../../../Lib/hooks/useHoverTrackingHandlers"; + +interface PageUrlLinkProps { + pageUrl: string; + urlHoveredHandlers: HoverHandlers; +} + +const PageUrlLink = ({ pageUrl, urlHoveredHandlers }: PageUrlLinkProps) => { + return ( + +
+ {pageUrl} +
+ + ); +}; + +export default PageUrlLink; \ No newline at end of file diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Lib/hooks/useHoverTrackingHandlers.ts b/src/Forte.Optimizely.ContentUsage/Frontend/Lib/hooks/useHoverTrackingHandlers.ts new file mode 100644 index 0000000..ce31c27 --- /dev/null +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Lib/hooks/useHoverTrackingHandlers.ts @@ -0,0 +1,24 @@ +import { useState, useMemo } from "react"; + +export type HoverTrackingHook = [ + isHovered: boolean, + handlers: HoverHandlers +] + +export interface HoverHandlers { + enter: () => void; + out: () => void; +} + +export function useHoverTrackingHandlers(): HoverTrackingHook { + const [isHovered, setIsHovered] = useState(false); + + const urlHoveredHandlers = useMemo(() => { + return { + enter: () => setIsHovered(true), + out: () => setIsHovered(false), + }; + }, []); + + return [isHovered, urlHoveredHandlers]; +} diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Styles/_variables.scss b/src/Forte.Optimizely.ContentUsage/Frontend/Styles/_variables.scss index 2722068..2f65887 100644 --- a/src/Forte.Optimizely.ContentUsage/Frontend/Styles/_variables.scss +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Styles/_variables.scss @@ -1 +1,5 @@ @import 'variables/breakpoints'; + +:root { + --muted-color: #707070 +} \ No newline at end of file diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.scss b/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.scss index 5272c3d..ffa7629 100644 --- a/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.scss +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.scss @@ -1,4 +1,39 @@ .forte-optimizely-content-usage-spinner__center { display: flex; justify-content: center; -} \ No newline at end of file +} + +.forte-optimizely-content-usage-action-label { + display: block; + color: var(--muted-color); + text-align: center; + opacity: 0; + transition: opacity 150ms ease-in-out; +} + +.forte-optimizely-content-usage-table-row:hover { + & .forte-optimizely-content-usage-action-label { + opacity: 1; + } +} + +.forte-optimizely-content-usage-page-url { + padding: 0 8px; + width: 100%; + &:hover { + background-color: var(--grey-50); + } +} + +.oui-disclose { + & .soft-half { + color: var(--muted-color); + padding: 0 4px !important; + } + + &:not(.is-active){ + & .oui-disclose__symbol { + top: 0; + } + } +} diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.tsx b/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.tsx index d4a6556..356c78c 100644 --- a/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.tsx +++ b/src/Forte.Optimizely.ContentUsage/Frontend/Views/ContentTypeUsageView.tsx @@ -9,6 +9,7 @@ import { Breadcrumb } from "@episerver/ui-framework"; import { TableColumn } from "../types"; import { ButtonIcon, + Disclose, DiscloseTable, Dropdown, Grid, @@ -35,22 +36,7 @@ import Filters from "../Components/Filters/Filters"; import { useScroll } from "../Lib/hooks/useScroll"; import "./ContentTypeUsageView.scss"; - -enum ContentTypeUsageTableColumn { - ID = "id", - Name = "name", - LanguageBranch = "languageBranch", - PageUrl = "pageUrl", - Actions = "actions", -} - -interface ContentTypeUsageTableRowProps extends ContentUsageDto { - tableColumns: TableColumn[]; - onRowClick?: (event: React.PointerEvent) => void; - onEditButtonClick?: (event: React.PointerEvent) => void; - onViewWebsiteClick?: () => void; - hasDiscloseTableRows?: boolean; -} +import ContentTypeUsagesTable, { ContentTypeUsageTableColumn } from "../Components/Tables/ContentTypeUsagesTable/ContentTypeUsageTable"; type ContentTypeUsageViewResponse = | APIResponse @@ -61,125 +47,6 @@ type ContentTypeUsageViewInitialResponse = [ APIResponse ]; -const ContentTypeUsageDiscloseTableRow = ({ - tableColumns, - onRowClick, - ...row -}: ContentTypeUsageTableRowProps) => { - const { - views: { - contentUsagesView: { - table: { actions }, - }, - }, - } = useTranslations(); - - return ( - column.visible) - .map((column) => ( - - {column.id.toString() === ContentTypeUsageTableColumn.PageUrl - ? null - : row[column.id]} - - )), - , - ]} - isFullWidth - key={row.id} - > -
-
- - - - URL - - - - {row.pageUrls.length > 0 && - row.pageUrls.map((pageUrl, index) => ( - - - {pageUrl} - - - ))} - -
-
-
-
- ); -}; - -const ContentTypeUsageTableRow = ({ - tableColumns, - onRowClick, - onEditButtonClick, - onViewWebsiteClick, - hasDiscloseTableRows, - ...row -}: ContentTypeUsageTableRowProps) => { - const { - views: { - contentUsagesView: { - table: { actions }, - }, - }, - } = useTranslations(); - - return ( - - {hasDiscloseTableRows && } - {tableColumns - .filter((column) => column.visible) - .map((column) => ( - - {column.id.toString() === ContentTypeUsageTableColumn.PageUrl && - row.pageUrls.length > 0 && - row.pageUrls[0] ? ( - - {row.pageUrls[0]} - - ) : ( - row[column.id] - )} - - ))} - - - } - > - - {row.pageUrls[0] && ( - - - - - - )} - - - - - - - - - - ); -}; const ContentTypeUsageView = () => { const [dataLoaded, setDataLoaded] = useState(false); @@ -208,6 +75,7 @@ const ContentTypeUsageView = () => { visible: true, filter: true, sorting: true, + columnSpanWidth: 1, }, { id: ContentTypeUsageTableColumn.Name, @@ -215,6 +83,7 @@ const ContentTypeUsageView = () => { visible: true, filter: true, sorting: true, + columnSpanWidth: 4, }, { id: ContentTypeUsageTableColumn.LanguageBranch, @@ -222,12 +91,14 @@ const ContentTypeUsageView = () => { visible: true, filter: true, sorting: true, + columnSpanWidth: 2, }, { id: ContentTypeUsageTableColumn.PageUrl, name: columns.pageUrl, visible: true, sorting: false, + columnSpanWidth: 5, }, ] as TableColumn[]; @@ -278,34 +149,6 @@ const ContentTypeUsageView = () => { [translations, contentTypeDisplayName] ); - const onTableRowClick = useCallback( - (url?: string | null, alwaysTriggerClick = false) => - (event: React.PointerEvent) => { - if (!url) return; - - const target = event.target as HTMLTableCellElement | undefined; - - if ((target && target.tagName === "TD") || alwaysTriggerClick) { - navigateTo(url); - } - }, - [navigateTo] - ); - - const onViewWebsiteClick = useCallback( - (url?: string | null) => () => { - if (!url) return; - - navigateTo(url); - }, - [navigateTo] - ); - - const hasDiscloseTableRows = useMemo( - () => rows.some((row) => row.pageUrls.length > 1), - [rows] - ); - const response = useLoaderData() as ContentTypeUsageViewResponse; const setDataFromInitialResponse = useCallback( @@ -343,6 +186,11 @@ const ContentTypeUsageView = () => { } }, [response]); + const handleSortChange = useCallback((column: TableColumn) => { + setDataLoaded(false); + onSortChange(column); + }, [setDataLoaded, onSortChange]); + return ( { /> - - {dataLoaded ? ( -
- - - - {hasDiscloseTableRows && ( - - )} - {tableColumns - .filter((column) => column.visible) - .map((column) => ( - { - setDataLoaded(false); - onSortChange(column); - }, - order: sortDirection.toLowerCase(), - }} - key={column.id} - > - {column.name} - - ))} - - - - - - {rows.length > 0 ? ( - rows.map((row) => - row.pageUrls.length > 1 ? ( - - ) : ( - - ) - ) - ) : ( - - {translations.noResults} - - )} - - -
+ + {dataLoaded ? ( +
+ +
) : (
)} -
+
{totalPages > 1 && dataLoaded && ( - - + + )} diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/routes.ts b/src/Forte.Optimizely.ContentUsage/Frontend/routes.ts index d910a90..ecc6963 100644 --- a/src/Forte.Optimizely.ContentUsage/Frontend/routes.ts +++ b/src/Forte.Optimizely.ContentUsage/Frontend/routes.ts @@ -19,8 +19,12 @@ export const removeTrailingSlash = (url: string) => url.replace(/\/$/, ""); export const getRoutePath = (route: string) => removeTrailingSlash(getBaseUrl()) + baseViewPath + "#" + route; -export const navigateTo = (url: string | URL) => { +export const navigateTo = (url: string | URL, newWindow = false) => { if (typeof window !== "undefined") { + if (newWindow) { + window.open(url, "_blank"); + return; + } window.location.assign(url); } }; diff --git a/src/Forte.Optimizely.ContentUsage/Frontend/types.ts b/src/Forte.Optimizely.ContentUsage/Frontend/types.ts index 0b477e0..1e86e83 100644 --- a/src/Forte.Optimizely.ContentUsage/Frontend/types.ts +++ b/src/Forte.Optimizely.ContentUsage/Frontend/types.ts @@ -9,6 +9,7 @@ export interface TableColumn { visible?: boolean; filter?: boolean; sorting?: boolean; + columnSpanWidth?: number; } export interface ContentTypeBase {