Improve apps contextual actions (#2943)

This commit is contained in:
Dawid 2023-01-12 09:19:13 +01:00 committed by GitHub
parent ddf4adda9d
commit eb01b84412
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 128 additions and 209 deletions

View file

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" height="20px" width="20px" viewBox="0 0 13.2 14.4">
<path fill="#828282" d="M13.2,2.7v0.6c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.1,0.1-0.2,0.1H12v8.9c0,0.5-0.1,1-0.4,1.3
<path fill="currentColor" d="M13.2,2.7v0.6c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.1,0.1-0.2,0.1H12v8.9c0,0.5-0.1,1-0.4,1.3
c-0.3,0.4-0.6,0.6-1.1,0.6H2.7c-0.4,0-0.8-0.2-1.1-0.5s-0.4-0.8-0.4-1.3V3.6H0.3c-0.1,0-0.2,0-0.2-0.1C0,3.5,0,3.4,0,3.3V2.7
c0-0.1,0-0.2,0.1-0.2c0.1-0.1,0.1-0.1,0.2-0.1h2.9l0.7-1.6C4,0.6,4.1,0.4,4.4,0.2C4.6,0.1,4.9,0,5.1,0h3c0.3,0,0.5,0.1,0.7,0.2
c0.2,0.2,0.4,0.4,0.5,0.6L10,2.4h2.9c0.1,0,0.2,0,0.2,0.1C13.2,2.5,13.2,2.6,13.2,2.7z M10.8,12.5V3.6H2.4v8.9c0,0.1,0,0.3,0.1,0.4

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -11,6 +11,7 @@ const props: AppDetailsPageProps = {
navigateToApp: () => undefined,
onAppActivateOpen: () => undefined,
onAppDeactivateOpen: () => undefined,
onAppDeleteOpen: () => undefined,
};
storiesOf("Apps / App details", module)

View file

@ -9,13 +9,14 @@ import ExternalLink from "@saleor/components/ExternalLink";
import PageHeader from "@saleor/components/PageHeader";
import Skeleton from "@saleor/components/Skeleton";
import { AppQuery } from "@saleor/graphql";
import { sectionNames } from "@saleor/intl";
import { buttonMessages, sectionNames } from "@saleor/intl";
import React from "react";
import SVG from "react-inlinesvg";
import { FormattedMessage, useIntl } from "react-intl";
import ReactMarkdown from "react-markdown";
import activateIcon from "../../../../assets/images/activate-icon.svg";
import deleteIcon from "../../../../assets/images/delete.svg";
import supportIcon from "../../../../assets/images/support-icon.svg";
import { useStyles } from "../../styles";
import DeactivatedText from "../DeactivatedText";
@ -26,6 +27,7 @@ export interface AppDetailsPageProps {
navigateToApp: () => void;
onAppActivateOpen: () => void;
onAppDeactivateOpen: () => void;
onAppDeleteOpen: () => void;
}
export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
@ -34,6 +36,7 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
navigateToApp,
onAppActivateOpen,
onAppDeactivateOpen,
onAppDeleteOpen,
}) => {
const intl = useIntl();
const classes = useStyles({});
@ -80,19 +83,19 @@ export const AppDetailsPage: React.FC<AppDetailsPageProps> = ({
>
<SVG src={activateIcon} />
{data?.isActive ? (
<FormattedMessage
id="whTEcF"
defaultMessage="Deactivate"
description="link"
/>
<FormattedMessage {...buttonMessages.deactivate} />
) : (
<FormattedMessage
id="P5twxk"
defaultMessage="Activate"
description="link"
/>
<FormattedMessage {...buttonMessages.activate} />
)}
</ButtonBase>
<ButtonBase
className={classes.headerLinkContainer}
disableRipple
onClick={onAppDeleteOpen}
>
<SVG src={deleteIcon} />
<FormattedMessage {...buttonMessages.delete} />
</ButtonBase>
</div>
) : (
<Skeleton />

View file

@ -1,4 +1,3 @@
import { AppListContext } from "@saleor/apps/context";
import {
listActionsProps,
pageListProps,
@ -28,18 +27,11 @@ const props: AppsListPageProps = {
installedAppsList: appsList,
onAppInProgressRemove: () => undefined,
onAppInstallRetry: () => undefined,
onInstalledAppRemove: () => undefined,
onSettingsAppOpen: () => undefined,
};
storiesOf("Apps / Apps list", module)
.addDecorator(Decorator)
.addDecorator(story => (
<AppListContext.Provider
value={{ activateApp: () => undefined, deactivateApp: () => undefined }}
>
{story()}
</AppListContext.Provider>
))
.addDecorator(PaginatorContextDecorator)
.add("default", () => <AppsListPage {...props} />)
.add("loading", () => (

View file

@ -19,7 +19,7 @@ import InstalledApps from "../InstalledApps/InstalledApps";
export interface AppsListPageProps extends ListProps {
installedAppsList: AppListItemFragment[];
appsInProgressList?: AppsInstallationsQuery;
onInstalledAppRemove: (id: string) => void;
onSettingsAppOpen: (id: string) => void;
onAppInProgressRemove: (id: string) => void;
onAppInstallRetry: (id: string) => void;
}
@ -42,7 +42,7 @@ const useStyles = makeStyles(
const AppsListPage: React.FC<AppsListPageProps> = ({
appsInProgressList,
installedAppsList,
onInstalledAppRemove,
onSettingsAppOpen,
onAppInProgressRemove,
onAppInstallRetry,
...listProps
@ -123,7 +123,7 @@ const AppsListPage: React.FC<AppsListPageProps> = ({
description: "section header",
})}
appsList={thirdPartyApps}
onRemove={onInstalledAppRemove}
onSettingsClick={onSettingsAppOpen}
displayQuickManifestButton
{...listProps}
/>
@ -149,7 +149,7 @@ const AppsListPage: React.FC<AppsListPageProps> = ({
description: "section header",
})}
appsList={saleorApps}
onRemove={onInstalledAppRemove}
onSettingsClick={onSettingsAppOpen}
{...listProps}
/>
<div className={styles.browseMarketplaceContainer}>

View file

@ -1,13 +1,6 @@
import {
Card,
Switch,
TableBody,
TableCell,
Typography,
} from "@material-ui/core";
import { Card, TableBody, TableCell, Typography } from "@material-ui/core";
import { AppManifestTableDisplay } from "@saleor/apps/components/AppManifestTableDisplay/AppManifestTableDisplay";
import { InstallWithManifestFormButton } from "@saleor/apps/components/InstallWithManifestFormButton";
import { useAppListContext } from "@saleor/apps/context";
import { appUrl, createAppInstallUrl } from "@saleor/apps/urls";
import { isAppInTunnel } from "@saleor/apps/utils";
import CardTitle from "@saleor/components/CardTitle";
@ -16,7 +9,7 @@ import { TableButtonWrapper } from "@saleor/components/TableButtonWrapper/TableB
import TableRowLink from "@saleor/components/TableRowLink";
import { AppListItemFragment } from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
import { DeleteIcon, ResponsiveTable } from "@saleor/macaw-ui";
import { ResponsiveTable, SettingsIcon } from "@saleor/macaw-ui";
import { renderCollection } from "@saleor/misc";
import { ListProps } from "@saleor/types";
import React, { useCallback } from "react";
@ -28,20 +21,19 @@ import AppsSkeleton from "../AppsSkeleton";
export interface InstalledAppsProps extends ListProps {
appsList: AppListItemFragment[];
onRemove: (id: string) => void;
onSettingsClick: (id: string) => void;
displayQuickManifestButton?: boolean;
title: string;
}
const InstalledApps: React.FC<InstalledAppsProps> = ({
appsList,
onRemove,
onSettingsClick,
title,
displayQuickManifestButton = false,
...props
}) => {
const classes = useStyles(props);
const { activateApp, deactivateApp } = useAppListContext();
const navigate = useNavigator();
const navigateToAppInstallPage = useCallback(
@ -51,14 +43,6 @@ const InstalledApps: React.FC<InstalledAppsProps> = ({
[navigate],
);
const getHandleToggle = (app: AppListItemFragment) => () => {
if (app.isActive) {
deactivateApp(app.id);
} else {
activateApp(app.id);
}
};
return (
<Card className={classes.apps}>
<CardTitle
@ -102,20 +86,14 @@ const InstalledApps: React.FC<InstalledAppsProps> = ({
{app.manifestUrl && (
<AppManifestTableDisplay manifestUrl={app.manifestUrl} />
)}
<TableButtonWrapper>
<Switch
checked={!!app.isActive}
onChange={getHandleToggle(app)}
/>
</TableButtonWrapper>
<AppPermissions permissions={app.permissions || []} />
<TableButtonWrapper>
<IconButton
variant="secondary"
color="primary"
onClick={() => onRemove(app.id)}
onClick={() => onSettingsClick(app.id)}
>
<DeleteIcon />
<SettingsIcon />
</IconButton>
</TableButtonWrapper>
</TableCell>

View file

@ -1,21 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import React from "react";
export interface AppListContextValues {
activateApp: (appId: string) => void;
deactivateApp: (appId: string) => void;
}
export const AppListContext = React.createContext<
AppListContextValues | undefined
>(undefined);
export const useAppListContext = () => {
const context = React.useContext(AppListContext);
if (!context) {
throw new Error("useAppListContext must be used within a AppListContext");
}
return context;
};

View file

@ -6,13 +6,12 @@ import { ActiveTab, Dialog, Pagination, SingleAction } from "../types";
export const MANIFEST_ATTR = "manifestUrl";
export type AppListUrlDialog =
| "remove"
| "remove-app"
| "app-activate"
| "app-deactivate";
export type AppListUrlDialog = "app-installation-remove";
export type AppDetailsUrlDialog = "app-activate" | "app-deactivate";
export type AppDetailsUrlDialog =
| "app-activate"
| "app-deactivate"
| "app-delete";
export type AppListUrlQueryParams = ActiveTab &
Dialog<AppListUrlDialog> &

View file

@ -1,4 +1,11 @@
import { AppsInstallationsQuery } from "@saleor/graphql";
const tunnelKeywords = [".ngrok.io", ".saleor.live"];
export const isAppInTunnel = (manifestUrl: string) =>
Boolean(tunnelKeywords.find(keyword => manifestUrl.includes(keyword)));
export const getAppInProgressName = (
id: string,
collection?: AppsInstallationsQuery["appsInstallations"],
) => collection?.find(app => app.id === id)?.appName || id;

View file

@ -1,8 +1,12 @@
import { useApolloClient } from "@apollo/client";
import AppDeleteDialog from "@saleor/apps/components/AppDeleteDialog";
import { appMessages } from "@saleor/apps/messages";
import { EXTENSION_LIST_QUERY } from "@saleor/apps/queries";
import NotFoundPage from "@saleor/components/NotFoundPage";
import {
useAppActivateMutation,
useAppDeactivateMutation,
useAppDeleteMutation,
useAppQuery,
} from "@saleor/graphql";
import useNavigator from "@saleor/hooks/useNavigator";
@ -22,6 +26,7 @@ import {
appsListPath,
appUrl,
} from "../../urls";
import { messages } from "./messages";
interface AppDetailsProps {
id: string;
@ -29,6 +34,7 @@ interface AppDetailsProps {
}
export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
const client = useApolloClient();
const { data, loading, refetch } = useAppQuery({
displayLoader: true,
variables: { id },
@ -85,17 +91,38 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
},
});
const refetchExtensionList = () => {
client.refetchQueries({
include: [EXTENSION_LIST_QUERY],
});
};
const removeAppNotify = () => {
notify({
status: "success",
text: intl.formatMessage(messages.appRemoved),
});
};
const [deleteApp, deleteAppOpts] = useAppDeleteMutation({
onCompleted: data => {
if (!data?.appDelete?.errors?.length) {
refetch();
closeModal();
refetchExtensionList();
removeAppNotify();
}
},
});
const [openModal, closeModal] = createDialogActionHandlers<
AppDetailsUrlDialog,
AppDetailsUrlQueryParams
>(navigate, params => appDetailsUrl(id, params), params);
const handleActivateConfirm = () => {
activateApp(mutationOpts);
};
const handleDeactivateConfirm = () => {
deactivateApp(mutationOpts);
};
const handleActivateConfirm = () => activateApp(mutationOpts);
const handleDeactivateConfirm = () => deactivateApp(mutationOpts);
const handleRemoveConfirm = () => deleteApp(mutationOpts);
if (!appExists) {
return <NotFoundPage backHref={appsListPath} />;
@ -117,12 +144,21 @@ export const AppDetails: React.FC<AppDetailsProps> = ({ id, params }) => {
onConfirm={handleDeactivateConfirm}
open={params.action === "app-deactivate"}
/>
<AppDeleteDialog
confirmButtonState={deleteAppOpts.status}
name={data?.app?.name || ""}
onClose={closeModal}
onConfirm={handleRemoveConfirm}
type="EXTERNAL"
open={params.action === "app-delete"}
/>
<AppDetailsPage
data={data?.app || null}
loading={loading}
navigateToApp={() => navigate(appUrl(id))}
onAppActivateOpen={() => openModal("app-activate")}
onAppDeactivateOpen={() => openModal("app-deactivate")}
onAppDeleteOpen={() => openModal("app-delete")}
/>
</>
);

View file

@ -0,0 +1,9 @@
import { defineMessages } from "react-intl";
export const messages = defineMessages({
appRemoved: {
id: "uIPD1i",
defaultMessage: "App successfully removed",
description: "app has been removed",
},
});

View file

@ -1,17 +1,11 @@
import { useApolloClient } from "@apollo/client";
import AppActivateDialog from "@saleor/apps/components/AppActivateDialog";
import AppDeactivateDialog from "@saleor/apps/components/AppDeactivateDialog";
import { AppListContext, AppListContextValues } from "@saleor/apps/context";
import { getAppInProgressName } from "@saleor/apps/utils";
import {
AppsInstallationsQuery,
AppSortField,
AppTypeEnum,
JobStatusEnum,
OrderDirection,
useAppActivateMutation,
useAppDeactivateMutation,
useAppDeleteFailedInstallationMutation,
useAppDeleteMutation,
useAppRetryInstallMutation,
useAppsInstallationsQuery,
useAppsListQuery,
@ -25,28 +19,23 @@ import usePaginator, {
PageInfo,
PaginatorContext,
} from "@saleor/hooks/usePaginator";
import { findById } from "@saleor/misc";
import { ListViews } from "@saleor/types";
import createDialogActionHandlers from "@saleor/utils/handlers/dialogActionHandlers";
import { mapEdgesToItems } from "@saleor/utils/maps";
import React, { useEffect, useRef } from "react";
import { useIntl } from "react-intl";
import AppDeleteDialog from "../../components/AppDeleteDialog";
import AppInProgressDeleteDialog from "../../components/AppInProgressDeleteDialog";
import AppsListPage from "../../components/AppsListPage";
import { EXTENSION_LIST_QUERY } from "../../queries";
import {
appDetailsUrl,
AppListUrlDialog,
AppListUrlQueryParams,
appsListUrl,
} from "../../urls";
import { messages } from "./messages";
const getAppInProgressName = (
id: string,
collection?: AppsInstallationsQuery["appsInstallations"],
) => collection?.find(app => app.id === id)?.appName || id;
interface AppsListProps {
params: AppListUrlQueryParams;
}
@ -127,45 +116,11 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
}
},
});
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 [openModal, closeModal] = createDialogActionHandlers<
AppListUrlDialog,
AppListUrlQueryParams
>(navigate, appsListUrl, params);
const [deleteApp, deleteAppOpts] = useAppDeleteMutation({
onCompleted: data => {
if (!data?.appDelete?.errors?.length) {
refetch();
closeModal();
refetchExtensionList();
removeAppNotify();
}
},
});
const [
deleteInProgressApp,
deleteInProgressAppOpts,
@ -236,13 +191,6 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
},
});
const handleRemoveConfirm = () =>
deleteApp({
variables: {
id: params?.id || "",
},
});
const removeAppNotify = () => {
notify({
status: "success",
@ -250,81 +198,38 @@ export const AppsList: React.FC<AppsListProps> = ({ params }) => {
});
};
const handleActivateAppConfirm = () =>
activateApp({ variables: { id: params?.id || "" } });
const handleDeactivateAppConfirm = () =>
deactivateApp({ variables: { id: params?.id || "" } });
const onAppInstallRetry = (id: string) =>
retryInstallApp({ variables: { id } });
const context: AppListContextValues = React.useMemo(
() => ({
activateApp: id => openModal("app-activate", { id }),
deactivateApp: id => openModal("app-deactivate", { id }),
}),
[activateApp, deactivateApp],
);
const installedApps = mapEdgesToItems(data?.apps || { edges: [] }) || [];
const currentAppName = findById(params?.id || "", installedApps)?.name || "";
return (
<AppListContext.Provider value={context}>
<PaginatorContext.Provider value={paginationValues}>
<AppDeleteDialog
confirmButtonState={deleteAppOpts.status}
name={currentAppName}
onClose={closeModal}
onConfirm={handleRemoveConfirm}
type="EXTERNAL"
open={action === "remove-app"}
/>
<AppActivateDialog
confirmButtonState={activateAppResult.status}
name={currentAppName}
onClose={closeModal}
onConfirm={handleActivateAppConfirm}
open={params.action === "app-activate"}
/>
<AppDeactivateDialog
confirmButtonState={deactivateAppResult.status}
name={currentAppName}
onClose={closeModal}
onConfirm={handleDeactivateAppConfirm}
open={params.action === "app-deactivate"}
/>
<AppInProgressDeleteDialog
confirmButtonState={deleteInProgressAppOpts.status}
name={getAppInProgressName(
params.id || "",
appsInProgressData?.appsInstallations,
)}
onClose={closeModal}
onConfirm={handleRemoveInProgressConfirm}
open={action === "remove"}
/>
<AppsListPage
installedAppsList={installedApps}
appsInProgressList={appsInProgressData}
disabled={loading}
settings={settings}
onUpdateListSettings={updateListSettings}
onAppInstallRetry={onAppInstallRetry}
onInstalledAppRemove={id =>
openModal("remove-app", {
id,
})
}
onAppInProgressRemove={id =>
openModal("remove", {
id,
})
}
/>
</PaginatorContext.Provider>
</AppListContext.Provider>
<PaginatorContext.Provider value={paginationValues}>
<AppInProgressDeleteDialog
confirmButtonState={deleteInProgressAppOpts.status}
name={getAppInProgressName(
params.id || "",
appsInProgressData?.appsInstallations,
)}
onClose={closeModal}
onConfirm={handleRemoveInProgressConfirm}
open={action === "app-installation-remove"}
/>
<AppsListPage
installedAppsList={installedApps}
appsInProgressList={appsInProgressData}
disabled={loading}
settings={settings}
onUpdateListSettings={updateListSettings}
onAppInstallRetry={onAppInstallRetry}
onSettingsAppOpen={id => navigate(appDetailsUrl(id))}
onAppInProgressRemove={id =>
openModal("app-installation-remove", {
id,
})
}
/>
</PaginatorContext.Provider>
);
};

View file

@ -237,6 +237,16 @@ export const buttonMessages = defineMessages({
defaultMessage: "Assign",
description: "button",
},
activate: {
id: "+b3KCV",
defaultMessage: "Activate",
description: "button",
},
deactivate: {
id: "gygOA1",
defaultMessage: "Deactivate",
description: "button",
},
back: {
id: "0OfZJA",
defaultMessage: "Back",