Improve contextual actions in new apps marketplace (#2975)
This commit is contained in:
parent
ae5af8ff3d
commit
513655f3a1
50 changed files with 1598 additions and 301 deletions
|
@ -531,6 +531,10 @@
|
|||
"context": "order status",
|
||||
"string": "Partially returned"
|
||||
},
|
||||
"26VlBZ": {
|
||||
"context": "information",
|
||||
"string": "Problem occured during installation."
|
||||
},
|
||||
"28GZnc": {
|
||||
"string": "Start typing to begin search..."
|
||||
},
|
||||
|
@ -2123,6 +2127,10 @@
|
|||
"context": "dialog header",
|
||||
"string": "Select a page type"
|
||||
},
|
||||
"F1VtFa": {
|
||||
"context": "information",
|
||||
"string": "Installation is pending..."
|
||||
},
|
||||
"F2cR19": {
|
||||
"context": "error message",
|
||||
"string": "Error details: {message}"
|
||||
|
@ -2938,6 +2946,10 @@
|
|||
"context": "edit menu item, header",
|
||||
"string": "Edit Item"
|
||||
},
|
||||
"KMxiiF": {
|
||||
"context": "button",
|
||||
"string": "Retry"
|
||||
},
|
||||
"KN7zKn": {
|
||||
"string": "Error"
|
||||
},
|
||||
|
|
|
@ -36,11 +36,7 @@ export const appsList = gql`
|
|||
export const appsInProgressList = gql`
|
||||
query AppsInstallations {
|
||||
appsInstallations {
|
||||
status
|
||||
message
|
||||
appName
|
||||
manifestUrl
|
||||
id
|
||||
...AppInstallation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -108,9 +108,9 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
|
|||
onCompleted: data => {
|
||||
if (!data?.appDelete?.errors?.length) {
|
||||
refetch();
|
||||
closeModal();
|
||||
refetchExtensionList();
|
||||
removeAppNotify();
|
||||
navigate(appsListPath);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -53,6 +53,16 @@ export const appFragment = gql`
|
|||
}
|
||||
`;
|
||||
|
||||
export const appInstallationFragment = gql`
|
||||
fragment AppInstallation on AppInstallation {
|
||||
status
|
||||
message
|
||||
appName
|
||||
manifestUrl
|
||||
id
|
||||
}
|
||||
`;
|
||||
|
||||
export const appListItemFragment = gql`
|
||||
fragment AppListItem on App {
|
||||
id
|
||||
|
|
|
@ -67,6 +67,15 @@ export const AppFragmentDoc = gql`
|
|||
}
|
||||
}
|
||||
${WebhookFragmentDoc}`;
|
||||
export const AppInstallationFragmentDoc = gql`
|
||||
fragment AppInstallation on AppInstallation {
|
||||
status
|
||||
message
|
||||
appName
|
||||
manifestUrl
|
||||
id
|
||||
}
|
||||
`;
|
||||
export const AppPermissionFragmentDoc = gql`
|
||||
fragment AppPermission on Permission {
|
||||
name
|
||||
|
@ -3320,14 +3329,10 @@ export type AppsListQueryResult = Apollo.QueryResult<Types.AppsListQuery, Types.
|
|||
export const AppsInstallationsDocument = gql`
|
||||
query AppsInstallations {
|
||||
appsInstallations {
|
||||
status
|
||||
message
|
||||
appName
|
||||
manifestUrl
|
||||
id
|
||||
...AppInstallation
|
||||
}
|
||||
}
|
||||
`;
|
||||
${AppInstallationFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useAppsInstallationsQuery__
|
||||
|
|
|
@ -7057,6 +7057,8 @@ export type AppManifestFragment = { __typename: 'Manifest', identifier: string,
|
|||
|
||||
export type AppFragment = { __typename: 'App', id: string, name: string | null, created: any | null, isActive: boolean | null, type: AppTypeEnum | null, homepageUrl: string | null, appUrl: string | null, manifestUrl: string | null, configurationUrl: string | null, supportUrl: string | null, version: string | null, accessToken: string | null, privateMetadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, metadata: Array<{ __typename: 'MetadataItem', key: string, value: string }>, tokens: Array<{ __typename: 'AppToken', authToken: string | null, id: string, name: string | null }> | null, webhooks: Array<{ __typename: 'Webhook', id: string, name: string, isActive: boolean, app: { __typename: 'App', id: string, name: string | null } }> | null };
|
||||
|
||||
export type AppInstallationFragment = { __typename: 'AppInstallation', status: JobStatusEnum, message: string | null, appName: string, manifestUrl: string, id: string };
|
||||
|
||||
export type AppListItemFragment = { __typename: 'App', id: string, name: string | null, isActive: boolean | null, type: AppTypeEnum | null, appUrl: string | null, manifestUrl: string | null, version: string | null, permissions: Array<{ __typename: 'Permission', name: string, code: PermissionEnum }> | null };
|
||||
|
||||
export type AppPermissionFragment = { __typename: 'Permission', name: string, code: PermissionEnum };
|
||||
|
|
|
@ -352,6 +352,11 @@ export const buttonMessages = defineMessages({
|
|||
defaultMessage: "Install",
|
||||
description: "button",
|
||||
},
|
||||
retry: {
|
||||
id: "KMxiiF",
|
||||
defaultMessage: "Retry",
|
||||
description: "button",
|
||||
},
|
||||
});
|
||||
|
||||
export const sectionNames = defineMessages({
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { AppInstallationFragment } from "@dashboard/graphql";
|
||||
import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
||||
import { resolveInstallationOfMarketplaceApp } from "@dashboard/new-apps/utils";
|
||||
import { Skeleton } from "@material-ui/lab";
|
||||
import React from "react";
|
||||
|
||||
|
@ -7,12 +9,14 @@ import { useStyles } from "./styles";
|
|||
|
||||
interface AllAppListProps {
|
||||
appList?: GetV2SaleorAppsResponse.SaleorApp[];
|
||||
appInstallationList?: AppInstallationFragment[];
|
||||
navigateToAppInstallPage?: (manifestUrl: string) => void;
|
||||
navigateToVercelDeploymentPage?: (vercelDeploymentUrl: string) => void;
|
||||
}
|
||||
|
||||
const AllAppList: React.FC<AllAppListProps> = ({
|
||||
appList,
|
||||
appInstallationList,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
}) => {
|
||||
|
@ -28,6 +32,10 @@ const AllAppList: React.FC<AllAppListProps> = ({
|
|||
<AppListCard
|
||||
key={app.name.en}
|
||||
app={app}
|
||||
appInstallation={resolveInstallationOfMarketplaceApp(
|
||||
app,
|
||||
appInstallationList,
|
||||
)}
|
||||
navigateToAppInstallPage={navigateToAppInstallPage}
|
||||
navigateToVercelDeploymentPage={navigateToVercelDeploymentPage}
|
||||
/>
|
||||
|
|
|
@ -4,9 +4,15 @@ export const useStyles = makeStyles(
|
|||
theme => ({
|
||||
appListWrapper: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(400px, 1fr))`,
|
||||
gridTemplateColumns: `repeat(1, 1fr)`,
|
||||
gap: theme.spacing(2),
|
||||
marginTop: theme.spacing(1),
|
||||
[theme.breakpoints.up(900)]: {
|
||||
gridTemplateColumns: `repeat(2, 1fr)`,
|
||||
},
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
gridTemplateColumns: `repeat(3, 1fr)`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
|
|
@ -25,10 +25,10 @@ const AppActivateDialog: React.FC<AppActivateDialogProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
const isNameMissing = name === null || name === "";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName) {
|
||||
if (isNameMissing) {
|
||||
return intl.formatMessage(msgs.activateApp);
|
||||
}
|
||||
return intl.formatMessage(msgs.activateNamedApp, {
|
||||
|
|
|
@ -27,10 +27,10 @@ const AppDeactivateDialog: React.FC<AppDeactivateDialogProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
const isNameMissing = name === null || name === "";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName) {
|
||||
if (isNameMissing) {
|
||||
return intl.formatMessage(msgs.deactivateApp);
|
||||
}
|
||||
return intl.formatMessage(msgs.deactivateNamedApp, {
|
||||
|
|
|
@ -26,14 +26,14 @@ const AppDeleteDialog: React.FC<AppDeleteDialogProps> = ({
|
|||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const missingName = name === null || name === "";
|
||||
const isNameMissing = name === null || name === "";
|
||||
const isExternal = type === "EXTERNAL";
|
||||
|
||||
const getMainText = () => {
|
||||
if (missingName && isExternal) {
|
||||
if (isNameMissing && isExternal) {
|
||||
return intl.formatMessage(msgs.deleteApp);
|
||||
}
|
||||
if (missingName) {
|
||||
if (isNameMissing) {
|
||||
return intl.formatMessage(msgs.deleteLocalApp);
|
||||
}
|
||||
if (isExternal) {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import Decorator from "@dashboard/storybook/Decorator";
|
||||
import { storiesOf } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import AppInProgressDeleteDialog, {
|
||||
AppInProgressDeleteDialogProps,
|
||||
} from "./AppInProgressDeleteDialog";
|
||||
|
||||
const props: AppInProgressDeleteDialogProps = {
|
||||
confirmButtonState: "default",
|
||||
name: "App",
|
||||
onClose: () => undefined,
|
||||
onConfirm: () => undefined,
|
||||
open: true,
|
||||
};
|
||||
|
||||
storiesOf("Views / Apps / Delete app failed installation", module)
|
||||
.addDecorator(Decorator)
|
||||
.add("default", () => <AppInProgressDeleteDialog {...props} />)
|
||||
.add("unnamed app", () => <AppInProgressDeleteDialog {...props} name="" />);
|
|
@ -0,0 +1,72 @@
|
|||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
import AppInProgressDeleteDialog from "./AppInProgressDeleteDialog";
|
||||
import msgs from "./messages";
|
||||
|
||||
describe("Apps AppInProgressDeleteDialog", () => {
|
||||
it("displays action text with app name when app name passed", () => {
|
||||
// Arrange
|
||||
const name = "Test App";
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppInProgressDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={name}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteNamedApp.defaultMessage.replace(
|
||||
"{name}",
|
||||
name,
|
||||
);
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
|
||||
it("displays action text without app name when app name is empty", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppInProgressDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={""}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteApp.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
|
||||
it("displays action text without app name when app name is null", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppInProgressDeleteDialog
|
||||
confirmButtonState="default"
|
||||
open={true}
|
||||
name={null}
|
||||
onClose={jest.fn()}
|
||||
onConfirm={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const dialogContent = screen.getByTestId("dialog-content");
|
||||
|
||||
// Assert
|
||||
const expectedText = msgs.deleteApp.defaultMessage;
|
||||
expect(dialogContent).toHaveTextContent(expectedText);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
import ActionDialog from "@dashboard/components/ActionDialog";
|
||||
import { getStringOrPlaceholder } from "@dashboard/misc";
|
||||
import { DialogContentText } from "@material-ui/core";
|
||||
import { ConfirmButtonTransitionState } from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import msgs from "./messages";
|
||||
|
||||
export interface AppInProgressDeleteDialogProps {
|
||||
confirmButtonState: ConfirmButtonTransitionState;
|
||||
open: boolean;
|
||||
name?: string | null;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
const AppInProgressDeleteDialog = ({
|
||||
confirmButtonState,
|
||||
open,
|
||||
name,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: AppInProgressDeleteDialogProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const isNameMissing = name === null || name === "";
|
||||
|
||||
const getMainText = () => {
|
||||
if (isNameMissing) {
|
||||
return intl.formatMessage(msgs.deleteApp);
|
||||
}
|
||||
return intl.formatMessage(msgs.deleteNamedApp, {
|
||||
name: <strong>{getStringOrPlaceholder(name)}</strong>,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionDialog
|
||||
confirmButtonState={confirmButtonState}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
title={intl.formatMessage(msgs.header)}
|
||||
variant="delete"
|
||||
>
|
||||
<DialogContentText data-test-id="dialog-content">
|
||||
{getMainText()}
|
||||
</DialogContentText>
|
||||
</ActionDialog>
|
||||
);
|
||||
};
|
||||
AppInProgressDeleteDialog.displayName = "AppInProgressDeleteDialog";
|
||||
export default AppInProgressDeleteDialog;
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./AppInProgressDeleteDialog";
|
||||
export { default } from "./AppInProgressDeleteDialog";
|
|
@ -0,0 +1,20 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export default defineMessages({
|
||||
header: {
|
||||
id: "zQX6xO",
|
||||
defaultMessage: "Delete App",
|
||||
description: "dialog header",
|
||||
},
|
||||
deleteApp: {
|
||||
id: "6hLZNA",
|
||||
defaultMessage: "Are you sure you want to delete this app?",
|
||||
description: "delete app",
|
||||
},
|
||||
deleteNamedApp: {
|
||||
id: "EWD/wU",
|
||||
defaultMessage:
|
||||
"Deleting {name}, you will remove installation of the app. If you are paying for app subscription, remember to unsubscribe from the app in Saleor Marketplace. Are you sure you want to delete the app?",
|
||||
description: "delete app",
|
||||
},
|
||||
});
|
|
@ -1,5 +1,12 @@
|
|||
import { comingSoonApp, releasedApp } from "@dashboard/new-apps/fixtures";
|
||||
import * as context from "@dashboard/new-apps/context";
|
||||
import {
|
||||
comingSoonApp,
|
||||
failedAppInProgress,
|
||||
pendingAppInProgress,
|
||||
releasedApp,
|
||||
} from "@dashboard/new-apps/fixtures";
|
||||
import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
||||
import { appInstallationStatusMessages } from "@dashboard/new-apps/messages";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
@ -7,6 +14,14 @@ import React from "react";
|
|||
|
||||
import AppListCard from "./AppListCard";
|
||||
|
||||
jest.mock("@dashboard/new-apps/context", () => ({
|
||||
useAppListContext: jest.fn(() => ({
|
||||
openAppSettings: jest.fn(),
|
||||
removeAppInstallation: jest.fn(),
|
||||
retryAppInstallation: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("Apps AppListCard", () => {
|
||||
it("displays released app details when released app data passed", () => {
|
||||
// Arrange
|
||||
|
@ -144,4 +159,66 @@ describe("Apps AppListCard", () => {
|
|||
expect(logoPlaceholder?.textContent).toBe(app.name.en[0]);
|
||||
expect(logoImage).toBeFalsy();
|
||||
});
|
||||
|
||||
it("displays app installation details when failed installation data passed", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={releasedApp} appInstallation={failedAppInProgress} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const status = screen.getByTestId("app-installation-failed");
|
||||
const statusDetails = within(status).queryByText(
|
||||
appInstallationStatusMessages.failed.defaultMessage,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(statusDetails).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays app installation details when pending installation data passed", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={releasedApp} appInstallation={pendingAppInProgress} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const status = screen.getByTestId("app-installation-pending");
|
||||
const statusText = within(status).queryByText(
|
||||
appInstallationStatusMessages.pending.defaultMessage,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(statusText).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls handlers when failed installation data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
jest.spyOn(context, "useAppListContext").mockImplementation(() => ({
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}));
|
||||
render(
|
||||
<Wrapper>
|
||||
<AppListCard app={releasedApp} appInstallation={failedAppInProgress} />
|
||||
</Wrapper>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const retryButton = screen.getByTestId("app-retry-install-button");
|
||||
const removeButton = screen.getByTestId("app-remove-install-button");
|
||||
|
||||
// Act
|
||||
await user.click(retryButton);
|
||||
await user.click(removeButton);
|
||||
|
||||
// Assert
|
||||
expect(retryAppInstallation).toHaveBeenCalledWith(failedAppInProgress.id);
|
||||
expect(retryAppInstallation).toHaveBeenCalledTimes(1);
|
||||
expect(removeAppInstallation).toHaveBeenCalledWith(failedAppInProgress.id);
|
||||
expect(removeAppInstallation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { AppInstallationFragment } from "@dashboard/graphql";
|
||||
import { useAppListContext } from "@dashboard/new-apps/context";
|
||||
import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
||||
import { getAppDetails } from "@dashboard/new-apps/utils";
|
||||
import { Card, CardContent } from "@material-ui/core";
|
||||
|
@ -12,24 +14,30 @@ import { useStyles } from "./styles";
|
|||
|
||||
interface AppListCardProps {
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
appInstallation?: AppInstallationFragment;
|
||||
navigateToAppInstallPage?: (manifestUrl: string) => void;
|
||||
navigateToVercelDeploymentPage?: (vercelDeploymentUrl: string) => void;
|
||||
}
|
||||
|
||||
const AppListCard: React.FC<AppListCardProps> = ({
|
||||
app,
|
||||
appInstallation,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const intl = useIntl();
|
||||
const { retryAppInstallation, removeAppInstallation } = useAppListContext();
|
||||
|
||||
const details = getAppDetails(
|
||||
const details = getAppDetails({
|
||||
intl,
|
||||
app,
|
||||
appInstallation,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
);
|
||||
retryAppInstallation,
|
||||
removeAppInstallation,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -41,8 +49,11 @@ const AppListCard: React.FC<AppListCardProps> = ({
|
|||
</CardContent>
|
||||
<AppListCardActions
|
||||
releaseDate={details.releaseDate}
|
||||
installationPending={details.installationPending}
|
||||
installHandler={details.installHandler}
|
||||
vercelDeployHandler={details.vercelDeployHandler}
|
||||
retryInstallHandler={details.retryInstallHandler}
|
||||
removeInstallHandler={details.removeInstallHandler}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
|
|
|
@ -1,27 +1,45 @@
|
|||
import { Button } from "@dashboard/components/Button";
|
||||
import Hr from "@dashboard/components/Hr";
|
||||
import { AppInstallationFragment } from "@dashboard/graphql";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { appInstallationStatusMessages } from "@dashboard/new-apps/messages";
|
||||
import { CardActions, Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import InstallErrorAction from "./ErrorInstallAction";
|
||||
import { messages } from "./messages";
|
||||
import { useActionsStyles } from "./styles";
|
||||
|
||||
interface AppListCardActionsProps {
|
||||
releaseDate: string | undefined;
|
||||
installationPending?: boolean;
|
||||
appInstallation?: AppInstallationFragment;
|
||||
installHandler?: () => void;
|
||||
vercelDeployHandler?: () => void;
|
||||
retryInstallHandler?: () => void;
|
||||
removeInstallHandler?: () => void;
|
||||
}
|
||||
|
||||
const AppListCardActions: React.FC<AppListCardActionsProps> = ({
|
||||
releaseDate,
|
||||
installationPending = false,
|
||||
appInstallation,
|
||||
installHandler,
|
||||
vercelDeployHandler,
|
||||
retryInstallHandler,
|
||||
removeInstallHandler,
|
||||
}) => {
|
||||
const classes = useActionsStyles();
|
||||
|
||||
if (!installHandler && !vercelDeployHandler && !releaseDate) {
|
||||
if (
|
||||
!installHandler &&
|
||||
!vercelDeployHandler &&
|
||||
!releaseDate &&
|
||||
!retryInstallHandler &&
|
||||
!removeInstallHandler &&
|
||||
!installationPending
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -47,6 +65,19 @@ const AppListCardActions: React.FC<AppListCardActionsProps> = ({
|
|||
<FormattedMessage {...buttonMessages.install} />
|
||||
</Button>
|
||||
)}
|
||||
{installationPending && (
|
||||
<Typography
|
||||
className={classes.cardActionsText}
|
||||
data-test-id="app-installation-pending"
|
||||
>
|
||||
<FormattedMessage {...appInstallationStatusMessages.pending} />
|
||||
</Typography>
|
||||
)}
|
||||
<InstallErrorAction
|
||||
appInstallation={appInstallation}
|
||||
retryInstall={retryInstallHandler}
|
||||
removeInstall={removeInstallHandler}
|
||||
/>
|
||||
{releaseDate && (
|
||||
<Typography className={classes.releaseDate}>
|
||||
<FormattedMessage
|
||||
|
|
68
src/new-apps/components/AppListCard/ErrorInstallAction.tsx
Normal file
68
src/new-apps/components/AppListCard/ErrorInstallAction.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { AppInstallationFragment } from "@dashboard/graphql";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { appInstallationStatusMessages } from "@dashboard/new-apps/messages";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import {
|
||||
Button,
|
||||
Indicator,
|
||||
Tooltip,
|
||||
TooltipMountWrapper,
|
||||
} from "@saleor/macaw-ui";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { useActionsStyles } from "./styles";
|
||||
|
||||
interface InstallErrorActionProps {
|
||||
appInstallation?: AppInstallationFragment;
|
||||
retryInstall?: () => void;
|
||||
removeInstall?: () => void;
|
||||
}
|
||||
|
||||
const InstallErrorAction = ({
|
||||
appInstallation,
|
||||
retryInstall,
|
||||
removeInstall,
|
||||
}: InstallErrorActionProps) => {
|
||||
const classes = useActionsStyles();
|
||||
|
||||
if (!retryInstall && !removeInstall) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography
|
||||
className={classes.cardActionsIssueText}
|
||||
data-test-id="app-installation-failed"
|
||||
>
|
||||
<FormattedMessage {...appInstallationStatusMessages.failed} />
|
||||
<Tooltip title={appInstallation?.message} variant="error">
|
||||
<TooltipMountWrapper>
|
||||
<Indicator icon="error" />
|
||||
</TooltipMountWrapper>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
{retryInstall && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={retryInstall}
|
||||
data-test-id="app-retry-install-button"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.retry} />
|
||||
</Button>
|
||||
)}
|
||||
{removeInstall && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={removeInstall}
|
||||
data-test-id="app-remove-install-button"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.cancel} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
InstallErrorAction.displayName = "InstallErrorAction";
|
||||
export default InstallErrorAction;
|
|
@ -103,6 +103,18 @@ export const useActionsStyles = makeStyles(
|
|||
padding: theme.spacing(2, 4),
|
||||
minHeight: theme.spacing(9),
|
||||
},
|
||||
cardActionsText: {
|
||||
width: "100%",
|
||||
},
|
||||
cardActionsIssueText: {
|
||||
width: "100%",
|
||||
color: theme.palette.error.main,
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
releaseDate: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Container from "@dashboard/components/Container";
|
||||
import PageHeader from "@dashboard/components/PageHeader";
|
||||
import PreviewPill from "@dashboard/components/PreviewPill";
|
||||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import { sectionNames } from "@dashboard/intl";
|
||||
import { AppUrls } from "@dashboard/new-apps/urls";
|
||||
|
@ -17,6 +18,7 @@ import { messages } from "./messages";
|
|||
import { useStyles } from "./styles";
|
||||
import { AppListPageSections } from "./types";
|
||||
import {
|
||||
getVerifiedAppsInstallations,
|
||||
getVerifiedInstallableMarketplaceApps,
|
||||
getVerifiedInstalledApps,
|
||||
resolveSectionsAvailability,
|
||||
|
@ -28,6 +30,7 @@ export interface AppListPageProps extends AppListPageSections, ListProps {
|
|||
|
||||
export const AppListPage: React.FC<AppListPageProps> = props => {
|
||||
const {
|
||||
appsInstallations,
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
comingSoonMarketplaceApps,
|
||||
|
@ -42,6 +45,10 @@ export const AppListPage: React.FC<AppListPageProps> = props => {
|
|||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
const verifiedAppsIntallations = getVerifiedAppsInstallations(
|
||||
appsInstallations,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
const verifiedInstallableMarketplaceApps = getVerifiedInstallableMarketplaceApps(
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
|
@ -80,6 +87,7 @@ export const AppListPage: React.FC<AppListPageProps> = props => {
|
|||
<SectionHeader title={intl.formatMessage(messages.installedApps)} />
|
||||
<InstalledAppList
|
||||
appList={verifiedInstalledApps}
|
||||
appInstallationList={verifiedAppsIntallations}
|
||||
disabled={disabled}
|
||||
settings={settings}
|
||||
onUpdateListSettings={onUpdateListSettings}
|
||||
|
@ -89,9 +97,17 @@ export const AppListPage: React.FC<AppListPageProps> = props => {
|
|||
<MarketplaceAlert error={marketplaceError} />
|
||||
{sectionsAvailability.all && !marketplaceError && (
|
||||
<>
|
||||
<SectionHeader title={intl.formatMessage(messages.allApps)} />
|
||||
<SectionHeader
|
||||
title={
|
||||
<>
|
||||
<FormattedMessage {...messages.allApps} />
|
||||
<PreviewPill className={classes.previewLabel} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<AllAppList
|
||||
appList={verifiedInstallableMarketplaceApps}
|
||||
appInstallationList={appsInstallations}
|
||||
navigateToAppInstallPage={navigateToAppInstallPage}
|
||||
navigateToVercelDeploymentPage={navigateToVercelDeploymentPage}
|
||||
/>
|
||||
|
@ -102,7 +118,10 @@ export const AppListPage: React.FC<AppListPageProps> = props => {
|
|||
<SectionHeader
|
||||
title={intl.formatMessage(messages.comingSoonApps)}
|
||||
/>
|
||||
<AllAppList appList={comingSoonMarketplaceApps} />
|
||||
<AllAppList
|
||||
appList={comingSoonMarketplaceApps}
|
||||
appInstallationList={appsInstallations}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,11 @@ export const messages = defineMessages({
|
|||
defaultMessage: "Installed Apps",
|
||||
description: "section header",
|
||||
},
|
||||
ongoingInstallations: {
|
||||
id: "nIrjSR",
|
||||
defaultMessage: "Ongoing Installations",
|
||||
description: "section header",
|
||||
},
|
||||
allApps: {
|
||||
id: "yZB41s",
|
||||
defaultMessage: "All Apps",
|
||||
|
|
|
@ -12,6 +12,9 @@ export const useStyles = makeStyles(
|
|||
margin: theme.spacing(8, 0, 3, 0),
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
previewLabel: {
|
||||
marginLeft: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
{ name: "AppListPage" },
|
||||
);
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { AppListItemFragment } from "@dashboard/graphql";
|
||||
import {
|
||||
AppInstallationFragment,
|
||||
AppListItemFragment,
|
||||
} from "@dashboard/graphql";
|
||||
import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
||||
|
||||
export interface AppListPageSections {
|
||||
appsInstallations?: AppInstallationFragment[];
|
||||
installedApps?: AppListItemFragment[];
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[];
|
||||
comingSoonMarketplaceApps?: GetV2SaleorAppsResponse.ComingSoonSaleorApp[];
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
PermissionEnum,
|
||||
} from "@dashboard/graphql";
|
||||
import {
|
||||
appsInProgress,
|
||||
comingSoonApp,
|
||||
installedAppsList,
|
||||
releasedApp,
|
||||
|
@ -20,6 +21,7 @@ describe("App List available sections util", () => {
|
|||
it("should return available app list sections when all data are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: appsInProgress,
|
||||
installedApps: installedAppsList,
|
||||
installableMarketplaceApps: [releasedApp],
|
||||
comingSoonMarketplaceApps: [comingSoonApp],
|
||||
|
@ -40,6 +42,7 @@ describe("App List available sections util", () => {
|
|||
it("should return no available app list sections when no data are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: [],
|
||||
installedApps: [],
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
|
@ -56,6 +59,111 @@ describe("App List available sections util", () => {
|
|||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return only installed apps section as available when only installed apps are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: [],
|
||||
installedApps: installedAppsList,
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: false,
|
||||
comingSoon: false,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return only installed apps section as available when only installations are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: appsInProgress,
|
||||
installedApps: [],
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: false,
|
||||
comingSoon: false,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return available app list sections when all data are undefined", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: undefined,
|
||||
installedApps: undefined,
|
||||
installableMarketplaceApps: undefined,
|
||||
comingSoonMarketplaceApps: undefined,
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: true,
|
||||
comingSoon: true,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return installed apps section as available when installed apps passed and installations is undefined", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: undefined,
|
||||
installedApps: installedAppsList,
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: false,
|
||||
comingSoon: false,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
|
||||
it("should return installed apps section as available when installed apps is undefined and installations are passed", () => {
|
||||
// Arrange
|
||||
const appSectionsData: AppListPageSections = {
|
||||
appsInstallations: appsInProgress,
|
||||
installedApps: undefined,
|
||||
installableMarketplaceApps: [],
|
||||
comingSoonMarketplaceApps: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const sectionsAvailability = resolveSectionsAvailability(appSectionsData);
|
||||
|
||||
// Assert
|
||||
const expectedSectionsAvailability = {
|
||||
installed: true,
|
||||
all: false,
|
||||
comingSoon: false,
|
||||
};
|
||||
expect(sectionsAvailability).toEqual(expectedSectionsAvailability);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App List verified installed apps util", () => {
|
||||
|
@ -134,10 +242,15 @@ describe("App List verified installed apps util", () => {
|
|||
{
|
||||
app: installedApps[0],
|
||||
isExternal: false,
|
||||
logo: {
|
||||
source: "https://www.example.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
},
|
||||
{
|
||||
app: installedApps[1],
|
||||
isExternal: true,
|
||||
logo: undefined,
|
||||
},
|
||||
];
|
||||
expect(verifiedInstalledApps).toEqual(expectedVerifiedInstalledApps);
|
||||
|
|
|
@ -1,34 +1,73 @@
|
|||
import { AppListItemFragment } from "@dashboard/graphql";
|
||||
import {
|
||||
AppInstallationFragment,
|
||||
AppListItemFragment,
|
||||
} from "@dashboard/graphql";
|
||||
import { GetV2SaleorAppsResponse } from "@dashboard/new-apps/marketplace.types";
|
||||
import { InstalledApp } from "@dashboard/new-apps/types";
|
||||
import { AppInstallation, InstalledApp } from "@dashboard/new-apps/types";
|
||||
|
||||
import { AppListPageSections } from "./types";
|
||||
|
||||
export const resolveSectionsAvailability = ({
|
||||
appsInstallations,
|
||||
installedApps,
|
||||
installableMarketplaceApps,
|
||||
comingSoonMarketplaceApps,
|
||||
}: AppListPageSections) => ({
|
||||
installed: !installedApps || !!installedApps.length,
|
||||
installed:
|
||||
!installedApps ||
|
||||
!!installedApps.length ||
|
||||
!appsInstallations ||
|
||||
!!appsInstallations.length,
|
||||
all: !installableMarketplaceApps || !!installableMarketplaceApps.length,
|
||||
comingSoon: !comingSoonMarketplaceApps || !!comingSoonMarketplaceApps.length,
|
||||
});
|
||||
|
||||
const isAppExternal = (
|
||||
const findAppInMarketplace = (
|
||||
manifestUrl: string | null,
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
) =>
|
||||
!!manifestUrl &&
|
||||
!!installableMarketplaceApps?.every(app => app.manifestUrl !== manifestUrl);
|
||||
) => {
|
||||
if (!manifestUrl) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return installableMarketplaceApps?.find(
|
||||
app => app.manifestUrl === manifestUrl,
|
||||
);
|
||||
};
|
||||
|
||||
export const getVerifiedInstalledApps = (
|
||||
installedApps?: AppListItemFragment[],
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
): InstalledApp[] | undefined =>
|
||||
installedApps?.map(app => ({
|
||||
app,
|
||||
isExternal: isAppExternal(app.manifestUrl, installableMarketplaceApps),
|
||||
}));
|
||||
installedApps?.map(app => {
|
||||
const marketplaceApp = findAppInMarketplace(
|
||||
app.manifestUrl,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
|
||||
return {
|
||||
app,
|
||||
isExternal: !marketplaceApp,
|
||||
logo: marketplaceApp?.logo,
|
||||
};
|
||||
});
|
||||
|
||||
export const getVerifiedAppsInstallations = (
|
||||
appsInstallations?: AppInstallationFragment[],
|
||||
installableMarketplaceApps?: GetV2SaleorAppsResponse.ReleasedSaleorApp[],
|
||||
): AppInstallation[] | undefined =>
|
||||
appsInstallations?.map(appInstallation => {
|
||||
const marketplaceApp = findAppInMarketplace(
|
||||
appInstallation.manifestUrl,
|
||||
installableMarketplaceApps,
|
||||
);
|
||||
|
||||
return {
|
||||
appInstallation,
|
||||
isExternal: !marketplaceApp,
|
||||
logo: marketplaceApp?.logo,
|
||||
};
|
||||
});
|
||||
|
||||
/*
|
||||
* Temporary solution for checking if app is not installed.
|
||||
|
|
|
@ -8,6 +8,7 @@ export const useStyles = makeStyles(
|
|||
},
|
||||
manifestText: {
|
||||
color: theme.palette.text.secondary,
|
||||
display: "inline",
|
||||
"&:hover svg": {
|
||||
visibility: "visible",
|
||||
},
|
||||
|
|
|
@ -1,34 +1,50 @@
|
|||
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
|
||||
import { InstalledApp } from "@dashboard/new-apps/types";
|
||||
import { AppInstallation, InstalledApp } from "@dashboard/new-apps/types";
|
||||
import { ListProps } from "@dashboard/types";
|
||||
import { Table, TableBody, TableFooter, TableRow } from "@material-ui/core";
|
||||
import { Skeleton } from "@material-ui/lab";
|
||||
import React from "react";
|
||||
|
||||
import InstalledAppListRow from "../InstalledAppListRow";
|
||||
import NotInstalledAppListRow from "../NotInstalledAppListRow";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface InstalledAppListProps extends ListProps {
|
||||
appList?: InstalledApp[];
|
||||
appInstallationList?: AppInstallation[];
|
||||
}
|
||||
|
||||
const InstalledAppList: React.FC<InstalledAppListProps> = ({
|
||||
appList,
|
||||
appInstallationList,
|
||||
disabled,
|
||||
settings,
|
||||
onUpdateListSettings,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
if (!appList) {
|
||||
if (!appList || !appInstallationList) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table className={classes.table}>
|
||||
<TableBody>
|
||||
{appList.map(({ app, isExternal }) => (
|
||||
<InstalledAppListRow key={app.id} app={app} isExternal={isExternal} />
|
||||
{appInstallationList?.map(({ appInstallation, logo, isExternal }) => (
|
||||
<NotInstalledAppListRow
|
||||
key={appInstallation.id}
|
||||
appInstallation={appInstallation}
|
||||
isExternal={isExternal}
|
||||
logo={logo}
|
||||
/>
|
||||
))}
|
||||
{appList.map(({ app, isExternal, logo }) => (
|
||||
<InstalledAppListRow
|
||||
key={app.id}
|
||||
app={app}
|
||||
isExternal={isExternal}
|
||||
logo={logo}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter>
|
||||
|
|
|
@ -3,10 +3,10 @@ import {
|
|||
AppListContext,
|
||||
AppListContextValues,
|
||||
} from "@dashboard/new-apps/context";
|
||||
import { activeApp, inactiveApp } from "@dashboard/new-apps/fixtures";
|
||||
import { activeApp } from "@dashboard/new-apps/fixtures";
|
||||
import { InstalledApp } from "@dashboard/new-apps/types";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen, within } from "@testing-library/react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { MemoryRouter as Router } from "react-router-dom";
|
||||
|
@ -32,9 +32,9 @@ const Component = ({
|
|||
describe("Apps InstalledAppListRow", () => {
|
||||
it("displays app details when basic app data passed", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
|
@ -42,9 +42,9 @@ describe("Apps InstalledAppListRow", () => {
|
|||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
@ -68,9 +68,9 @@ describe("Apps InstalledAppListRow", () => {
|
|||
|
||||
it("displays external label when app is external", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
|
@ -78,9 +78,9 @@ describe("Apps InstalledAppListRow", () => {
|
|||
isExternal: true,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
@ -92,9 +92,9 @@ describe("Apps InstalledAppListRow", () => {
|
|||
|
||||
it("displays tunnnel label when app is served via tunnnel", () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
const AppsConfig = getAppsConfig();
|
||||
render(
|
||||
<Component
|
||||
|
@ -107,9 +107,9 @@ describe("Apps InstalledAppListRow", () => {
|
|||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
@ -119,11 +119,11 @@ describe("Apps InstalledAppListRow", () => {
|
|||
expect(tunnelLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it("calls handlers when active app data passed and buttons clicked", async () => {
|
||||
it("calls handlers when app data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
|
@ -131,69 +131,20 @@ describe("Apps InstalledAppListRow", () => {
|
|||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const activeSwitch = within(
|
||||
screen.getByTestId("app-active-switch"),
|
||||
).getByRole("checkbox");
|
||||
const removeButton = screen.getByTestId("app-remove-button");
|
||||
|
||||
// Assert
|
||||
expect(activeSwitch).toBeChecked();
|
||||
const settingsButton = screen.getByTestId("app-settings-button");
|
||||
|
||||
// Act
|
||||
await user.click(activeSwitch);
|
||||
await user.click(removeButton);
|
||||
await user.click(settingsButton);
|
||||
|
||||
// Assert
|
||||
expect(deactivateApp).toHaveBeenCalledWith(activeApp.id);
|
||||
expect(deactivateApp).toHaveBeenCalledTimes(1);
|
||||
expect(activateApp).not.toHaveBeenCalled();
|
||||
expect(removeApp).toHaveBeenCalledWith(activeApp.id);
|
||||
expect(removeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls handlers when inactive app data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const activateApp = jest.fn();
|
||||
const deactivateApp = jest.fn();
|
||||
const removeApp = jest.fn();
|
||||
render(
|
||||
<Component
|
||||
data={{
|
||||
app: inactiveApp,
|
||||
isExternal: false,
|
||||
}}
|
||||
context={{
|
||||
activateApp,
|
||||
deactivateApp,
|
||||
removeApp,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const activeSwitch = within(
|
||||
screen.getByTestId("app-active-switch"),
|
||||
).getByRole("checkbox");
|
||||
const removeButton = screen.getByTestId("app-remove-button");
|
||||
|
||||
// Assert
|
||||
expect(activeSwitch).not.toBeChecked();
|
||||
|
||||
// Act
|
||||
await user.click(activeSwitch);
|
||||
await user.click(removeButton);
|
||||
|
||||
// Assert
|
||||
expect(activateApp).toHaveBeenCalledWith(inactiveApp.id);
|
||||
expect(activateApp).toHaveBeenCalledTimes(1);
|
||||
expect(deactivateApp).not.toHaveBeenCalled();
|
||||
expect(removeApp).toHaveBeenCalledWith(inactiveApp.id);
|
||||
expect(removeApp).toHaveBeenCalledTimes(1);
|
||||
expect(openAppSettings).toHaveBeenCalledWith(activeApp.id);
|
||||
expect(openAppSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,11 +2,12 @@ import { TableButtonWrapper } from "@dashboard/components/TableButtonWrapper/Tab
|
|||
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { useAppListContext } from "@dashboard/new-apps/context";
|
||||
import { appsMessages } from "@dashboard/new-apps/messages";
|
||||
import { InstalledApp } from "@dashboard/new-apps/types";
|
||||
import { AppUrls } from "@dashboard/new-apps/urls";
|
||||
import { isAppInTunnel } from "@dashboard/new-apps/utils";
|
||||
import { Switch, TableCell, Typography } from "@material-ui/core";
|
||||
import { DeleteIcon, IconButton, Pill } from "@saleor/macaw-ui";
|
||||
import { TableCell, Typography } from "@material-ui/core";
|
||||
import { IconButton, Pill, SettingsIcon } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
@ -16,27 +17,18 @@ import { AppPermissions } from "../AppPermissions";
|
|||
import { messages } from "./messages";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
export const InstalledAppListRow: React.FC<InstalledApp> = ({
|
||||
app,
|
||||
isExternal,
|
||||
}) => {
|
||||
export const InstalledAppListRow: React.FC<InstalledApp> = props => {
|
||||
const { app, isExternal, logo } = props;
|
||||
const intl = useIntl();
|
||||
const classes = useStyles();
|
||||
const { activateApp, deactivateApp, removeApp } = useAppListContext();
|
||||
|
||||
const handleToggleActive = () => {
|
||||
if (app.isActive) {
|
||||
deactivateApp(app.id);
|
||||
} else {
|
||||
activateApp(app.id);
|
||||
}
|
||||
};
|
||||
const classes = useStyles(props);
|
||||
const { openAppSettings } = useAppListContext();
|
||||
|
||||
return (
|
||||
<TableRowLink className={classes.row} href={AppUrls.resolveAppUrl(app.id)}>
|
||||
<TableCellAvatar
|
||||
initials={app.name?.[0]?.toUpperCase()}
|
||||
thumbnail={undefined}
|
||||
thumbnail={logo?.source || undefined}
|
||||
avatarProps={classes.logo}
|
||||
className={clsx(classes.col, classes.colLogo)}
|
||||
>
|
||||
<div className={classes.mainContent}>
|
||||
|
@ -50,7 +42,7 @@ export const InstalledAppListRow: React.FC<InstalledApp> = ({
|
|||
<Pill
|
||||
color="warning"
|
||||
className={classes.externalAppLabel}
|
||||
label={intl.formatMessage(messages.externalApp)}
|
||||
label={intl.formatMessage(appsMessages.externalApp)}
|
||||
data-test-id="app-external-label"
|
||||
/>
|
||||
)}
|
||||
|
@ -70,22 +62,15 @@ export const InstalledAppListRow: React.FC<InstalledApp> = ({
|
|||
{`(${intl.formatMessage(messages.tunnelDevelopment)})`}
|
||||
</Typography>
|
||||
) : null}
|
||||
<TableButtonWrapper>
|
||||
<Switch
|
||||
checked={!!app.isActive}
|
||||
onChange={handleToggleActive}
|
||||
data-test-id="app-active-switch"
|
||||
/>
|
||||
</TableButtonWrapper>
|
||||
<AppPermissions permissions={app.permissions} />
|
||||
<TableButtonWrapper>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={() => removeApp(app.id)}
|
||||
data-test-id="app-remove-button"
|
||||
onClick={() => openAppSettings(app.id)}
|
||||
data-test-id="app-settings-button"
|
||||
>
|
||||
<DeleteIcon />
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</TableButtonWrapper>
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
externalApp: {
|
||||
id: "nA2g2e",
|
||||
defaultMessage: "External app",
|
||||
description: "label",
|
||||
},
|
||||
tunnelDevelopment: {
|
||||
id: "RjT+Vo",
|
||||
defaultMessage: "Tunnel - development",
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import { InstalledApp } from "@dashboard/new-apps/types";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles(
|
||||
export const useStyles = makeStyles<InstalledApp>(
|
||||
theme => ({
|
||||
mainContent: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
logo: props => ({
|
||||
backgroundColor: props.logo?.color || theme.palette.grey[300],
|
||||
color: props.logo?.color ? "#fff" : theme.palette.grey[600],
|
||||
padding: theme.spacing(1.2),
|
||||
borderRadius: theme.spacing(1),
|
||||
borderWidth: 0,
|
||||
}),
|
||||
name: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
import * as context from "@dashboard/new-apps/context";
|
||||
import {
|
||||
failedAppInProgress,
|
||||
pendingAppInProgress,
|
||||
} from "@dashboard/new-apps/fixtures";
|
||||
import Wrapper from "@test/wrapper";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
|
||||
import NotInstalledAppListRow from "./NotInstalledAppListRow";
|
||||
|
||||
jest.mock("@dashboard/new-apps/context", () => ({
|
||||
useAppListContext: jest.fn(() => ({
|
||||
openAppSettings: jest.fn(),
|
||||
removeAppInstallation: jest.fn(),
|
||||
retryAppInstallation: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("Apps NotInstalledAppListRow", () => {
|
||||
it("displays app installation details when failed installation data passed", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<NotInstalledAppListRow
|
||||
appInstallation={failedAppInProgress}
|
||||
isExternal={false}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const name = screen.queryByText(failedAppInProgress.appName);
|
||||
const manifestDomain = screen.queryByText(
|
||||
new URL(failedAppInProgress.manifestUrl as string).host,
|
||||
);
|
||||
const pendingLabel = screen.queryByTestId("app-pending-label");
|
||||
const failedLabel = screen.queryByTestId("app-failed-label");
|
||||
|
||||
// Assert
|
||||
expect(name).toBeTruthy();
|
||||
expect(manifestDomain).toBeTruthy();
|
||||
expect(pendingLabel).toBeFalsy();
|
||||
expect(failedLabel).toBeTruthy();
|
||||
});
|
||||
|
||||
it("displays app installation details when pending installation data passed", () => {
|
||||
// Arrange
|
||||
render(
|
||||
<Wrapper>
|
||||
<NotInstalledAppListRow
|
||||
appInstallation={pendingAppInProgress}
|
||||
isExternal={false}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const name = screen.queryByText(pendingAppInProgress.appName);
|
||||
const manifestDomain = screen.queryByText(
|
||||
new URL(pendingAppInProgress.manifestUrl as string).host,
|
||||
);
|
||||
const pendingLabel = screen.queryByTestId("app-pending-label");
|
||||
const failedLabel = screen.queryByTestId("app-failed-label");
|
||||
|
||||
// Assert
|
||||
expect(name).toBeTruthy();
|
||||
expect(manifestDomain).toBeTruthy();
|
||||
expect(pendingLabel).toBeTruthy();
|
||||
expect(failedLabel).toBeFalsy();
|
||||
});
|
||||
|
||||
it("calls handlers when app installation data passed and buttons clicked", async () => {
|
||||
// Arrange
|
||||
const openAppSettings = jest.fn();
|
||||
const removeAppInstallation = jest.fn();
|
||||
const retryAppInstallation = jest.fn();
|
||||
jest.spyOn(context, "useAppListContext").mockImplementation(() => ({
|
||||
openAppSettings,
|
||||
removeAppInstallation,
|
||||
retryAppInstallation,
|
||||
}));
|
||||
render(
|
||||
<Wrapper>
|
||||
<NotInstalledAppListRow
|
||||
appInstallation={failedAppInProgress}
|
||||
isExternal={false}
|
||||
/>
|
||||
</Wrapper>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const retryButton = screen.getByTestId("app-installation-retry-button");
|
||||
const removeButton = screen.getByTestId("app-installation-remove-button");
|
||||
|
||||
// Act
|
||||
await user.click(retryButton);
|
||||
await user.click(removeButton);
|
||||
|
||||
// Assert
|
||||
expect(retryAppInstallation).toHaveBeenCalledWith(failedAppInProgress.id);
|
||||
expect(retryAppInstallation).toHaveBeenCalledTimes(1);
|
||||
expect(removeAppInstallation).toHaveBeenCalledWith(failedAppInProgress.id);
|
||||
expect(removeAppInstallation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
import TableButtonWrapper from "@dashboard/components/TableButtonWrapper/TableButtonWrapper";
|
||||
import TableCellAvatar from "@dashboard/components/TableCellAvatar";
|
||||
import TableRowLink from "@dashboard/components/TableRowLink";
|
||||
import { JobStatusEnum } from "@dashboard/graphql";
|
||||
import { buttonMessages } from "@dashboard/intl";
|
||||
import { useAppListContext } from "@dashboard/new-apps/context";
|
||||
import {
|
||||
appInstallationStatusMessages,
|
||||
appsMessages,
|
||||
} from "@dashboard/new-apps/messages";
|
||||
import { AppInstallation } from "@dashboard/new-apps/types";
|
||||
import { CircularProgress, TableCell, Typography } from "@material-ui/core";
|
||||
import {
|
||||
Button,
|
||||
DeleteIcon,
|
||||
IconButton,
|
||||
Indicator,
|
||||
Pill,
|
||||
Tooltip,
|
||||
TooltipMountWrapper,
|
||||
} from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import AppManifestTableDisplay from "../AppManifestTableDisplay";
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
export const NotInstalledAppListRow: React.FC<AppInstallation> = props => {
|
||||
const { appInstallation, isExternal, logo } = props;
|
||||
const intl = useIntl();
|
||||
const classes = useStyles(props);
|
||||
const { retryAppInstallation, removeAppInstallation } = useAppListContext();
|
||||
|
||||
return (
|
||||
<TableRowLink>
|
||||
<TableCellAvatar
|
||||
initials={appInstallation.appName?.[0]?.toUpperCase()}
|
||||
thumbnail={logo?.source || undefined}
|
||||
avatarProps={classes.logo}
|
||||
className={clsx(classes.col, classes.colLogo)}
|
||||
>
|
||||
<div className={classes.mainContent}>
|
||||
<Typography variant="body1" className={classes.name}>
|
||||
{appInstallation.appName}
|
||||
</Typography>
|
||||
{isExternal && (
|
||||
<Pill
|
||||
color="warning"
|
||||
className={classes.externalAppLabel}
|
||||
label={intl.formatMessage(appsMessages.externalApp)}
|
||||
data-test-id="app-external-label"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{appInstallation.manifestUrl && (
|
||||
<AppManifestTableDisplay manifestUrl={appInstallation.manifestUrl} />
|
||||
)}
|
||||
</TableCellAvatar>
|
||||
<TableCell className={clsx(classes.col, classes.colActions)}>
|
||||
<div className={classes.actions}>
|
||||
{appInstallation?.status === JobStatusEnum.PENDING && (
|
||||
<>
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={classes.pending}
|
||||
data-test-id="app-pending-label"
|
||||
>
|
||||
{intl.formatMessage(appInstallationStatusMessages.pending)}
|
||||
</Typography>
|
||||
<div className={classes.colSpinner}>
|
||||
<CircularProgress size={20} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{appInstallation?.status === JobStatusEnum.FAILED && (
|
||||
<>
|
||||
<Typography
|
||||
variant="body2"
|
||||
className={classes.failed}
|
||||
data-test-id="app-failed-label"
|
||||
>
|
||||
<FormattedMessage {...appInstallationStatusMessages.failed} />
|
||||
<Tooltip title={appInstallation.message} variant="error">
|
||||
<TooltipMountWrapper>
|
||||
<Indicator icon="error" />
|
||||
</TooltipMountWrapper>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
<TableButtonWrapper>
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={() => retryAppInstallation(appInstallation.id)}
|
||||
data-test-id="app-installation-retry-button"
|
||||
>
|
||||
<FormattedMessage {...buttonMessages.retry} />
|
||||
</Button>
|
||||
</TableButtonWrapper>
|
||||
<TableButtonWrapper>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
color="primary"
|
||||
onClick={() => removeAppInstallation(appInstallation.id)}
|
||||
data-test-id="app-installation-remove-button"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</TableButtonWrapper>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRowLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotInstalledAppListRow;
|
2
src/new-apps/components/NotInstalledAppListRow/index.ts
Normal file
2
src/new-apps/components/NotInstalledAppListRow/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./NotInstalledAppListRow";
|
||||
export { default } from "./NotInstalledAppListRow";
|
|
@ -0,0 +1,9 @@
|
|||
import { defineMessages } from "react-intl";
|
||||
|
||||
export const messages = defineMessages({
|
||||
tunnelDevelopment: {
|
||||
id: "RjT+Vo",
|
||||
defaultMessage: "Tunnel - development",
|
||||
description: "label",
|
||||
},
|
||||
});
|
78
src/new-apps/components/NotInstalledAppListRow/styles.ts
Normal file
78
src/new-apps/components/NotInstalledAppListRow/styles.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { AppInstallation } from "@dashboard/new-apps/types";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
|
||||
export const useStyles = makeStyles<AppInstallation>(
|
||||
theme => ({
|
||||
mainContent: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
logo: props => ({
|
||||
backgroundColor: props.logo?.color || theme.palette.grey[300],
|
||||
color: props.logo?.color ? "#fff" : theme.palette.grey[600],
|
||||
padding: theme.spacing(1.2),
|
||||
borderRadius: theme.spacing(1),
|
||||
borderWidth: 0,
|
||||
}),
|
||||
name: {
|
||||
fontWeight: 600,
|
||||
},
|
||||
tunnel: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
failed: {
|
||||
color: theme.palette.error.main,
|
||||
whiteSpace: "nowrap",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
pending: {
|
||||
color: theme.palette.saleor.main[3],
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
colSpinner: {
|
||||
"& svg": {
|
||||
textAlign: "right",
|
||||
},
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(2),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
actions: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
textAlign: "right",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
externalAppLabel: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
col: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
colLogo: {
|
||||
width: "100%",
|
||||
"&.MuiTableCell-root:first-child:not(.MuiTableCell-paddingCheckbox)": {
|
||||
paddingLeft: theme.spacing(3),
|
||||
width: "100%",
|
||||
},
|
||||
height: "auto",
|
||||
},
|
||||
colActions: {
|
||||
"&&": {
|
||||
paddingRight: theme.spacing(3),
|
||||
textAlign: "right",
|
||||
},
|
||||
height: "auto",
|
||||
},
|
||||
}),
|
||||
{ name: "InstalledAppListRow" },
|
||||
);
|
|
@ -1,10 +1,10 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { useStyles } from "./styles";
|
||||
|
||||
interface SectionHeaderProps {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
}
|
||||
|
||||
export const SectionHeader: React.FC<SectionHeaderProps> = ({ title }) => {
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import React from "react";
|
||||
|
||||
export interface AppListContextValues {
|
||||
activateApp: (appId: string) => void;
|
||||
deactivateApp: (appId: string) => void;
|
||||
removeApp: (appId: string) => void;
|
||||
removeAppInstallation: (installationId: string) => void;
|
||||
retryAppInstallation: (installationId: string) => void;
|
||||
openAppSettings: (appId: string) => void;
|
||||
}
|
||||
|
||||
export const AppListContext = React.createContext<
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AppInstallationFragment,
|
||||
AppListItemFragment,
|
||||
AppManifestFragment,
|
||||
AppsInstallationsQuery,
|
||||
AppTypeEnum,
|
||||
JobStatusEnum,
|
||||
PermissionEnum,
|
||||
|
@ -55,31 +55,37 @@ export const installedAppsList: AppListItemFragment[] = [
|
|||
inactiveApp,
|
||||
];
|
||||
|
||||
export const appsInProgress: AppsInstallationsQuery["appsInstallations"] = [
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Failed to connect to app. Try later or contact with app support.",
|
||||
status: JobStatusEnum.FAILED,
|
||||
},
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app pending",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Pending.",
|
||||
status: JobStatusEnum.PENDING,
|
||||
},
|
||||
{
|
||||
__typename: "AppInstallation",
|
||||
appName: "app success",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Success.",
|
||||
status: JobStatusEnum.SUCCESS,
|
||||
},
|
||||
export const failedAppInProgress: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
appName: "app",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Failed to connect to app. Try later or contact with app support.",
|
||||
status: JobStatusEnum.FAILED,
|
||||
};
|
||||
|
||||
export const pendingAppInProgress: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
appName: "app pending",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Pending.",
|
||||
status: JobStatusEnum.PENDING,
|
||||
};
|
||||
|
||||
export const successAppInProgress: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
appName: "app success",
|
||||
id: "QXBwSW5zdGFsbGF0aW9uOjk2",
|
||||
manifestUrl: "http://localhost:3000/manifest",
|
||||
message: "Success.",
|
||||
status: JobStatusEnum.SUCCESS,
|
||||
};
|
||||
|
||||
export const appsInProgress: AppInstallationFragment[] = [
|
||||
failedAppInProgress,
|
||||
pendingAppInProgress,
|
||||
successAppInProgress,
|
||||
];
|
||||
|
||||
export const installApp: AppManifestFragment = {
|
||||
|
|
178
src/new-apps/hooks/useActiveAppsInstallations.ts
Normal file
178
src/new-apps/hooks/useActiveAppsInstallations.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { useApolloClient } from "@apollo/client";
|
||||
import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries";
|
||||
import {
|
||||
AppInstallationFragment,
|
||||
AppsInstallationsQuery,
|
||||
JobStatusEnum,
|
||||
useAppDeleteFailedInstallationMutation,
|
||||
useAppRetryInstallMutation,
|
||||
} from "@dashboard/graphql";
|
||||
import useLocalStorage from "@dashboard/hooks/useLocalStorage";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface UseActiveAppsInstallations {
|
||||
appsInProgressData: AppsInstallationsQuery | undefined;
|
||||
appsInProgressRefetch: () => void;
|
||||
appsRefetch: () => void;
|
||||
removeInProgressAppNotify: () => void;
|
||||
installedAppNotify: (name: string) => void;
|
||||
onInstallSuccess: () => void;
|
||||
onInstallError: (installation: AppInstallationFragment) => void;
|
||||
onRemoveInProgressAppSuccess: () => void;
|
||||
}
|
||||
|
||||
function useActiveAppsInstallations({
|
||||
appsInProgressData,
|
||||
appsInProgressRefetch,
|
||||
appsRefetch,
|
||||
installedAppNotify,
|
||||
removeInProgressAppNotify,
|
||||
onInstallSuccess,
|
||||
onInstallError,
|
||||
onRemoveInProgressAppSuccess,
|
||||
}: UseActiveAppsInstallations) {
|
||||
const client = useApolloClient();
|
||||
const [activeInstallations, setActiveInstallations] = useLocalStorage<
|
||||
Array<Record<"id" | "name", string>>
|
||||
>("activeInstallations", []);
|
||||
const intervalId = useRef<null | number>(null);
|
||||
|
||||
const refetchExtensionList = () => {
|
||||
client.refetchQueries({
|
||||
include: [EXTENSION_LIST_QUERY],
|
||||
});
|
||||
};
|
||||
|
||||
const removeInstallation = (id: string) =>
|
||||
setActiveInstallations(installations =>
|
||||
installations.filter(item => item.id !== id),
|
||||
);
|
||||
|
||||
const [retryInstallApp, retryInstallAppOpts] = useAppRetryInstallMutation({
|
||||
onCompleted: data => {
|
||||
if (!data?.appRetryInstall?.errors?.length) {
|
||||
const appInstallation = data.appRetryInstall?.appInstallation;
|
||||
if (appInstallation) {
|
||||
setActiveInstallations(installations => [
|
||||
...installations,
|
||||
{
|
||||
id: appInstallation.id,
|
||||
name: appInstallation.appName,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleAppInstallRetry = (id: string) =>
|
||||
retryInstallApp({ variables: { id } });
|
||||
|
||||
const [
|
||||
deleteInProgressApp,
|
||||
deleteInProgressAppOpts,
|
||||
] = useAppDeleteFailedInstallationMutation({
|
||||
onCompleted: data => {
|
||||
if (!data?.appDeleteFailedInstallation?.errors?.length) {
|
||||
removeInProgressAppNotify();
|
||||
appsInProgressRefetch();
|
||||
onRemoveInProgressAppSuccess();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleRemoveInProgress = (id: string) =>
|
||||
deleteInProgressApp({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if there has occured by any reason untracked installation with status PENDING and add it to activeInstallations.
|
||||
*/
|
||||
useEffect(
|
||||
() =>
|
||||
appsInProgressData?.appsInstallations?.forEach(app => {
|
||||
if (app.status === JobStatusEnum.PENDING) {
|
||||
const item = activeInstallations.find(
|
||||
installation => installation.id === app.id,
|
||||
);
|
||||
if (!item) {
|
||||
setActiveInstallations(installations => [
|
||||
...installations,
|
||||
{
|
||||
id: app.id,
|
||||
name: app.appName,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
[appsInProgressData],
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch active installations to make its status in localStorage up to date.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (activeInstallations.length && !!appsInProgressData) {
|
||||
if (!intervalId.current) {
|
||||
intervalId.current = window.setInterval(() => {
|
||||
appsInProgressRefetch();
|
||||
appsRefetch();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
if (!activeInstallations.length && intervalId.current) {
|
||||
clearInterval(intervalId.current);
|
||||
intervalId.current = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalId.current) {
|
||||
clearInterval(intervalId.current);
|
||||
intervalId.current = null;
|
||||
}
|
||||
};
|
||||
}, [activeInstallations.length, appsInProgressData]);
|
||||
|
||||
/**
|
||||
* Do what is needed and call chaange handlers when installation status changes.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const appsInProgress = appsInProgressData?.appsInstallations || [];
|
||||
if (activeInstallations.length && !!appsInProgressData) {
|
||||
let newAppInstalled = false;
|
||||
activeInstallations.forEach(installation => {
|
||||
const item = appsInProgress?.find(app => app.id === installation.id);
|
||||
if (!item) {
|
||||
removeInstallation(installation.id);
|
||||
installedAppNotify(installation.name);
|
||||
appsInProgressRefetch();
|
||||
appsRefetch();
|
||||
newAppInstalled = true;
|
||||
} else if (item.status === JobStatusEnum.SUCCESS) {
|
||||
removeInstallation(installation.id);
|
||||
installedAppNotify(item.appName);
|
||||
onInstallSuccess();
|
||||
newAppInstalled = true;
|
||||
} else if (item.status === JobStatusEnum.FAILED) {
|
||||
removeInstallation(installation.id);
|
||||
onInstallError(item);
|
||||
}
|
||||
});
|
||||
if (newAppInstalled) {
|
||||
refetchExtensionList();
|
||||
}
|
||||
}
|
||||
}, [activeInstallations.length, appsInProgressData]);
|
||||
|
||||
return {
|
||||
handleAppInstallRetry,
|
||||
handleRemoveInProgress,
|
||||
retryInstallAppOpts,
|
||||
deleteInProgressAppOpts,
|
||||
};
|
||||
}
|
||||
export default useActiveAppsInstallations;
|
|
@ -21,4 +21,22 @@ export const appsMessages = defineMessages({
|
|||
defaultMessage: "App Manifest URL",
|
||||
description: "label",
|
||||
},
|
||||
externalApp: {
|
||||
id: "nA2g2e",
|
||||
defaultMessage: "External app",
|
||||
description: "label",
|
||||
},
|
||||
});
|
||||
|
||||
export const appInstallationStatusMessages = defineMessages({
|
||||
pending: {
|
||||
id: "F1VtFa",
|
||||
defaultMessage: "Installation is pending...",
|
||||
description: "information",
|
||||
},
|
||||
failed: {
|
||||
id: "26VlBZ",
|
||||
defaultMessage: "Problem occured during installation.",
|
||||
description: "information",
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
import { AppListItemFragment } from "@dashboard/graphql";
|
||||
import {
|
||||
AppInstallationFragment,
|
||||
AppListItemFragment,
|
||||
} from "@dashboard/graphql";
|
||||
|
||||
interface AppLogo {
|
||||
source: string | null;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface InstalledApp {
|
||||
app: AppListItemFragment;
|
||||
isExternal: boolean;
|
||||
logo?: AppLogo;
|
||||
}
|
||||
|
||||
export interface AppInstallation {
|
||||
appInstallation: AppInstallationFragment;
|
||||
isExternal: boolean;
|
||||
logo?: AppLogo;
|
||||
}
|
||||
|
||||
export interface AppLink {
|
||||
|
|
|
@ -4,7 +4,7 @@ import urlJoin from "url-join";
|
|||
|
||||
import { Dialog, SingleAction } from "../types";
|
||||
|
||||
export type AppListUrlDialog = "remove-app" | "app-activate" | "app-deactivate";
|
||||
export type AppListUrlDialog = "app-installation-remove";
|
||||
export type AppListUrlQueryParams = Dialog<AppListUrlDialog> & SingleAction;
|
||||
|
||||
export const AppSections = {
|
||||
|
@ -14,6 +14,7 @@ export const AppSections = {
|
|||
export const AppPaths = {
|
||||
appListPath: AppSections.appsSection,
|
||||
resolveAppPath: (id: string) => urlJoin(AppSections.appsSection, id, "app"),
|
||||
resolveAppDetailsPath: (id: string) => urlJoin(AppSections.appsSection, id),
|
||||
appInstallPath: urlJoin(AppSections.appsSection, "install"),
|
||||
};
|
||||
|
||||
|
@ -22,6 +23,10 @@ export const AppUrls = {
|
|||
AppPaths.appListPath + "?" + stringifyQs(params),
|
||||
resolveAppUrl: (id: string, params?: AppDetailsUrlQueryParams) =>
|
||||
AppPaths.resolveAppPath(encodeURIComponent(id)) + "?" + stringifyQs(params),
|
||||
resolveAppDetailsUrl: (id: string, params?: AppDetailsUrlQueryParams) =>
|
||||
AppPaths.resolveAppDetailsPath(encodeURIComponent(id)) +
|
||||
"?" +
|
||||
stringifyQs(params),
|
||||
resolveAppInstallUrl: (manifestUrl: string) =>
|
||||
`${AppPaths.appInstallPath}?manifestUrl=${manifestUrl}`,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { AppInstallationFragment, JobStatusEnum } from "@dashboard/graphql";
|
||||
import { intlMock } from "@test/intl";
|
||||
|
||||
import { appsInProgress, releasedApp } from "./fixtures";
|
||||
import { GetV2SaleorAppsResponse } from "./marketplace.types";
|
||||
import { getAppDetails } from "./utils";
|
||||
import { getAppDetails, resolveInstallationOfMarketplaceApp } from "./utils";
|
||||
|
||||
type AppDetails = ReturnType<typeof getAppDetails>;
|
||||
|
||||
describe("App utils", () => {
|
||||
describe("App utils app details", () => {
|
||||
it("should return app details when required released app data passed", () => {
|
||||
// Arrange
|
||||
const app: GetV2SaleorAppsResponse.ReleasedSaleorApp = {
|
||||
|
@ -28,18 +30,24 @@ describe("App utils", () => {
|
|||
};
|
||||
|
||||
// Act
|
||||
const details = getAppDetails(
|
||||
intlMock,
|
||||
const details = getAppDetails({
|
||||
intl: intlMock,
|
||||
app,
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
);
|
||||
appInstallation: undefined,
|
||||
navigateToAppInstallPage: () => undefined,
|
||||
navigateToVercelDeploymentPage: () => undefined,
|
||||
removeAppInstallation: () => undefined,
|
||||
retryAppInstallation: () => undefined,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const expectedDetails: AppDetails = {
|
||||
releaseDate: undefined,
|
||||
installHandler: expect.any(Function),
|
||||
vercelDeployHandler: expect.any(Function),
|
||||
installationPending: undefined,
|
||||
removeInstallHandler: undefined,
|
||||
retryInstallHandler: undefined,
|
||||
links: [
|
||||
{
|
||||
name: expect.any(String),
|
||||
|
@ -76,7 +84,15 @@ describe("App utils", () => {
|
|||
};
|
||||
|
||||
// Act
|
||||
const details = getAppDetails(intlMock, app);
|
||||
const details = getAppDetails({
|
||||
intl: intlMock,
|
||||
app,
|
||||
appInstallation: undefined,
|
||||
navigateToAppInstallPage: () => undefined,
|
||||
navigateToVercelDeploymentPage: () => undefined,
|
||||
removeAppInstallation: () => undefined,
|
||||
retryAppInstallation: () => undefined,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const expectedDetails: AppDetails = {
|
||||
|
@ -84,7 +100,169 @@ describe("App utils", () => {
|
|||
installHandler: undefined,
|
||||
vercelDeployHandler: undefined,
|
||||
links: [],
|
||||
installationPending: undefined,
|
||||
removeInstallHandler: undefined,
|
||||
retryInstallHandler: undefined,
|
||||
};
|
||||
expect(details).toEqual(expectedDetails);
|
||||
});
|
||||
|
||||
it("should return app details when required app pending installation data passed", () => {
|
||||
// Arrange
|
||||
const app: GetV2SaleorAppsResponse.ReleasedSaleorApp = {
|
||||
name: {
|
||||
en: "Test app",
|
||||
},
|
||||
description: {
|
||||
en: "Test app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.example.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
integrations: [],
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
privacyUrl: "https://www.example.com/privacy",
|
||||
supportUrl: "https://www.example.com/support",
|
||||
repositoryUrl: "https://www.example.com/repository",
|
||||
vercelDeploymentUrl: "https://www.example.com/deployment",
|
||||
};
|
||||
const appInstallation: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
id: "test-installation-id",
|
||||
appName: "Test app",
|
||||
status: JobStatusEnum.PENDING,
|
||||
message: "Test message",
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
};
|
||||
|
||||
// Act
|
||||
const details = getAppDetails({
|
||||
intl: intlMock,
|
||||
app,
|
||||
appInstallation,
|
||||
navigateToAppInstallPage: () => undefined,
|
||||
navigateToVercelDeploymentPage: () => undefined,
|
||||
removeAppInstallation: () => undefined,
|
||||
retryAppInstallation: () => undefined,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const expectedDetails: AppDetails = {
|
||||
releaseDate: undefined,
|
||||
installHandler: undefined,
|
||||
vercelDeployHandler: undefined,
|
||||
links: [
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/repository",
|
||||
},
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/support",
|
||||
},
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/privacy",
|
||||
},
|
||||
],
|
||||
installationPending: true,
|
||||
removeInstallHandler: undefined,
|
||||
retryInstallHandler: undefined,
|
||||
};
|
||||
expect(details).toEqual(expectedDetails);
|
||||
});
|
||||
|
||||
it("should return app details when required app failed installation data passed", () => {
|
||||
// Arrange
|
||||
const app: GetV2SaleorAppsResponse.ReleasedSaleorApp = {
|
||||
name: {
|
||||
en: "Test app",
|
||||
},
|
||||
description: {
|
||||
en: "Test app description",
|
||||
},
|
||||
logo: {
|
||||
source: "https://www.example.com/logo",
|
||||
color: "#000000",
|
||||
},
|
||||
integrations: [],
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
privacyUrl: "https://www.example.com/privacy",
|
||||
supportUrl: "https://www.example.com/support",
|
||||
repositoryUrl: "https://www.example.com/repository",
|
||||
vercelDeploymentUrl: "https://www.example.com/deployment",
|
||||
};
|
||||
const appInstallation: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
id: "test-installation-id",
|
||||
appName: "Test app",
|
||||
status: JobStatusEnum.FAILED,
|
||||
message: "Test message",
|
||||
manifestUrl: "https://www.example.com/manifest",
|
||||
};
|
||||
|
||||
// Act
|
||||
const details = getAppDetails({
|
||||
intl: intlMock,
|
||||
app,
|
||||
appInstallation,
|
||||
navigateToAppInstallPage: () => undefined,
|
||||
navigateToVercelDeploymentPage: () => undefined,
|
||||
removeAppInstallation: () => undefined,
|
||||
retryAppInstallation: () => undefined,
|
||||
});
|
||||
|
||||
// Assert
|
||||
const expectedDetails: AppDetails = {
|
||||
releaseDate: undefined,
|
||||
installHandler: undefined,
|
||||
vercelDeployHandler: undefined,
|
||||
links: [
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/repository",
|
||||
},
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/support",
|
||||
},
|
||||
{
|
||||
name: expect.any(String),
|
||||
url: "https://www.example.com/privacy",
|
||||
},
|
||||
],
|
||||
installationPending: false,
|
||||
removeInstallHandler: expect.any(Function),
|
||||
retryInstallHandler: expect.any(Function),
|
||||
};
|
||||
expect(details).toEqual(expectedDetails);
|
||||
});
|
||||
|
||||
describe("App utils resolve apps installations", () => {
|
||||
it("should return app installation that has manifest related to passed app details when app installation list and app details are passed", () => {
|
||||
// Arrange
|
||||
const releasedAppInstallation: AppInstallationFragment = {
|
||||
__typename: "AppInstallation",
|
||||
id: "test-installation-id",
|
||||
appName: releasedApp.name.en,
|
||||
status: JobStatusEnum.PENDING,
|
||||
message: "Test message",
|
||||
manifestUrl: releasedApp.manifestUrl,
|
||||
};
|
||||
const appInstallationList: AppInstallationFragment[] = [
|
||||
releasedAppInstallation,
|
||||
...appsInProgress,
|
||||
];
|
||||
|
||||
// Act
|
||||
const installation = resolveInstallationOfMarketplaceApp(
|
||||
releasedApp,
|
||||
appInstallationList,
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(installation).toEqual(releasedAppInstallation);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getAppsConfig } from "@dashboard/config";
|
||||
import { AppInstallationFragment, JobStatusEnum } from "@dashboard/graphql";
|
||||
import { IntlShape } from "react-intl";
|
||||
|
||||
import { GetV2SaleorAppsResponse } from "./marketplace.types";
|
||||
|
@ -22,6 +23,27 @@ const getComingSoonMarketplaceApps = (
|
|||
"releaseDate" in app,
|
||||
) as GetV2SaleorAppsResponse.ComingSoonSaleorApp[] | undefined;
|
||||
|
||||
const getAppManifestUrl = (
|
||||
marketplaceApp: GetV2SaleorAppsResponse.SaleorApp,
|
||||
) => {
|
||||
if ("manifestUrl" in marketplaceApp) {
|
||||
return marketplaceApp.manifestUrl;
|
||||
}
|
||||
};
|
||||
|
||||
export const resolveInstallationOfMarketplaceApp = (
|
||||
marketplaceApp: GetV2SaleorAppsResponse.SaleorApp,
|
||||
appInstallations?: AppInstallationFragment[],
|
||||
) => {
|
||||
const manifestUrl = getAppManifestUrl(marketplaceApp);
|
||||
|
||||
if (manifestUrl) {
|
||||
return appInstallations?.find(
|
||||
appInstallation => appInstallation.manifestUrl === manifestUrl,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getMarketplaceAppsLists = (
|
||||
isMarketplaceAvailable: boolean,
|
||||
marketplaceAppList?: GetV2SaleorAppsResponse.SaleorApp[],
|
||||
|
@ -66,12 +88,25 @@ const prepareAppLinks = (
|
|||
},
|
||||
];
|
||||
|
||||
export const getAppDetails = (
|
||||
intl: IntlShape,
|
||||
app: GetV2SaleorAppsResponse.SaleorApp,
|
||||
navigateToAppInstallPage?: (url: string) => void,
|
||||
navigateToVercelDeploymentPage?: (url?: string) => void,
|
||||
) => {
|
||||
interface GetAppDetailsOpts {
|
||||
intl: IntlShape;
|
||||
app: GetV2SaleorAppsResponse.SaleorApp;
|
||||
appInstallation?: AppInstallationFragment;
|
||||
navigateToAppInstallPage?: (url: string) => void;
|
||||
navigateToVercelDeploymentPage?: (url?: string) => void;
|
||||
retryAppInstallation: (installationId: string) => void;
|
||||
removeAppInstallation: (installationId: string) => void;
|
||||
}
|
||||
|
||||
export const getAppDetails = ({
|
||||
intl,
|
||||
app,
|
||||
appInstallation,
|
||||
navigateToAppInstallPage,
|
||||
navigateToVercelDeploymentPage,
|
||||
retryAppInstallation,
|
||||
removeAppInstallation,
|
||||
}: GetAppDetailsOpts) => {
|
||||
const isAppComingSoon =
|
||||
!("manifestUrl" in app) &&
|
||||
!("vercelDeploymentUrl" in app) &&
|
||||
|
@ -79,16 +114,34 @@ export const getAppDetails = (
|
|||
const isAppInstallable = "manifestUrl" in app && !!navigateToAppInstallPage;
|
||||
const isAppVercelDeployable =
|
||||
"vercelDeploymentUrl" in app && !!navigateToVercelDeploymentPage;
|
||||
const installationPending =
|
||||
appInstallation && appInstallation.status === JobStatusEnum.PENDING;
|
||||
|
||||
return {
|
||||
releaseDate: isAppComingSoon ? app.releaseDate : undefined,
|
||||
installHandler: isAppInstallable
|
||||
? () => navigateToAppInstallPage(app.manifestUrl)
|
||||
: undefined,
|
||||
releaseDate:
|
||||
!appInstallation && isAppComingSoon ? app.releaseDate : undefined,
|
||||
installHandler:
|
||||
!appInstallation && isAppInstallable
|
||||
? () => navigateToAppInstallPage(app.manifestUrl)
|
||||
: undefined,
|
||||
vercelDeployHandler:
|
||||
isAppVercelDeployable && !!app.vercelDeploymentUrl
|
||||
!appInstallation && isAppVercelDeployable && !!app.vercelDeploymentUrl
|
||||
? () => navigateToVercelDeploymentPage(app.vercelDeploymentUrl)
|
||||
: undefined,
|
||||
installationPending,
|
||||
retryInstallHandler:
|
||||
appInstallation && !installationPending
|
||||
? () => retryAppInstallation(appInstallation.id)
|
||||
: undefined,
|
||||
removeInstallHandler:
|
||||
appInstallation && !installationPending
|
||||
? () => removeAppInstallation(appInstallation.id)
|
||||
: undefined,
|
||||
links: isAppComingSoon ? [] : prepareAppLinks(intl, app),
|
||||
};
|
||||
};
|
||||
|
||||
export const getAppInProgressName = (
|
||||
id: string,
|
||||
collection?: AppInstallationFragment[],
|
||||
) => collection?.find(app => app.id === id)?.appName || id;
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import { useApolloClient } from "@apollo/client";
|
||||
import { EXTENSION_LIST_QUERY } from "@dashboard/apps/queries";
|
||||
import { getAppsConfig } from "@dashboard/config";
|
||||
import {
|
||||
AppInstallationFragment,
|
||||
AppSortField,
|
||||
AppTypeEnum,
|
||||
OrderDirection,
|
||||
useAppActivateMutation,
|
||||
useAppDeactivateMutation,
|
||||
useAppDeleteMutation,
|
||||
useAppsInstallationsQuery,
|
||||
useAppsListQuery,
|
||||
} from "@dashboard/graphql";
|
||||
import useListSettings from "@dashboard/hooks/useListSettings";
|
||||
|
@ -17,22 +14,23 @@ import useLocalPaginator, {
|
|||
import useNavigator from "@dashboard/hooks/useNavigator";
|
||||
import useNotifier from "@dashboard/hooks/useNotifier";
|
||||
import { PaginatorContext } from "@dashboard/hooks/usePaginator";
|
||||
import { findById } from "@dashboard/misc";
|
||||
import AppActivateDialog from "@dashboard/new-apps/components/AppActivateDialog";
|
||||
import AppDeactivateDialog from "@dashboard/new-apps/components/AppDeactivateDialog";
|
||||
import AppDeleteDialog from "@dashboard/new-apps/components/AppDeleteDialog";
|
||||
import AppInProgressDeleteDialog from "@dashboard/new-apps/components/AppInProgressDeleteDialog";
|
||||
import AppListPage from "@dashboard/new-apps/components/AppListPage/AppListPage";
|
||||
import {
|
||||
AppListContext,
|
||||
AppListContextValues,
|
||||
} from "@dashboard/new-apps/context";
|
||||
import useActiveAppsInstallations from "@dashboard/new-apps/hooks/useActiveAppsInstallations";
|
||||
import useMarketplaceApps from "@dashboard/new-apps/hooks/useMarketplaceApps";
|
||||
import {
|
||||
AppListUrlDialog,
|
||||
AppListUrlQueryParams,
|
||||
AppUrls,
|
||||
} from "@dashboard/new-apps/urls";
|
||||
import { getMarketplaceAppsLists } from "@dashboard/new-apps/utils";
|
||||
import {
|
||||
getAppInProgressName,
|
||||
getMarketplaceAppsLists,
|
||||
} from "@dashboard/new-apps/utils";
|
||||
import { ListViews } from "@dashboard/types";
|
||||
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
|
||||
import { mapEdgesToItems } from "@dashboard/utils/maps";
|
||||
|
@ -49,7 +47,7 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
|
|||
const navigate = useNavigator();
|
||||
const notify = useNotifier();
|
||||
const intl = useIntl();
|
||||
const client = useApolloClient();
|
||||
|
||||
const [openModal, closeModal] = createDialogActionHandlers<
|
||||
AppListUrlDialog,
|
||||
AppListUrlQueryParams
|
||||
|
@ -69,7 +67,11 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
|
|||
);
|
||||
const paginate = useLocalPaginator(setPaginationState);
|
||||
|
||||
const { data: installedAppsData, loading, refetch } = useAppsListQuery({
|
||||
const {
|
||||
data: installedAppsData,
|
||||
loading,
|
||||
refetch: appsRefetch,
|
||||
} = useAppsListQuery({
|
||||
displayLoader: true,
|
||||
variables: {
|
||||
...paginationState,
|
||||
|
@ -84,74 +86,63 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
|
|||
paginationState,
|
||||
);
|
||||
|
||||
const refetchExtensionList = () => {
|
||||
client.refetchQueries({
|
||||
include: [EXTENSION_LIST_QUERY],
|
||||
const {
|
||||
data: appsInProgressData,
|
||||
refetch: appsInProgressRefetch,
|
||||
} = useAppsInstallationsQuery({
|
||||
displayLoader: false,
|
||||
});
|
||||
|
||||
const installedAppNotify = (name: string) => {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(messages.appReadyToUse, { name }),
|
||||
title: intl.formatMessage(messages.appInstalled),
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveConfirm = () =>
|
||||
deleteApp({
|
||||
variables: {
|
||||
id: params.id || "",
|
||||
},
|
||||
});
|
||||
|
||||
const removeAppNotify = () => {
|
||||
const removeInProgressAppNotify = () => {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(messages.appRemoved),
|
||||
});
|
||||
};
|
||||
|
||||
const handleActivateAppConfirm = () =>
|
||||
activateApp({ variables: { id: params.id || "" } });
|
||||
const onAppInstallError = (item: AppInstallationFragment) => {
|
||||
notify({
|
||||
status: "error",
|
||||
text: item.message,
|
||||
title: intl.formatMessage(messages.appCouldntInstall, {
|
||||
name: item.appName,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeactivateAppConfirm = () =>
|
||||
deactivateApp({ variables: { id: params.id || "" } });
|
||||
|
||||
const [deleteApp, deleteAppOpts] = useAppDeleteMutation({
|
||||
onCompleted: data => {
|
||||
if (!data?.appDelete?.errors?.length) {
|
||||
refetch();
|
||||
closeModal();
|
||||
refetchExtensionList();
|
||||
removeAppNotify();
|
||||
}
|
||||
},
|
||||
});
|
||||
const [activateApp, activateAppResult] = useAppActivateMutation({
|
||||
onCompleted: data => {
|
||||
if (!data?.appActivate?.errors?.length) {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(messages.appActivated),
|
||||
});
|
||||
refetch();
|
||||
closeModal();
|
||||
}
|
||||
},
|
||||
});
|
||||
const [deactivateApp, deactivateAppResult] = useAppDeactivateMutation({
|
||||
onCompleted: data => {
|
||||
if (!data?.appDeactivate?.errors?.length) {
|
||||
notify({
|
||||
status: "success",
|
||||
text: intl.formatMessage(messages.appDeactivated),
|
||||
});
|
||||
refetch();
|
||||
closeModal();
|
||||
}
|
||||
const {
|
||||
handleAppInstallRetry,
|
||||
handleRemoveInProgress,
|
||||
deleteInProgressAppOpts,
|
||||
} = useActiveAppsInstallations({
|
||||
appsInProgressData,
|
||||
appsInProgressRefetch,
|
||||
appsRefetch,
|
||||
installedAppNotify,
|
||||
removeInProgressAppNotify,
|
||||
onInstallSuccess: () => {
|
||||
appsRefetch();
|
||||
appsInProgressRefetch();
|
||||
},
|
||||
onInstallError: onAppInstallError,
|
||||
onRemoveInProgressAppSuccess: closeModal,
|
||||
});
|
||||
|
||||
const context: AppListContextValues = React.useMemo(
|
||||
() => ({
|
||||
activateApp: id => openModal("app-activate", { id }),
|
||||
deactivateApp: id => openModal("app-deactivate", { id }),
|
||||
removeApp: id => openModal("remove-app", { id }),
|
||||
retryAppInstallation: handleAppInstallRetry,
|
||||
removeAppInstallation: id => openModal("app-installation-remove", { id }),
|
||||
openAppSettings: id => navigate(AppUrls.resolveAppDetailsUrl(id)),
|
||||
}),
|
||||
[activateApp, deactivateApp, deleteApp],
|
||||
[navigate, openModal],
|
||||
);
|
||||
|
||||
const { data: marketplaceAppList, error } = useMarketplaceApps(
|
||||
|
@ -165,35 +156,24 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
|
|||
!!AppsConfig.marketplaceApiUri,
|
||||
marketplaceAppList,
|
||||
);
|
||||
const appsInstallations = appsInProgressData?.appsInstallations;
|
||||
const installedApps = mapEdgesToItems(installedAppsData?.apps);
|
||||
const currentAppName = params.id && findById(params.id, installedApps)?.name;
|
||||
|
||||
return (
|
||||
<AppListContext.Provider value={context}>
|
||||
<PaginatorContext.Provider value={{ ...pageInfo, ...paginationValues }}>
|
||||
<AppDeleteDialog
|
||||
confirmButtonState={deleteAppOpts.status}
|
||||
name={currentAppName}
|
||||
<AppInProgressDeleteDialog
|
||||
confirmButtonState={deleteInProgressAppOpts.status}
|
||||
name={getAppInProgressName(
|
||||
params.id || "",
|
||||
appsInProgressData?.appsInstallations,
|
||||
)}
|
||||
onClose={closeModal}
|
||||
onConfirm={handleRemoveConfirm}
|
||||
type="EXTERNAL"
|
||||
open={params.action === "remove-app" && !!params.id}
|
||||
/>
|
||||
<AppActivateDialog
|
||||
confirmButtonState={activateAppResult.status}
|
||||
name={currentAppName}
|
||||
onClose={closeModal}
|
||||
onConfirm={handleActivateAppConfirm}
|
||||
open={params.action === "app-activate" && !!params.id}
|
||||
/>
|
||||
<AppDeactivateDialog
|
||||
confirmButtonState={deactivateAppResult.status}
|
||||
name={currentAppName}
|
||||
onClose={closeModal}
|
||||
onConfirm={handleDeactivateAppConfirm}
|
||||
open={params.action === "app-deactivate" && !!params.id}
|
||||
onConfirm={() => handleRemoveInProgress(params?.id || "")}
|
||||
open={params.action === "app-installation-remove"}
|
||||
/>
|
||||
<AppListPage
|
||||
appsInstallations={appsInstallations}
|
||||
installedApps={installedApps}
|
||||
installableMarketplaceApps={installableMarketplaceApps}
|
||||
comingSoonMarketplaceApps={comingSoonMarketplaceApps}
|
||||
|
|
|
@ -6,14 +6,19 @@ export const messages = defineMessages({
|
|||
defaultMessage: "App successfully removed",
|
||||
description: "app has been removed",
|
||||
},
|
||||
appActivated: {
|
||||
id: "D/+84n",
|
||||
defaultMessage: "App activated",
|
||||
description: "snackbar text",
|
||||
appInstalled: {
|
||||
id: "0fM/pV",
|
||||
defaultMessage: "App installed",
|
||||
description: "message title",
|
||||
},
|
||||
appDeactivated: {
|
||||
id: "USO8PB",
|
||||
defaultMessage: "App deactivated",
|
||||
description: "snackbar text",
|
||||
appReadyToUse: {
|
||||
id: "ZprV2g",
|
||||
defaultMessage: "{name} is ready to be used",
|
||||
description: "app has been installed",
|
||||
},
|
||||
appCouldntInstall: {
|
||||
id: "5t/4um",
|
||||
defaultMessage: "Couldn’t Install {name}",
|
||||
description: "message title",
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue