mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Fleet UI: Ability to edit software display names (#34872)
This commit is contained in:
parent
1d7db85d66
commit
3efeeb1ad0
35 changed files with 413 additions and 135 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -30,6 +30,9 @@ frontend/coverage
|
|||
# typescript generated test files
|
||||
tmp/
|
||||
|
||||
# test debug files
|
||||
debug.test*
|
||||
|
||||
# operating system artifacts
|
||||
.DS_Store
|
||||
|
||||
|
|
|
|||
1
changes/33779-custom-software-display-names
Normal file
1
changes/33779-custom-software-display-names
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fleet UI: Added ability to change software display names
|
||||
|
|
@ -204,6 +204,7 @@ export const DEFAULT_INSTALLED_VERSION = {
|
|||
const DEFAULT_HOST_SOFTWARE_MOCK: IHostSoftware = {
|
||||
id: 1,
|
||||
name: "mock software.app",
|
||||
display_name: "Mock Software",
|
||||
icon_url: null,
|
||||
software_package: createMockHostSoftwarePackage(),
|
||||
app_store_app: null,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const createMockSoftwareVulnerability = (
|
|||
const DEFAULT_SOFTWARE_VERSION_MOCK: ISoftwareVersion = {
|
||||
id: 1,
|
||||
name: "test.app",
|
||||
display_name: "Test App",
|
||||
version: "1.2.3",
|
||||
bundle_identifier: "com.test.Desktop",
|
||||
source: "apps",
|
||||
|
|
|
|||
|
|
@ -366,7 +366,9 @@ export const SoftwareInstallDetailsModal = ({
|
|||
<div className={`${baseClass}__modal-content`}>
|
||||
<StatusMessage
|
||||
installResult={installResultWithHostDisplayName}
|
||||
softwareName={hostSoftware?.name || "Software"} // will always be defined at this point
|
||||
softwareName={
|
||||
hostSoftware?.display_name || hostSoftware?.name || "Software"
|
||||
} // will always be defined at this point
|
||||
isMyDevicePage={!!deviceAuthToken}
|
||||
contactUrl={contactUrl}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export type ISupportedGraphicNames = Extract<
|
|||
>;
|
||||
|
||||
interface IFileUploaderProps {
|
||||
label?: React.ReactNode;
|
||||
graphicName: ISupportedGraphicNames | ISupportedGraphicNames[];
|
||||
message: React.ReactNode;
|
||||
title?: string;
|
||||
|
|
@ -79,6 +80,7 @@ interface IFileUploaderProps {
|
|||
* A component that encapsulates the UI for uploading a file and a file selected.
|
||||
*/
|
||||
export const FileUploader = ({
|
||||
label,
|
||||
graphicName: graphicNames,
|
||||
message,
|
||||
title,
|
||||
|
|
@ -132,6 +134,11 @@ export const FileUploader = ({
|
|||
}
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
return label ? (
|
||||
<div className={`${baseClass}__label form-field__label`}>{label}</div>
|
||||
) : null;
|
||||
};
|
||||
const renderGraphics = () => {
|
||||
const graphicNamesArr =
|
||||
typeof graphicNames === "string" ? [graphicNames] : graphicNames;
|
||||
|
|
@ -249,22 +256,25 @@ export const FileUploader = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Card color="grey" className={classes}>
|
||||
{fileDetails ? (
|
||||
<FileDetails
|
||||
graphicNames={graphicNames}
|
||||
fileDetails={fileDetails}
|
||||
canEdit={canEdit}
|
||||
onDeleteFile={onDeleteFile}
|
||||
onFileSelect={onFileSelect}
|
||||
accept={accept}
|
||||
gitopsCompatible={gitopsCompatible}
|
||||
gitOpsModeEnabled={gitOpsModeEnabled}
|
||||
/>
|
||||
) : (
|
||||
renderFileUploader()
|
||||
)}
|
||||
</Card>
|
||||
<div className={`${baseClass}__wrapper form-field`}>
|
||||
{renderLabel()}
|
||||
<Card color="grey" className={classes}>
|
||||
{fileDetails ? (
|
||||
<FileDetails
|
||||
graphicNames={graphicNames}
|
||||
fileDetails={fileDetails}
|
||||
canEdit={canEdit}
|
||||
onDeleteFile={onDeleteFile}
|
||||
onFileSelect={onFileSelect}
|
||||
accept={accept}
|
||||
gitopsCompatible={gitopsCompatible}
|
||||
gitOpsModeEnabled={gitOpsModeEnabled}
|
||||
/>
|
||||
) : (
|
||||
renderFileUploader()
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
padding: $pad-xlarge $pad-large;
|
||||
font-size: $x-small;
|
||||
text-align: center;
|
||||
|
||||
|
||||
.content-wrapper {
|
||||
@include content-wrapper();
|
||||
}
|
||||
|
|
@ -42,7 +42,8 @@
|
|||
// we handle the padding in the label so the entire button is clickable
|
||||
padding: 0;
|
||||
|
||||
label, span {
|
||||
label,
|
||||
span {
|
||||
padding: $pad-small $pad-medium;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -129,7 +129,10 @@ const InstallIconWithTooltip = ({
|
|||
};
|
||||
|
||||
interface ISoftwareNameCellProps {
|
||||
/** Used to key default software icon and name displayed if no display_name */
|
||||
name: string;
|
||||
/** Overrides name for display */
|
||||
display_name?: string;
|
||||
source?: string;
|
||||
/** pass in a `path` that this cell will link to */
|
||||
path?: string;
|
||||
|
|
@ -144,6 +147,7 @@ interface ISoftwareNameCellProps {
|
|||
|
||||
const SoftwareNameCell = ({
|
||||
name,
|
||||
display_name,
|
||||
source,
|
||||
path,
|
||||
router,
|
||||
|
|
@ -156,7 +160,9 @@ const SoftwareNameCell = ({
|
|||
const icon = <SoftwareIcon name={name} source={source} url={iconUrl} />;
|
||||
// My device page > Software fake link as entire row opens a modal
|
||||
if (pageContext === "deviceUser" && !isSelfService) {
|
||||
return <LinkCell tooltipTruncate prefix={icon} value={name} />;
|
||||
return (
|
||||
<LinkCell tooltipTruncate prefix={icon} value={display_name || name} />
|
||||
);
|
||||
}
|
||||
|
||||
// Non-clickable cell if no router/path (e.g. My device page > SelfService)
|
||||
|
|
@ -165,7 +171,7 @@ const SoftwareNameCell = ({
|
|||
<div className={baseClass}>
|
||||
<TooltipTruncatedTextCell
|
||||
prefix={icon}
|
||||
value={name}
|
||||
value={display_name || name}
|
||||
className="software-name"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -185,7 +191,7 @@ const SoftwareNameCell = ({
|
|||
tooltipTruncate
|
||||
customOnClick={onClickSoftware}
|
||||
prefix={icon}
|
||||
value={name}
|
||||
value={display_name || name}
|
||||
suffix={
|
||||
hasInstaller ? (
|
||||
<InstallIconWithTooltip
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ export default PropTypes.shape({
|
|||
persistOnPageChange: PropTypes.bool,
|
||||
});
|
||||
|
||||
export type IAlertType = "success" | "error" | "warning-filled";
|
||||
|
||||
export interface INotification {
|
||||
alertType: "success" | "error" | "warning-filled" | null;
|
||||
alertType: IAlertType | null;
|
||||
isVisible: boolean;
|
||||
message: JSX.Element | string | null;
|
||||
persistOnPageChange?: boolean;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface IGetSoftwareByIdResponse {
|
|||
export interface ISoftware {
|
||||
id: number;
|
||||
name: string; // e.g., "Figma.app"
|
||||
display_name?: string; // e.g. "Figma for Desktop"
|
||||
version: string; // e.g., "2.1.11"
|
||||
bundle_identifier?: string | null; // e.g., "com.figma.Desktop"
|
||||
application_id?: string | null; // e.g., "us.zoom.videomeetings" for Android apps
|
||||
|
|
@ -90,6 +91,7 @@ export interface ISoftwareAppStoreAppStatus {
|
|||
|
||||
export interface ISoftwarePackage {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
title_id: number;
|
||||
url: string;
|
||||
version: string;
|
||||
|
|
@ -118,6 +120,7 @@ export const isSoftwarePackage = (
|
|||
|
||||
export interface IAppStoreApp {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
app_store_id: string; // API returns this as a string
|
||||
latest_version: string;
|
||||
created_at: string;
|
||||
|
|
@ -142,6 +145,7 @@ export interface IAppStoreApp {
|
|||
export interface ISoftwareTitle {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name?: string;
|
||||
icon_url: string | null;
|
||||
versions_count: number;
|
||||
source: SoftwareSource;
|
||||
|
|
@ -157,6 +161,7 @@ export interface ISoftwareTitle {
|
|||
export interface ISoftwareTitleDetails {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name?: string;
|
||||
icon_url: string | null;
|
||||
software_package: ISoftwarePackage | null;
|
||||
app_store_app: IAppStoreApp | null;
|
||||
|
|
@ -186,6 +191,7 @@ export interface ISoftwareVulnerability {
|
|||
export interface ISoftwareVersion {
|
||||
id: number;
|
||||
name: string; // e.g., "Figma.app"
|
||||
display_name?: string; // e.g. "Figma for Desktop"
|
||||
version: string; // e.g., "2.1.11"
|
||||
bundle_identifier?: string; // e.g., "com.figma.Desktop"
|
||||
source: SoftwareSource;
|
||||
|
|
@ -493,6 +499,7 @@ export interface ISoftwareInstallVersion {
|
|||
|
||||
export interface IHostSoftwarePackage {
|
||||
name: string;
|
||||
display_name?: string;
|
||||
self_service: boolean;
|
||||
icon_url: string | null;
|
||||
version: string;
|
||||
|
|
@ -504,6 +511,7 @@ export interface IHostSoftwarePackage {
|
|||
}
|
||||
|
||||
export interface IHostAppStoreApp {
|
||||
display_name?: string;
|
||||
app_store_id: string;
|
||||
self_service: boolean;
|
||||
icon_url: string;
|
||||
|
|
@ -516,6 +524,7 @@ export interface IHostAppStoreApp {
|
|||
export interface IHostSoftware {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name?: string;
|
||||
icon_url: string | null;
|
||||
software_package: IHostSoftwarePackage | null;
|
||||
app_store_app: IHostAppStoreApp | null;
|
||||
|
|
|
|||
|
|
@ -63,10 +63,15 @@ const generateTableConfig = (
|
|||
disableSortBy: true,
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
const { name, source, icon_url } = cellProps.row.original;
|
||||
const { name, display_name, source, icon_url } = cellProps.row.original;
|
||||
|
||||
return (
|
||||
<SoftwareNameCell name={name} source={source} iconUrl={icon_url} />
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
iconUrl={icon_url}
|
||||
/>
|
||||
);
|
||||
},
|
||||
sortType: "caseInsensitive",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const SummaryCard = ({
|
|||
}: ISummaryCardProps) => (
|
||||
<Card borderRadiusSize="xxlarge" className={`${baseClass}__summary-section`}>
|
||||
<SoftwareDetailsSummary
|
||||
title={osVersion.name}
|
||||
displayName={osVersion.name}
|
||||
hostCount={osVersion.hosts_count}
|
||||
countsUpdatedAt={countsUpdatedAt}
|
||||
queryParams={{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { createCustomRenderer } from "test/test-utils";
|
||||
import {
|
||||
createMockSoftwarePackage,
|
||||
|
|
@ -24,6 +24,7 @@ const MOCK_PROPS = {
|
|||
source: software.source,
|
||||
currentIconUrl: null,
|
||||
name: software.name,
|
||||
titleName: software.name,
|
||||
countsUpdatedAt: "2025-09-03T12:00:00Z",
|
||||
},
|
||||
};
|
||||
|
|
@ -62,4 +63,31 @@ describe("EditIconModal", () => {
|
|||
});
|
||||
|
||||
// Note: Rely on QA Wolf for E2e testing of file upload, preview, save, and remove icon
|
||||
|
||||
describe("Display name tests", () => {
|
||||
it("shows the Display name input with correct default value", () => {
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
render(<EditIconModal {...MOCK_PROPS} />);
|
||||
// Should default to blank if previewInfo.titleName === previewInfo.name
|
||||
const displayNameInput = screen.getByLabelText("Display name");
|
||||
expect(displayNameInput).toBeInTheDocument();
|
||||
expect(displayNameInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("pre-fills Display name if previewInfo.name has been modified", () => {
|
||||
const MODIFIED_PROPS = {
|
||||
...MOCK_PROPS,
|
||||
previewInfo: {
|
||||
...MOCK_PROPS.previewInfo,
|
||||
name: "New Custom Name",
|
||||
titleName: "Original Title Name",
|
||||
},
|
||||
};
|
||||
const render = createCustomRenderer({ withBackendMock: true });
|
||||
render(<EditIconModal {...MODIFIED_PROPS} />);
|
||||
const displayNameInput = screen.getByLabelText("Display name");
|
||||
expect(displayNameInput).toBeInTheDocument();
|
||||
expect(displayNameInput).toHaveValue("New Custom Name");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@ import React, { useContext, useEffect, useState } from "react";
|
|||
import { useQuery } from "react-query";
|
||||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
|
||||
import { IAppStoreApp, ISoftwarePackage } from "interfaces/software";
|
||||
import { IInputFieldParseTarget } from "interfaces/form_field";
|
||||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { INotification } from "interfaces/notification";
|
||||
import { getErrorReason } from "interfaces/errors";
|
||||
import softwareAPI from "services/entities/software";
|
||||
import mdmAppleAPI from "services/entities/mdm_apple";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import ModalFooter from "components/ModalFooter";
|
||||
// @ts-ignore
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import FileUploader from "components/FileUploader";
|
||||
import TabNav from "components/TabNav";
|
||||
import TabText from "components/TabText";
|
||||
|
|
@ -58,9 +63,15 @@ const makeFileDetails = (
|
|||
description: `Software icon • ${dimensions || "?"}x${dimensions || "?"} px`,
|
||||
});
|
||||
|
||||
interface IIconFormData {
|
||||
interface IFormData {
|
||||
icon: File;
|
||||
display_name?: string;
|
||||
}
|
||||
|
||||
export interface ISoftwareDisplayNameFormData {
|
||||
displayName?: string; // Edit Display name is in the edit icon modal
|
||||
}
|
||||
|
||||
interface IFileDetails {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
@ -84,7 +95,7 @@ type IconStatus = "customUpload" | "apiCustom" | "fallback";
|
|||
*/
|
||||
interface IconState {
|
||||
previewUrl: string | null;
|
||||
formData: IIconFormData | null;
|
||||
formData: IFormData | null;
|
||||
dimensions: number | null;
|
||||
fileDetails: IFileDetails | null;
|
||||
status: IconStatus;
|
||||
|
|
@ -117,6 +128,8 @@ interface IEditIconModalProps {
|
|||
currentIconUrl: string | null;
|
||||
/** Name used in preview UI but also for FMA default icon matching */
|
||||
name: string;
|
||||
/** Default title name used to check if name has been modified */
|
||||
titleName: string;
|
||||
countsUpdatedAt?: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -132,7 +145,7 @@ const EditIconModal = ({
|
|||
installerType,
|
||||
previewInfo,
|
||||
}: IEditIconModalProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { renderFlash, renderMultiFlash } = useContext(NotificationContext);
|
||||
|
||||
const isSoftwarePackage = installerType === "package";
|
||||
|
||||
|
|
@ -141,10 +154,15 @@ const EditIconModal = ({
|
|||
!!previewInfo.currentIconUrl &&
|
||||
previewInfo.currentIconUrl.startsWith("/api/");
|
||||
|
||||
// Encapsulates icon preview/upload/edit state
|
||||
// Unmodified names default to empty Display name field
|
||||
const hasNameBeenModified = previewInfo.titleName !== previewInfo.name;
|
||||
const defaultName = hasNameBeenModified ? previewInfo.name : "";
|
||||
|
||||
// Encapsulates software name and icon preview/upload/edit state
|
||||
const [displayName, setDisplayName] = useState(defaultName);
|
||||
const [iconState, setIconState] = useState<IconState>(defaultIconState);
|
||||
const [previewTabIndex, setPreviewTabIndex] = useState(0);
|
||||
const [isUpdatingIcon, setIsUpdatingIcon] = useState(false);
|
||||
const [isUpdatingSoftwareInfo, setIsUpdatingSoftwareInfo] = useState(false);
|
||||
/** Shows loading spinner only if a custom icon and its information is loading from API */
|
||||
const [isFirstLoadWithCustomIcon, setIsFirstLoadWithCustomIcon] = useState(
|
||||
shouldFetchCustomIcon
|
||||
|
|
@ -162,6 +180,15 @@ const EditIconModal = ({
|
|||
iconState.status === "fallback" &&
|
||||
!iconState.formData;
|
||||
const canSaveIcon = isCustomUpload || isRemovedCustom;
|
||||
// Determine if any changes have been made to allow enabling Save button
|
||||
const canSaveDisplayName =
|
||||
(hasNameBeenModified && displayName === "") || // user cleared an override display name
|
||||
(!hasNameBeenModified && displayName !== "") || // user set an override display name
|
||||
(hasNameBeenModified &&
|
||||
displayName !== "" &&
|
||||
displayName !== previewInfo.name); // user changed override display name
|
||||
// Ensures Save button is only enabled when icon or name has been changed
|
||||
const canSaveForm = canSaveIcon || canSaveDisplayName;
|
||||
|
||||
// Sets state after fetching current API custom icon
|
||||
const setCurrentApiCustomIcon = (
|
||||
|
|
@ -171,7 +198,7 @@ const EditIconModal = ({
|
|||
) =>
|
||||
setIconState({
|
||||
previewUrl,
|
||||
formData: { icon: file },
|
||||
formData: { icon: file, display_name: displayName },
|
||||
dimensions: width,
|
||||
fileDetails: makeFileDetails(file, width),
|
||||
status: "apiCustom",
|
||||
|
|
@ -229,6 +256,19 @@ const EditIconModal = ({
|
|||
onExit();
|
||||
};
|
||||
|
||||
const onInputChange = ({ value }: IInputFieldParseTarget) => {
|
||||
setDisplayName((value as string) || "");
|
||||
// If you want live update in the formData:
|
||||
setIconState((prev) =>
|
||||
prev.formData
|
||||
? {
|
||||
...prev,
|
||||
formData: { ...prev.formData, display_name: value as string },
|
||||
}
|
||||
: prev
|
||||
);
|
||||
};
|
||||
|
||||
const onFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
|
|
@ -359,7 +399,7 @@ const EditIconModal = ({
|
|||
className={`${baseClass}__preview-card__fleet`}
|
||||
>
|
||||
<SoftwareDetailsSummary
|
||||
title={name}
|
||||
displayName={displayName || name}
|
||||
name={name}
|
||||
type={type}
|
||||
source={source}
|
||||
|
|
@ -460,14 +500,14 @@ const EditIconModal = ({
|
|||
// 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
|
||||
<SoftwareIcon
|
||||
name={previewInfo.name}
|
||||
name={displayName || previewInfo.name}
|
||||
source={previewInfo.source}
|
||||
url={isSoftwarePackage ? undefined : software.icon_url} // fallback PNG icons only exist for VPP apps
|
||||
uploadedAt={iconUploadedAt}
|
||||
/>
|
||||
)}
|
||||
<div className={`${baseClass}__self-service-preview-name`}>
|
||||
<TooltipTruncatedText value={previewInfo.name} />
|
||||
<TooltipTruncatedText value={displayName || previewInfo.name} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -479,7 +519,22 @@ const EditIconModal = ({
|
|||
|
||||
const renderForm = () => (
|
||||
<>
|
||||
<InputField
|
||||
label="Display name"
|
||||
onChange={onInputChange}
|
||||
name="displayName"
|
||||
value={displayName}
|
||||
parseTarget
|
||||
helpText={
|
||||
<>
|
||||
Optional. If left blank, Fleet will use{" "}
|
||||
<strong>{previewInfo.name}</strong>.
|
||||
</>
|
||||
}
|
||||
autofocus
|
||||
/>
|
||||
<FileUploader
|
||||
label="Icon"
|
||||
canEdit
|
||||
onDeleteFile={onDeleteFile}
|
||||
graphicName="file-png"
|
||||
|
|
@ -511,29 +566,97 @@ const EditIconModal = ({
|
|||
);
|
||||
|
||||
const onClickSave = async () => {
|
||||
setIsUpdatingIcon(true);
|
||||
setIsUpdatingSoftwareInfo(true);
|
||||
try {
|
||||
if (!iconState.formData?.icon) {
|
||||
await softwareAPI.deleteSoftwareIcon(softwareId, teamIdForApi);
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully removed icon from <b>{software?.name}</b>.
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
await softwareAPI.editSoftwareIcon(
|
||||
softwareId,
|
||||
teamIdForApi,
|
||||
iconState.formData
|
||||
);
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Successfully edited <b>{previewInfo.name}</b>.
|
||||
</>
|
||||
);
|
||||
const notifications: INotification[] = [];
|
||||
|
||||
// Process icon change
|
||||
try {
|
||||
if (!iconState.formData?.icon) {
|
||||
await softwareAPI.deleteSoftwareIcon(softwareId, teamIdForApi);
|
||||
notifications.push({
|
||||
id: "icon-removed",
|
||||
alertType: "success",
|
||||
isVisible: true,
|
||||
message: (
|
||||
<>
|
||||
Successfully removed icon from <b>{software?.name}</b>.
|
||||
</>
|
||||
),
|
||||
persistOnPageChange: false,
|
||||
});
|
||||
} else {
|
||||
await softwareAPI.editSoftwareIcon(
|
||||
softwareId,
|
||||
teamIdForApi,
|
||||
iconState.formData
|
||||
);
|
||||
notifications.push({
|
||||
id: "icon-edited",
|
||||
alertType: "success",
|
||||
isVisible: true,
|
||||
message: (
|
||||
<>
|
||||
Successfully edited <b>{previewInfo.name}</b>.
|
||||
</>
|
||||
),
|
||||
persistOnPageChange: false,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorReason(e) || DEFAULT_ERROR_MESSAGE;
|
||||
notifications.push({
|
||||
id: "icon-error",
|
||||
alertType: "error",
|
||||
isVisible: true,
|
||||
message: errorMessage,
|
||||
persistOnPageChange: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Process display name change
|
||||
if (displayName !== previewInfo.name) {
|
||||
try {
|
||||
await (installerType === "package"
|
||||
? softwareAPI.editSoftwarePackage({
|
||||
data: { displayName },
|
||||
softwareId,
|
||||
teamId: teamIdForApi,
|
||||
})
|
||||
: mdmAppleAPI.editVppApp(softwareId, teamIdForApi, {
|
||||
displayName,
|
||||
}));
|
||||
notifications.push({
|
||||
id: "name-edited",
|
||||
alertType: "success",
|
||||
isVisible: true,
|
||||
message: (
|
||||
<>
|
||||
Successfully renamed <b>{previewInfo.name}</b> to{" "}
|
||||
<b>{displayName}</b>.
|
||||
</>
|
||||
),
|
||||
persistOnPageChange: false,
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage = getErrorReason(e) || DEFAULT_ERROR_MESSAGE;
|
||||
notifications.push({
|
||||
id: "name-error",
|
||||
alertType: "error",
|
||||
isVisible: true,
|
||||
message: errorMessage,
|
||||
persistOnPageChange: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show all gathered messages
|
||||
if (notifications.length > 1) {
|
||||
renderMultiFlash({ notifications });
|
||||
} else if (notifications.length === 1) {
|
||||
renderFlash(notifications[0].alertType, notifications[0].message);
|
||||
}
|
||||
|
||||
refetchSoftwareTitle();
|
||||
setIconUploadedAt(new Date().toISOString());
|
||||
onExitEditIconModal();
|
||||
|
|
@ -541,7 +664,7 @@ const EditIconModal = ({
|
|||
const errorMessage = getErrorReason(e) || DEFAULT_ERROR_MESSAGE;
|
||||
renderFlash("error", errorMessage);
|
||||
} finally {
|
||||
setIsUpdatingIcon(false);
|
||||
setIsUpdatingSoftwareInfo(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -562,8 +685,8 @@ const EditIconModal = ({
|
|||
<Button
|
||||
type="submit"
|
||||
onClick={onClickSave}
|
||||
isLoading={isUpdatingIcon}
|
||||
disabled={!canSaveIcon || isUpdatingIcon}
|
||||
isLoading={isUpdatingSoftwareInfo}
|
||||
disabled={!canSaveForm || isUpdatingSoftwareInfo}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ const SoftwareSummaryCard = ({
|
|||
<>
|
||||
<Card borderRadiusSize="xxlarge" className={baseClass}>
|
||||
<SoftwareDetailsSummary
|
||||
title={title.name}
|
||||
displayName={title.display_name || title.name}
|
||||
type={formatSoftwareType(title)}
|
||||
versions={title.versions?.length ?? 0}
|
||||
hostCount={title.hosts_count}
|
||||
|
|
@ -119,7 +119,8 @@ const SoftwareSummaryCard = ({
|
|||
isSoftwarePackage(softwareInstaller) ? "package" : "vpp"
|
||||
}
|
||||
previewInfo={{
|
||||
name: title.name,
|
||||
name: softwareInstaller.display_name || title.name,
|
||||
titleName: title.name,
|
||||
type: formatSoftwareType(title),
|
||||
source: title.source,
|
||||
currentIconUrl: title.icon_url,
|
||||
|
|
|
|||
|
|
@ -54,12 +54,14 @@ describe("SoftwareTitleDetailsPage helpers", () => {
|
|||
const softwareTitle: ISoftwareTitleDetails = {
|
||||
id: 1,
|
||||
name: "Test Software",
|
||||
display_name: "Test App",
|
||||
icon_url: "https://example.com/icon.png",
|
||||
versions: [{ id: 1, version: "1.0.0", vulnerabilities: [] }],
|
||||
software_package: null,
|
||||
app_store_app: {
|
||||
app_store_id: "1",
|
||||
name: "Test App",
|
||||
display_name: "Test App",
|
||||
created_at: "2020-01-01T00:00:00.000Z",
|
||||
latest_version: "1.0.1",
|
||||
platform: "darwin",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const getSoftwareNameCellData = (
|
|||
|
||||
return {
|
||||
name: softwareTitle.name,
|
||||
displayName: softwareTitle.display_name,
|
||||
source: softwareTitle.source,
|
||||
path: softwareTitleDetailsPath,
|
||||
hasInstaller: hasInstaller && !isAllTeams,
|
||||
|
|
@ -115,6 +116,7 @@ const generateTableHeaders = (
|
|||
return (
|
||||
<SoftwareNameCell
|
||||
name={nameCellData.name}
|
||||
display_name={nameCellData.displayName}
|
||||
source={nameCellData.source}
|
||||
path={nameCellData.path}
|
||||
router={router}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const generateTableHeaders = (
|
|||
disableSortBy: false,
|
||||
accessor: "name",
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
const { id, name, source } = cellProps.row.original;
|
||||
const { id, name, display_name, source } = cellProps.row.original;
|
||||
|
||||
const softwareVersionDetailsPath = getPathWithQueryParams(
|
||||
PATHS.SOFTWARE_VERSION_DETAILS(id.toString()),
|
||||
|
|
@ -55,6 +55,7 @@ const generateTableHeaders = (
|
|||
return (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
// iconUrl does not exist on ISoftwareVersion
|
||||
path={softwareVersionDetailsPath}
|
||||
|
|
|
|||
|
|
@ -179,7 +179,9 @@ const SoftwareVersionDetailsPage = ({
|
|||
className={`${baseClass}__summary-section`}
|
||||
>
|
||||
<SoftwareDetailsSummary
|
||||
title={`${softwareVersion.name}, ${softwareVersion.version}`}
|
||||
displayName={`${
|
||||
softwareVersion.display_name || softwareVersion.name
|
||||
}, ${softwareVersion.version}`}
|
||||
type={formatSoftwareType(softwareVersion)}
|
||||
hostCount={hostsCount}
|
||||
queryParams={{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import DataSet from "components/DataSet";
|
||||
import LastUpdatedHostCount from "components/LastUpdatedHostCount";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import TooltipTruncatedText from "components/TooltipTruncatedText";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
|
@ -28,7 +29,10 @@ import OSIcon from "../../icons/OSIcon";
|
|||
const baseClass = "software-details-summary";
|
||||
|
||||
interface ISoftwareDetailsSummaryProps {
|
||||
title: string;
|
||||
/** Name displayed in UI */
|
||||
displayName: string;
|
||||
/** Name is keyed for fallback icon */
|
||||
name?: string;
|
||||
type?: string;
|
||||
hostCount?: number;
|
||||
countsUpdatedAt?: string;
|
||||
|
|
@ -36,7 +40,6 @@ interface ISoftwareDetailsSummaryProps {
|
|||
* Optional as isPreview mode doesn't have host count/link
|
||||
*/
|
||||
queryParams?: QueryParams;
|
||||
name?: string;
|
||||
source?: string;
|
||||
versions?: number;
|
||||
iconUrl?: string | null;
|
||||
|
|
@ -52,7 +55,7 @@ interface ISoftwareDetailsSummaryProps {
|
|||
}
|
||||
|
||||
const SoftwareDetailsSummary = ({
|
||||
title,
|
||||
displayName,
|
||||
type,
|
||||
hostCount,
|
||||
countsUpdatedAt,
|
||||
|
|
@ -108,14 +111,14 @@ const SoftwareDetailsSummary = ({
|
|||
)}
|
||||
<dl className={`${baseClass}__info`}>
|
||||
<h1>
|
||||
{ROLLING_ARCH_LINUX_VERSIONS.includes(title) ? (
|
||||
{ROLLING_ARCH_LINUX_VERSIONS.includes(displayName) ? (
|
||||
// wrap a tooltip around the "rolling" suffix
|
||||
<>
|
||||
{title.slice(0, -8)}
|
||||
{displayName.slice(0, -8)}
|
||||
<TooltipWrapperArchLinuxRolling />
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
<TooltipTruncatedText value={displayName} />
|
||||
)}
|
||||
{onClickEditIcon && (
|
||||
<div className={`${baseClass}__edit-icon`}>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
&__info {
|
||||
flex-grow: 1;
|
||||
align-self: center;
|
||||
|
||||
.truncated-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
|
|||
|
|
@ -613,7 +613,9 @@ const HostSoftwareLibrary = ({
|
|||
details={{
|
||||
hostDisplayName,
|
||||
fleetInstallStatus: selectedHostSWIpaInstallDetails.status,
|
||||
appName: selectedHostSWIpaInstallDetails.name,
|
||||
appName:
|
||||
selectedHostSWIpaInstallDetails.display_name ||
|
||||
selectedHostSWIpaInstallDetails.name,
|
||||
commandUuid:
|
||||
selectedHostSWIpaInstallDetails.software_package?.last_install
|
||||
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
|
||||
|
|
@ -646,7 +648,9 @@ const HostSoftwareLibrary = ({
|
|||
details={{
|
||||
fleetInstallStatus: selectedVPPInstallDetails.status,
|
||||
hostDisplayName,
|
||||
appName: selectedVPPInstallDetails.name,
|
||||
appName:
|
||||
selectedVPPInstallDetails.display_name ||
|
||||
selectedVPPInstallDetails.name,
|
||||
commandUuid: selectedVPPInstallDetails.commandUuid,
|
||||
}}
|
||||
hostSoftware={selectedVPPInstallDetails}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export const generateHostSWLibraryTableHeaders = ({
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
display_name,
|
||||
source,
|
||||
icon_url,
|
||||
app_store_app,
|
||||
|
|
@ -114,6 +115,7 @@ export const generateHostSWLibraryTableHeaders = ({
|
|||
return (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
iconUrl={icon_url}
|
||||
path={softwareTitleDetailsPath}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,11 @@ export const generateSoftwareTableHeaders = (): ISoftwareTableConfig[] => {
|
|||
disableSortBy: false,
|
||||
disableGlobalFilter: false,
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
const { name, source, icon_url } = cellProps.row.original;
|
||||
const { name, display_name, source, icon_url } = cellProps.row.original;
|
||||
return (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
iconUrl={icon_url}
|
||||
pageContext="deviceUser"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export const generateSoftwareTableHeaders = ({
|
|||
const {
|
||||
id,
|
||||
name,
|
||||
display_name,
|
||||
source,
|
||||
app_store_app,
|
||||
software_package,
|
||||
|
|
@ -84,6 +85,7 @@ export const generateSoftwareTableHeaders = ({
|
|||
return (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
iconUrl={icon_url}
|
||||
path={softwareTitleDetailsPath}
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ type IInstallStatusCellProps = {
|
|||
};
|
||||
|
||||
const getSoftwarePackageName = (software: IHostSoftware) =>
|
||||
software.software_package?.name;
|
||||
software.software_package?.display_name || software.software_package?.name;
|
||||
|
||||
const resolveDisplayText = (
|
||||
displayText: IStatusDisplayConfig["displayText"],
|
||||
|
|
@ -455,7 +455,7 @@ const InstallStatusCell = ({
|
|||
if (lastUninstall) {
|
||||
if ("script_execution_id" in lastUninstall) {
|
||||
onShowUninstallDetails({
|
||||
softwareName: software.name || "",
|
||||
softwareName: software.display_name || software.name || "",
|
||||
softwarePackageName,
|
||||
uninstallStatus: (software.status ||
|
||||
"pending_uninstall") as SoftwareUninstallStatus,
|
||||
|
|
|
|||
|
|
@ -641,7 +641,9 @@ const SoftwareSelfService = ({
|
|||
details={{
|
||||
hostDisplayName,
|
||||
fleetInstallStatus: selectedHostSWIpaInstallDetails.status,
|
||||
appName: selectedHostSWIpaInstallDetails.name,
|
||||
appName:
|
||||
selectedHostSWIpaInstallDetails.display_name ||
|
||||
selectedHostSWIpaInstallDetails.name,
|
||||
commandUuid:
|
||||
selectedHostSWIpaInstallDetails.software_package?.last_install
|
||||
?.install_uuid, // slightly redundant, see explanation in `SoftwareInstallDetailsModal
|
||||
|
|
@ -672,7 +674,9 @@ const SoftwareSelfService = ({
|
|||
details={{
|
||||
fleetInstallStatus: selectedVPPInstallDetails.status,
|
||||
hostDisplayName,
|
||||
appName: selectedVPPInstallDetails.name,
|
||||
appName:
|
||||
selectedVPPInstallDetails.display_name ||
|
||||
selectedVPPInstallDetails.name,
|
||||
commandUuid: selectedVPPInstallDetails.commandUuid,
|
||||
}}
|
||||
hostSoftware={selectedVPPInstallDetails}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,11 @@ export const generateSoftwareTableHeaders = ({
|
|||
disableSortBy: false,
|
||||
disableGlobalFilter: false,
|
||||
Cell: (cellProps: ITableStringCellProps) => {
|
||||
const { name, source, icon_url } = cellProps.row.original;
|
||||
const { name, display_name, source, icon_url } = cellProps.row.original;
|
||||
return (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={display_name}
|
||||
source={source}
|
||||
iconUrl={icon_url}
|
||||
pageContext="deviceUser"
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ describe("SoftwareUpdateModal", () => {
|
|||
|
||||
expect(screen.queryByTestId("error-outline-icon")).toBeInTheDocument();
|
||||
expect(screen.getByText(/New version of/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/mock software.app/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Mock Software/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Update the current version on/i)
|
||||
).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -89,11 +89,13 @@ const SoftwareUpdateModal = ({
|
|||
id,
|
||||
status,
|
||||
name,
|
||||
display_name,
|
||||
installed_versions,
|
||||
software_package,
|
||||
app_store_app,
|
||||
} = software;
|
||||
const installerName = software_package?.name || "";
|
||||
const installerName =
|
||||
software_package?.display_name || software_package?.name || "";
|
||||
const installerVersion = software_package?.version || app_store_app?.version;
|
||||
|
||||
const onClickUpdate = () => {
|
||||
|
|
@ -114,7 +116,7 @@ const SoftwareUpdateModal = ({
|
|||
hostDisplayName={hostDisplayName}
|
||||
isDeviceUser={isDeviceUser}
|
||||
softwareStatus={status}
|
||||
softwareName={name}
|
||||
softwareName={display_name || name}
|
||||
installerName={installerName}
|
||||
installerVersion={installerVersion}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { IMdmVppToken } from "interfaces/mdm";
|
|||
import { ApplePlatform } from "interfaces/platform";
|
||||
import { SoftwareCategory } from "interfaces/software";
|
||||
import { ISoftwareVppFormData } from "pages/SoftwarePage/components/forms/SoftwareVppForm/SoftwareVppForm";
|
||||
import { ISoftwareDisplayNameFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal";
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { listNamesFromSelectedLabels } from "components/TargetLabelSelector/TargetLabelSelector";
|
||||
|
|
@ -56,6 +57,42 @@ export interface IUploadVppTokenReponse {
|
|||
|
||||
export type IRenewVppTokenResponse = IUploadVppTokenReponse;
|
||||
|
||||
const handleDisplayNameVppForm = (
|
||||
formData: ISoftwareDisplayNameFormData,
|
||||
teamId: number
|
||||
): IEditVppAppPostBody => {
|
||||
return {
|
||||
self_service: false, // or some default as needed
|
||||
team_id: teamId,
|
||||
// you might not need categories/labels with this form
|
||||
};
|
||||
};
|
||||
|
||||
const handleEditVppForm = (
|
||||
formData: ISoftwareVppFormData,
|
||||
teamId: number
|
||||
): IEditVppAppPostBody => {
|
||||
const body: IEditVppAppPostBody = {
|
||||
self_service: formData.selfService,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
if (formData.categories && formData.categories.length > 0) {
|
||||
body.categories = formData.categories as SoftwareCategory[];
|
||||
}
|
||||
|
||||
if (formData.targetType === "Custom") {
|
||||
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
|
||||
if (formData.customTarget === "labelsIncludeAny") {
|
||||
body.labels_include_any = selectedLabels;
|
||||
} else {
|
||||
body.labels_exclude_any = selectedLabels;
|
||||
}
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
export default {
|
||||
getAppleAPNInfo: () => {
|
||||
const { MDM_APPLE_PNS } = endpoints;
|
||||
|
|
@ -126,29 +163,17 @@ export default {
|
|||
editVppApp: (
|
||||
softwareId: number,
|
||||
teamId: number,
|
||||
formData: ISoftwareVppFormData
|
||||
formData: ISoftwareVppFormData | ISoftwareDisplayNameFormData
|
||||
) => {
|
||||
const { EDIT_SOFTWARE_VPP } = endpoints;
|
||||
|
||||
const body: IEditVppAppPostBody = {
|
||||
self_service: formData.selfService,
|
||||
team_id: teamId,
|
||||
};
|
||||
|
||||
// Add categories if present
|
||||
if (formData.categories && formData.categories.length > 0) {
|
||||
body.categories = formData.categories as SoftwareCategory[];
|
||||
let body: IEditVppAppPostBody;
|
||||
if ("displayName" in formData) {
|
||||
// Handles Edit display name form only
|
||||
body = handleDisplayNameVppForm(formData, teamId);
|
||||
} else {
|
||||
// Handles primary Edit VPP form
|
||||
body = handleEditVppForm(formData as ISoftwareVppFormData, teamId);
|
||||
}
|
||||
|
||||
if (formData.targetType === "Custom") {
|
||||
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
|
||||
if (formData.customTarget === "labelsIncludeAny") {
|
||||
body.labels_include_any = selectedLabels;
|
||||
} else {
|
||||
body.labels_exclude_any = selectedLabels;
|
||||
}
|
||||
}
|
||||
|
||||
return sendRequest("PATCH", EDIT_SOFTWARE_VPP(softwareId), body);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "utilities/url";
|
||||
import { IPackageFormData } from "pages/SoftwarePage/components/forms/PackageForm/PackageForm";
|
||||
import { IEditPackageFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal";
|
||||
import { ISoftwareDisplayNameFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal";
|
||||
import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage";
|
||||
import { listNamesFromSelectedLabels } from "components/TargetLabelSelector/TargetLabelSelector";
|
||||
|
||||
|
|
@ -156,6 +157,54 @@ const ORDER_DIRECTION = "asc";
|
|||
export const MAX_FILE_SIZE_MB = 3000;
|
||||
export const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
|
||||
|
||||
const handleDisplayNameForm = (
|
||||
data: ISoftwareDisplayNameFormData,
|
||||
formData: FormData
|
||||
) => {
|
||||
formData.append("display_name", data.displayName || "");
|
||||
};
|
||||
|
||||
const handleEditPackageForm = (
|
||||
data: IEditPackageFormData,
|
||||
formData: FormData,
|
||||
orignalPackage: ISoftwarePackage
|
||||
) => {
|
||||
data.software && formData.append("software", data.software);
|
||||
formData.append("self_service", data.selfService.toString());
|
||||
formData.append("install_script", data.installScript);
|
||||
formData.append("pre_install_query", data.preInstallQuery || "");
|
||||
formData.append("post_install_script", data.postInstallScript || "");
|
||||
formData.append("uninstall_script", data.uninstallScript || "");
|
||||
if (data.categories) {
|
||||
data.categories.forEach((category) => {
|
||||
formData.append("categories", category);
|
||||
});
|
||||
}
|
||||
|
||||
// clear out labels if targetType is "All hosts"
|
||||
if (data.targetType === "All hosts") {
|
||||
if (orignalPackage.labels_include_any) {
|
||||
formData.append("labels_include_any", "");
|
||||
} else {
|
||||
formData.append("labels_exclude_any", "");
|
||||
}
|
||||
}
|
||||
|
||||
// add custom labels if targetType is "Custom"
|
||||
if (data.targetType === "Custom") {
|
||||
const selectedLabels = listNamesFromSelectedLabels(data.labelTargets);
|
||||
let labelKey = "";
|
||||
if (data.customTarget === "labelsIncludeAny") {
|
||||
labelKey = "labels_include_any";
|
||||
} else {
|
||||
labelKey = "labels_exclude_any";
|
||||
}
|
||||
selectedLabels?.forEach((label) => {
|
||||
formData.append(labelKey, label);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
load: async ({
|
||||
page,
|
||||
|
|
@ -326,8 +375,8 @@ export default {
|
|||
onUploadProgress,
|
||||
signal,
|
||||
}: {
|
||||
data: IEditPackageFormData;
|
||||
orignalPackage: ISoftwarePackage;
|
||||
data: IEditPackageFormData | ISoftwareDisplayNameFormData;
|
||||
orignalPackage?: ISoftwarePackage;
|
||||
softwareId: number;
|
||||
teamId: number;
|
||||
timeout?: number;
|
||||
|
|
@ -335,42 +384,23 @@ export default {
|
|||
signal?: AbortSignal;
|
||||
}) => {
|
||||
const { EDIT_SOFTWARE_PACKAGE } = endpoints;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("team_id", teamId.toString());
|
||||
data.software && formData.append("software", data.software);
|
||||
formData.append("self_service", data.selfService.toString());
|
||||
formData.append("install_script", data.installScript);
|
||||
formData.append("pre_install_query", data.preInstallQuery || "");
|
||||
formData.append("post_install_script", data.postInstallScript || "");
|
||||
formData.append("uninstall_script", data.uninstallScript || "");
|
||||
if (data.categories) {
|
||||
data.categories.forEach((category) => {
|
||||
formData.append("categories", category);
|
||||
});
|
||||
}
|
||||
|
||||
// clear out labels if targetType is "All hosts"
|
||||
if (data.targetType === "All hosts") {
|
||||
if (orignalPackage.labels_include_any) {
|
||||
formData.append("labels_include_any", "");
|
||||
} else {
|
||||
formData.append("labels_exclude_any", "");
|
||||
if ("displayName" in data) {
|
||||
// Handles Edit display name form only
|
||||
handleDisplayNameForm(data, formData);
|
||||
} else {
|
||||
// TODO: Confirm if orignalPackage is required
|
||||
if (!orignalPackage) {
|
||||
throw new Error("originalPackage is required for EditPackageFormData");
|
||||
}
|
||||
}
|
||||
|
||||
// add custom labels if targetType is "Custom"
|
||||
if (data.targetType === "Custom") {
|
||||
const selectedLabels = listNamesFromSelectedLabels(data.labelTargets);
|
||||
let labelKey = "";
|
||||
if (data.customTarget === "labelsIncludeAny") {
|
||||
labelKey = "labels_include_any";
|
||||
} else {
|
||||
labelKey = "labels_exclude_any";
|
||||
}
|
||||
selectedLabels?.forEach((label) => {
|
||||
formData.append(labelKey, label);
|
||||
});
|
||||
// Handles primary Edit Package form
|
||||
handleEditPackageForm(
|
||||
data as IEditPackageFormData,
|
||||
formData,
|
||||
orignalPackage
|
||||
);
|
||||
}
|
||||
|
||||
return sendRequestWithProgress({
|
||||
|
|
|
|||
Loading…
Reference in a new issue