diff --git a/assets/images/categories-end-user-experience-preview-mobile@2x.png b/assets/images/categories-end-user-experience-preview-mobile@2x.png deleted file mode 100644 index f303df6ccc..0000000000 Binary files a/assets/images/categories-end-user-experience-preview-mobile@2x.png and /dev/null differ diff --git a/assets/images/preview-self-service-icon.png b/assets/images/preview-self-service-icon.png deleted file mode 100644 index 5a7016da60..0000000000 Binary files a/assets/images/preview-self-service-icon.png and /dev/null differ diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx index 8c1fd1ef0c..492d4f0720 100644 --- a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx +++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx @@ -158,6 +158,8 @@ interface ISoftwareNameCellProps { iconUrl?: string | null; isIosOrIpadosApp?: boolean; isAndroidPlayStoreApp?: boolean; + /** Only used on Edit icon modal to render a preview of the chosen unsaved icon */ + previewIcon?: JSX.Element; } const SoftwareNameCell = ({ @@ -173,8 +175,11 @@ const SoftwareNameCell = ({ iconUrl, isIosOrIpadosApp = false, isAndroidPlayStoreApp = false, + previewIcon, }: ISoftwareNameCellProps) => { - const icon = ; + const icon = previewIcon || ( + + ); // My device page > Software fake link as entire row opens a modal if (pageContext === "deviceUser" && !isSelfService) { return ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx index 6e5fc60a9b..9f86aea7eb 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal.tsx @@ -1,6 +1,8 @@ import React, { useContext, useEffect, useState, useCallback } from "react"; import { useQuery } from "react-query"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import { noop } from "lodash"; + import { IAppStoreApp, isIpadOrIphoneSoftwareSource, @@ -10,6 +12,7 @@ import { import { IInputFieldParseTarget } from "interfaces/form_field"; import { NotificationContext } from "context/notification"; +import { AppContext } from "context/app"; import { INotification } from "interfaces/notification"; import { getErrorReason } from "interfaces/errors"; import softwareAPI from "services/entities/software"; @@ -25,17 +28,14 @@ import Card from "components/Card"; import Button from "components/buttons/Button"; import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; import TableCount from "components/TableContainer/TableCount"; -import CardHeader from "components/CardHeader"; -import TooltipTruncatedText from "components/TooltipTruncatedText"; import Spinner from "components/Spinner"; import { isSafeImagePreviewUrl } from "pages/SoftwarePage/helpers"; import SoftwareDetailsSummary from "pages/SoftwarePage/components/cards/SoftwareDetailsSummary/SoftwareDetailsSummary"; -import { SELF_SERVICE_SUBHEADER } from "pages/hosts/details/cards/Software/SelfService/SelfService"; +import { BasicSoftwareTable } from "pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/CategoriesEndUserExperienceModal"; +import SelfServicePreview from "pages/SoftwarePage/components/cards/SelfServicePreview"; import { TitleVersionsLastUpdatedInfo } from "../SoftwareSummaryCard/TitleVersionsTable/TitleVersionsTable"; -import PreviewSelfServiceIcon from "../../../../../assets/images/preview-self-service-icon.png"; -import PreviewSelfServiceMobileIcon from "../../../../../assets/images/preview-self-service-mobile-icon.png"; const baseClass = "edit-icon-modal"; @@ -152,6 +152,7 @@ const EditIconModal = ({ previewInfo, }: IEditIconModalProps) => { const { renderFlash, renderMultiFlash } = useContext(NotificationContext); + const { config } = useContext(AppContext); const isSoftwarePackage = installerType === "package"; const isIosOrIpadosApp = isIpadOrIphoneSoftwareSource( @@ -474,111 +475,82 @@ const EditIconModal = ({ ); }; - const renderPreviewSelfServiceCard = () => ( - - - -
- Preview icon on Fleet Desktop > Self-service -
-
- {iconState.previewUrl && - isSafeImagePreviewUrl(iconState.previewUrl) ? ( - Uploaded self-service icon - ) : ( - // Known limitation: we cannot see VPP app icons as the fallback when a custom icon - // is set as VPP icon is not returned by the API if a custom icon is returned - - )} -
- -
-
-
-
- - ); + /** + * Preview matches preview in Edit Software modal > Categories End User Experience modal + * Non-mobile preview: + * - uses HTML/CSS instead for maintainability as the self-service UI changes + * - dynamic name/icon + * + * Mobile preview modal: + * - uses a screenshot + * - dynamic name/icon/version + */ - const renderPreviewSelfServiceMobileCard = () => ( - -
- Preview icon on Fleet Desktop > Self-service -
-
- {iconState.previewUrl && isSafeImagePreviewUrl(iconState.previewUrl) ? ( + const renderPreviewSelfServiceCard = () => ( + + iconState.previewUrl && isSafeImagePreviewUrl(iconState.previewUrl) ? ( Uploaded self-service icon ) : ( - // Known limitation: we cannot see VPP app icons as the fallback when a custom icon - // is set as VPP icon is not returned by the API if a custom icon is returned - )} -
-
- -
-
- {"latest_version" in software - ? software.latest_version - : software.version || "Version (unknown)"} -
-
-
-
+ ) + } + renderTable={() => ( + + ) : ( + + ) + } + /> + )} + /> ); const renderForm = () => ( @@ -623,11 +595,7 @@ const EditIconModal = ({ {renderPreviewFleetCard()} - - {isIosOrIpadosApp - ? renderPreviewSelfServiceMobileCard() - : renderPreviewSelfServiceCard()} - + {renderPreviewSelfServiceCard()} diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/_styles.scss index 006db757bf..747df65a96 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/_styles.scss @@ -7,8 +7,12 @@ @include vertical-modal-layout; } + .self-service-preview__preview-card { + height: 230px; // Match height of Fleet UI preview + } + &__preview-card { - max-height: 203px; + height: 230px; // Match height of self-service preview overflow: hidden; &__fleet, @@ -28,22 +32,6 @@ border-radius: $border-radius-xxlarge; } } - - &__self-service { - height: 350px; - } - } - - &__preview-img-container { - img { - width: 124%; - } - } - - &__preview-img-container--mobile { - img { - width: 100%; // TODO - } } &__preview-results-count { @@ -78,22 +66,14 @@ } } + // Specific offsets for mobile in edit modal &__self-service-preview--mobile { - position: relative; top: -88px; left: 156px; - display: flex; gap: $pad-small; - font-size: 12px; - align-items: center; - max-width: 210px; - - .software-icon__xsmall { - border-radius: $border-radius-small; - } } - // Required for tooltip truncating long software names + // Required for tooltip truncating long software names (desktop) &__self-service-preview-name { max-width: 180px; white-space: nowrap; @@ -101,22 +81,6 @@ text-overflow: ellipsis; } - &__self-service-preview-name-version--mobile { - display: flex; - flex-direction: column; - font-size: $xxx-small; - } - - // Required for tooltip truncating long software names - &__self-service-preview-name--mobile { - max-width: 115px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; - font-weight: $bold; - } - &__mask-overlay { position: relative; width: 550px; @@ -129,9 +93,10 @@ ); &--fleet { - bottom: 156px; + bottom: 130px; height: 135px; } + &--self-service { bottom: 200px; height: 75px; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx index 3179d841e1..b9c47f24af 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx @@ -58,6 +58,7 @@ interface IEditSoftwareModalProps { name: string; displayName: string; source?: string; + iconUrl?: string | null; } const EditSoftwareModal = ({ @@ -73,6 +74,7 @@ const EditSoftwareModal = ({ name, displayName, source, + iconUrl = undefined, }: IEditSoftwareModalProps) => { const { renderFlash } = useContext(NotificationContext); const { config } = useContext(AppContext); @@ -404,9 +406,14 @@ const EditSoftwareModal = ({ name={name} displayName={displayName} source={source} - iconUrl={softwareInstaller.icon_url || undefined} + iconUrl={iconUrl} // Must be software title icon url not installer icon url onCancel={togglePreviewEndUserExperienceModal} isIosOrIpadosApp={isIosOrIpadosApp} + mobileVersion={ + ("latest_version" in softwareInstaller && + softwareInstaller.latest_version) || + softwareInstaller.version + } /> )} {!!pendingPackageUpdates.software && showFileProgressModal && ( diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx index a4129fc530..cb2fb97231 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareSummaryCard/SoftwareSummaryCard.tsx @@ -202,6 +202,7 @@ const SoftwareSummaryCard = ({ name={softwareTitle.name} displayName={softwareTitle.display_name || softwareTitle.name} source={softwareTitle.source} + iconUrl={softwareTitle.icon_url} /> )} {showEditConfigurationModal && hasValidTeamId && softwareInstaller && ( diff --git a/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tests.tsx b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tests.tsx new file mode 100644 index 0000000000..fd52a12730 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tests.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import SelfServicePreview from "./SelfServicePreview"; + +describe("SelfServicePreview", () => { + it("renders mobile preview with screenshot, name, version, and icon", () => { + const MockIcon = () =>
Mock icon
; + + render( + } + /> + ); + + expect( + screen.getByAltText("Preview icon on Fleet Desktop > Self-service") + ).toBeVisible(); + + expect(screen.getByText("Mock icon")).toBeVisible(); + expect(screen.getByText("Display name")).toBeVisible(); + expect(screen.getByText("1.2.3")).toBeVisible(); + }); + + it("falls back to name when displayName is empty in mobile preview", () => { + render( +
Icon
} + /> + ); + + expect(screen.getByText("Fallback name")).toBeVisible(); + }); + + it("renders desktop preview with header, search field, categories menu, and table", () => { + const MockTable = () =>
Mock table
; + + render( +
Icon
} + renderTable={() => } + /> + ); + + expect(screen.getByText(/Self-service/i)).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Search by name")).toBeInTheDocument(); + expect(screen.getByText(/Browsers/i)).toBeInTheDocument(); + expect(screen.getByText("Mock table")).toBeVisible(); + }); +}); diff --git a/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tsx b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tsx new file mode 100644 index 0000000000..c206c0aaef --- /dev/null +++ b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/SelfServicePreview.tsx @@ -0,0 +1,117 @@ +/** + * Used on Software title > CategoriesEndUserExperienceModal and Software title > Edit Appearance Modal + * + * Non-mobile preview modal: + * - uses HTML/CSS instead for maintainability as the self-service UI changes + * - dynamic name/icon + * + * Mobile preview modal: + * - uses a screenshot + * - dynamic name/icon/version + */ + +import React from "react"; +import { noop } from "lodash"; +import Card from "components/Card"; +import SearchField from "components/forms/fields/SearchField"; +import TooltipTruncatedText from "components/TooltipTruncatedText"; +import CategoriesMenu from "pages/hosts/details/cards/Software/SelfService/components/CategoriesMenu"; +import SelfServiceHeader from "pages/hosts/details/cards/Software/SelfService/components/SelfServiceHeader"; +import { CATEGORIES_NAV_ITEMS } from "pages/hosts/details/cards/Software/SelfService/helpers"; +import PreviewSelfServiceMobileIcon from "../../../../../../assets/images/preview-self-service-mobile-icon.png"; + +const baseClass = "self-service-preview"; + +interface ISelfServicePreviewProps { + /** iOS/iPadOS uses screenshot + dynamic overlay; otherwise HTML preview */ + isIosOrIpadosApp: boolean; + /** Shared data for mobile preview */ + contactUrl: string; + name: string; + displayName: string; + versionLabel: string; + /** What to render for the app icon in the list (img or ) */ + renderIcon: () => React.ReactNode; + /** What to render as the “table” area for desktop (e.g. BasicSoftwareTable) */ + renderTable?: () => React.ReactNode; +} + +const SelfServicePreview = ({ + isIosOrIpadosApp, + contactUrl, + name, + displayName, + versionLabel, + renderIcon, + renderTable, +}: ISelfServicePreviewProps) => { + if (isIosOrIpadosApp) { + // Mobile preview with screenshot + overlay + return ( + +
+ Preview icon on Fleet Desktop > Self-service +
+
+ {renderIcon()} +
+
+ +
+
+ {versionLabel} +
+
+
+
+ ); + } + + // Desktop HTML/CSS self-service preview + return ( + +
+ + + +
+ + {renderTable && renderTable()} +
+
+ + ); +}; + +export default SelfServicePreview; diff --git a/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/_styles.scss b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/_styles.scss new file mode 100644 index 0000000000..3f26fa5e3d --- /dev/null +++ b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/_styles.scss @@ -0,0 +1,77 @@ +.self-service-preview { + &__preview-card { + overflow: hidden; + height: 310px; + + &--mobile { + height: 230px; // Mobile preview is shorter height than Desktop preview + } + + &__self-service { + width: 515px; + height: 310px; + @include vertical-card-layout; + } + + .card-header__header { + font-size: initial; + } + + .categories-menu { + top: 0; + width: 200px; + } + + .data-table__table { + width: 325px; + } + } + + &__preview-img-container--mobile { + img { + width: 100%; + } + } + + &__table { + display: flex; + flex-direction: row; + } + + &__disabled-overlay { + position: absolute; + width: 540px; + height: 335px; + z-index: 1000; + } + + &__self-service-preview--mobile { + position: relative; + display: flex; + align-items: center; + max-width: 210px; + left: 154px; + top: -85px; + gap: $pad-xsmall; + + .software-icon__xsmall { + border-radius: $border-radius-small; + } + } + + &__self-service-preview-name-version--mobile { + display: flex; + flex-direction: column; + font-size: 8px; + max-width: 110px; // Required for tooltip truncating long software names (mobile) + } + + // Required for tooltip truncating long software names + &__self-service-preview-name--mobile { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + position: relative; + font-weight: $bold; + } +} diff --git a/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/index.ts b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/index.ts new file mode 100644 index 0000000000..d38d476e84 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/cards/SelfServicePreview/index.ts @@ -0,0 +1 @@ +export { default } from "./SelfServicePreview"; diff --git a/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/CategoriesEndUserExperienceModal.tsx b/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/CategoriesEndUserExperienceModal.tsx index 221fa258cc..7461a6575b 100644 --- a/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/CategoriesEndUserExperienceModal.tsx +++ b/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/CategoriesEndUserExperienceModal.tsx @@ -1,15 +1,13 @@ -/** Mobile preview modal uses a screenshot - * Non-mobile now uses HTML/CSS instead for - * maintainability as the self-selvice UI changes +/** + * Previews match preview in Edit Appearance modal for Edit Appearance modal * * Currently only shown from the edit UI, though wired through the Add UI - * Users currently can set categories only when editing a curent installer + * Users currently can set categories only when editing a current installer */ import React, { useContext } from "react"; import { Column } from "react-table"; -import { noop } from "lodash"; import { AppContext } from "context/app"; import { IHeaderProps } from "interfaces/datatable_config"; @@ -17,14 +15,10 @@ import TableContainer from "components/TableContainer"; import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; -import Card from "components/Card"; -import SelfServiceHeader from "pages/hosts/details/cards/Software/SelfService/components/SelfServiceHeader"; -import SearchField from "components/forms/fields/SearchField"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; -import CategoriesMenu from "pages/hosts/details/cards/Software/SelfService/components/CategoriesMenu"; -import { CATEGORIES_NAV_ITEMS } from "pages/hosts/details/cards/Software/SelfService/helpers"; -import CategoriesEndUserExperiencePreviewMobile from "../../../../../../assets/images/categories-end-user-experience-preview-mobile@2x.png"; +import SelfServicePreview from "../../cards/SelfServicePreview"; +import SoftwareIcon from "../../icons/SoftwareIcon"; const baseClass = "categories-end-user-experience-preview-modal"; @@ -58,7 +52,8 @@ const getData = ( name: string, displayName: string, iconUrl: string | null, - source?: string + source?: string, + previewIcon?: JSX.Element ): ISoftwareRow[] => { const currentSoftwareRow: ISoftwareRow = { name: ( @@ -69,6 +64,7 @@ const getData = ( iconUrl={iconUrl} pageContext="deviceUser" isSelfService + previewIcon={previewIcon} /> ), }; @@ -97,18 +93,21 @@ interface BasicSoftwareTableProps { displayName: string; source?: string; iconUrl?: string | null; + /** Render a preview icon instead for edit icon preview */ + previewIcon?: JSX.Element; } -const BasicSoftwareTable = ({ +export const BasicSoftwareTable = ({ name, displayName, source, iconUrl = null, + previewIcon, }: BasicSoftwareTableProps) => { return ( columnConfigs={columns} - data={getData(name, displayName, iconUrl, source)} + data={getData(name, displayName, iconUrl, source, previewIcon)} isLoading={false} emptyComponent={EmptyState} showMarkAllPages={false} @@ -126,8 +125,9 @@ interface ICategoriesEndUserExperienceModal { isIosOrIpadosApp?: boolean; name?: string; displayName?: string; - iconUrl?: string; + iconUrl?: string | null; source?: string; + mobileVersion?: string; } const CategoriesEndUserExperienceModal = ({ @@ -137,63 +137,35 @@ const CategoriesEndUserExperienceModal = ({ displayName = "Software name", iconUrl, source, + mobileVersion, }: ICategoriesEndUserExperienceModal): JSX.Element => { const { config } = useContext(AppContext); return ( <> What end users see: - - {isIosOrIpadosApp ? ( -
- Categories end user experience preview ( + -
- ) : ( - -
- - - -
- - -
-
- - )} + )} + renderTable={() => ( + + )} + />
diff --git a/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/_styles.scss b/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/_styles.scss index 0f3ee56a5d..12799387d2 100644 --- a/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/_styles.scss +++ b/frontend/pages/SoftwarePage/components/modals/CategoriesEndUserExperienceModal/_styles.scss @@ -5,49 +5,20 @@ gap: $pad-large; } - &__preview { - img { - width: 100%; - } - } - .modal-cta-wrap { margin-top: 0; // Defer to new gap styling } &__preview-card { - max-height: 300px; - overflow: hidden; - - .card-header__header { - font-size: initial; - } - - &__self-service { - width: 515px; - height: 310px; - @include vertical-card-layout; - } - - .categories-menu { - top: 0; - width: 200px; - } - - .data-table__table { - width: 325px; - } - } - &__table { - display: flex; - flex-direction: row; + max-height: 290px; + padding: $pad-xlarge 0 0; } - &__disabled-overlay { - position: absolute; - width: 540px; - height: 335px; - z-index: 1000; + &__self-service-preview--mobile { + // Different offsets than edit modal + top: -94px; + left: 175px; + gap: $pad-small; } }