Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/Sidebar/menu/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ describe("getMenuItemExtension", () => {
COLLECTION_OVERVIEW_MORE_ACTIONS: [],
CUSTOMER_DETAILS_WIDGETS: [],
ORDER_DETAILS_WIDGETS: [],
ORDER_FULFILLMENTS_WIDGETS: [],
COLLECTION_DETAILS_WIDGETS: [],
PRODUCT_DETAILS_WIDGETS: [],
VOUCHER_DETAILS_WIDGETS: [],
Expand Down Expand Up @@ -322,6 +323,7 @@ describe("getMenuItemExtension", () => {
COLLECTION_OVERVIEW_MORE_ACTIONS: [],
CUSTOMER_DETAILS_WIDGETS: [],
ORDER_DETAILS_WIDGETS: [],
ORDER_FULFILLMENTS_WIDGETS: [],
COLLECTION_DETAILS_WIDGETS: [],
PRODUCT_DETAILS_WIDGETS: [],
VOUCHER_DETAILS_WIDGETS: [],
Expand Down
81 changes: 49 additions & 32 deletions src/extensions/components/AppWidgets/AppWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { useIntl } from "react-intl";
type AppWidgetsProps = {
extensions: ExtensionWithParams[];
params: AppDetailsUrlMountQueryParams;
condensed?: boolean;
};

const hiddenStyle = { visibility: "hidden" } as const;

// TODO We will add size negotiations after render
const defaultIframeSize = 200;
const defaultIframeSizeCondensed = 24;

/**
* Renders a form and iframe, the form is automatically submitted with POST action and <iframe> content is replaced
Expand All @@ -34,12 +36,14 @@ const IframePost = ({
appId,
accessToken,
params,
condensed,
}: {
extensionUrl: string;
extensionId: string;
accessToken: string;
appId: string;
params?: AppDetailsUrlMountQueryParams;
condensed?: boolean;
}) => {
const formRef = useRef<HTMLFormElement | null>(null);
const iframeRef = useRef<HTMLFormElement | null>(null);
Expand Down Expand Up @@ -93,15 +97,19 @@ const IframePost = ({
))}
</>
</form>
<Box ref={loadingRef} width={"100%"} __height={defaultIframeSize}>
<Skeleton __height={defaultIframeSize} />
<Box
ref={loadingRef}
width={"100%"}
__height={condensed ? defaultIframeSizeCondensed : defaultIframeSize}
>
<Skeleton __height={condensed ? defaultIframeSizeCondensed : defaultIframeSize} />
</Box>
<Box
style={hiddenStyle}
ref={iframeRef}
as="iframe"
borderWidth={0}
__height={defaultIframeSize}
__height={condensed ? defaultIframeSizeCondensed : defaultIframeSize}
sandbox="allow-same-origin allow-forms allow-scripts allow-downloads"
name={`ext-frame-${extensionId}`}
width={"100%"}
Expand All @@ -110,7 +118,7 @@ const IframePost = ({
);
};

export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {
export const AppWidgets = ({ extensions, params, condensed = false }: AppWidgetsProps) => {
const navigate = useNavigator();
const themeRef = useRef<ThemeType>();
const intl = useIntl();
Expand Down Expand Up @@ -141,9 +149,11 @@ export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {

return (
<DashboardCard>
<DashboardCard.Header>
<DashboardCard.Title>Apps</DashboardCard.Title>
</DashboardCard.Header>
{!condensed && (
<DashboardCard.Header>
<DashboardCard.Title>Apps</DashboardCard.Title>
</DashboardCard.Header>
)}
<DashboardCard.Content>
{sortedByAppName.map(([appId, appWithExtensions]) => {
const logo = appWithExtensions.app.brand?.logo.default;
Expand All @@ -153,23 +163,25 @@ export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {
);

return (
<Box marginBottom={8} key={appId}>
<Box display="flex" alignItems="center" marginBottom={2}>
<AppAvatar size={6} logo={logo ? { source: logo } : undefined} marginRight={2} />
<Text
onClick={e => {
navigate(appPageUrl);

e.preventDefault();
}}
as="a"
size={3}
color="default2"
href={appPageUrl}
>
{appWithExtensions.app.name}
</Text>
</Box>
<Box marginBottom={condensed ? 4 : 8} key={appId}>
{!condensed && (
<Box display="flex" alignItems="center" marginBottom={2}>
<AppAvatar size={6} logo={logo ? { source: logo } : undefined} marginRight={2} />
<Text
onClick={e => {
navigate(appPageUrl);

e.preventDefault();
}}
as="a"
size={3}
color="default2"
href={appPageUrl}
>
{appWithExtensions.app.name}
</Text>
</Box>
)}
{appWithExtensions.extensions.map(ext => {
const settingsValidation = appExtensionManifestOptionsSchema.safeParse(
ext.settings,
Expand Down Expand Up @@ -204,10 +216,12 @@ export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {
);

const renderIframeGETvariant = () => (
<Box __height={defaultIframeSize}>
<Text size={3} color="default2" href={appPageUrl}>
{ext.label}
</Text>
<Box __height={condensed ? defaultIframeSizeCondensed : defaultIframeSize}>
{!condensed && (
<Text size={3} color="default2" href={appPageUrl}>
{ext.label}
</Text>
)}
<AppFrame
target="WIDGET"
src={GETappIframeUrl}
Expand All @@ -221,15 +235,18 @@ export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {

const renderIframePOSTvariant = () => (
<Box>
<Text size={3} color="default2" href={appPageUrl}>
{ext.label}
</Text>
{!condensed && (
<Text size={3} color="default2" href={appPageUrl}>
{ext.label}
</Text>
)}
<IframePost
appId={ext.app.id}
accessToken={ext.accessToken}
extensionId={ext.id}
extensionUrl={extensionUrl}
params={params}
condensed={condensed}
/>
</Box>
);
Expand Down Expand Up @@ -284,7 +301,7 @@ export const AppWidgets = ({ extensions, params }: AppWidgetsProps) => {
const renderNonIframe = !isIframeType;

return (
<Box marginBottom={4} key={ext.id}>
<Box marginBottom={condensed ? 2 : 4} key={ext.id}>
{renderGETiframe && renderIframeGETvariant()}
{renderPOSTiframe && renderIframePOSTvariant()}
{renderNonIframe && renderNonIframeExtension()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ORDER_MOUNTS = [
"ORDER_OVERVIEW_MORE_ACTIONS",
"DRAFT_ORDER_OVERVIEW_CREATE",
"DRAFT_ORDER_OVERVIEW_MORE_ACTIONS",
"ORDER_FULFILLMENTS_WIDGETS",
] as const;

const CUSTOMER_MOUNTS = [
Expand Down Expand Up @@ -112,6 +113,7 @@ export type AllAppExtensionMounts = z.infer<typeof ALL_APP_EXTENSION_MOUNTS>;
// Subset of mounts available for WIDGET target
export const WIDGET_AVAILABLE_MOUNTS = [
"ORDER_DETAILS_WIDGETS",
"ORDER_FULFILLMENTS_WIDGETS",
"PRODUCT_DETAILS_WIDGETS",
"VOUCHER_DETAILS_WIDGETS",
"DRAFT_ORDER_DETAILS_WIDGETS",
Expand Down
6 changes: 6 additions & 0 deletions src/extensions/domain/app-extension-manifest-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ const httpMethodSchema = z
.enum(["GET", "POST"], { message: "Method must be either GET or POST" })
.default("GET");

const widgetTargetDisplaySchema = z
.enum(["BLOCK", "INLINE"], { message: "Style must be either INLINE or BLOCK" })
.default("BLOCK");

const newTabTargetOptionsSchema = z.object({
method: httpMethodSchema.optional().nullable(),
});

const widgetTargetOptionsSchema = z.object({
method: httpMethodSchema.optional().nullable(),
display: widgetTargetDisplaySchema.optional().nullable(),
});

export const appExtensionManifestOptionsSchema = z
Expand All @@ -34,6 +39,7 @@ export const appExtensionManifestOptionsSchemaWithDefault =
},
widgetTarget: {
method: "POST",
display: "BLOCK",
},
});

Expand Down
1 change: 1 addition & 0 deletions src/extensions/domain/app-extension-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ describe("App Extension Manifest Schema", () => {
// Arrange
const mounts = [
"ORDER_DETAILS_WIDGETS",
"ORDER_FULFILLMENTS_WIDGETS",
"PRODUCT_DETAILS_WIDGETS",
"VOUCHER_DETAILS_WIDGETS",
"DRAFT_ORDER_DETAILS_WIDGETS",
Expand Down
6 changes: 5 additions & 1 deletion src/extensions/extensionMountPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ export const extensionMountPoints = {
CATEGORY_DETAILS: ["CATEGORY_DETAILS_MORE_ACTIONS"],
GIFT_CARD_DETAILS: ["GIFT_CARD_DETAILS_MORE_ACTIONS", "GIFT_CARD_DETAILS_WIDGETS"],
CUSTOMER_DETAILS: ["CUSTOMER_DETAILS_MORE_ACTIONS", "CUSTOMER_DETAILS_WIDGETS"],
ORDER_DETAILS: ["ORDER_DETAILS_MORE_ACTIONS", "ORDER_DETAILS_WIDGETS"],
ORDER_DETAILS: [
"ORDER_DETAILS_MORE_ACTIONS",
"ORDER_DETAILS_WIDGETS",
"ORDER_FULFILLMENTS_WIDGETS",
],
DRAFT_ORDER_DETAILS: ["DRAFT_ORDER_DETAILS_MORE_ACTIONS", "DRAFT_ORDER_DETAILS_WIDGETS"],
PRODUCT_DETAILS: ["PRODUCT_DETAILS_MORE_ACTIONS", "PRODUCT_DETAILS_WIDGETS"],
DISCOUNT_DETAILS: ["DISCOUNT_DETAILS_MORE_ACTIONS"],
Expand Down
1 change: 1 addition & 0 deletions src/extensions/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface AppDetailsUrlMountQueryParams {
productIds?: string[];
productSlug?: string;
orderId?: string;
fulfillmentId?: string;
customerId?: string;
customerIds?: string[];
categoryIds?: string[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DashboardModal } from "@dashboard/components/Modal";
import { useAppFrameReferences } from "@dashboard/extensions/popup-frame-reference";
import { AppDetailsUrlQueryParams } from "@dashboard/extensions/urls";
import { useAllFlags } from "@dashboard/featureFlags";
Expand Down Expand Up @@ -48,16 +49,11 @@ export const AppFrame = ({
/**
* React on messages from App
*/
const { postToExtension, handshakeDone, setHandshakeDone } = useAppActions(
frameRef.current,
appOrigin,
appId,
appToken,
{
const { postToExtension, handshakeDone, setHandshakeDone, modalState, closeModal } =
useAppActions(frameRef.current, appOrigin, appId, appToken, {
core: coreVersion,
dashboard: dashboardVersion,
},
);
});

/**
* Listen to Dashboard context like theme or locale and inform app about it
Expand Down Expand Up @@ -113,6 +109,12 @@ export const AppFrame = ({
[classes.iframeHidden]: !handshakeDone,
})}
/>
<DashboardModal open={modalState.open} onChange={closeModal}>
<DashboardModal.Content size={modalState.size}>
<DashboardModal.Header>{modalState.title}</DashboardModal.Header>
{modalState.content}
</DashboardModal.Content>
</DashboardModal>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ContentSize } from "@dashboard/components/Modal/Content";
import { getAppMountUri } from "@dashboard/config";
import { useActiveAppExtension } from "@dashboard/extensions/components/AppExtensionContext/AppExtensionContextProvider";
import { ExtensionsUrls, LegacyAppPaths } from "@dashboard/extensions/urls";
Expand All @@ -13,6 +14,7 @@ import {
RequestPermissions,
UpdateRouting,
} from "@saleor/app-sdk/app-bridge";
import { ReactNode, useState } from "react";
import { useIntl } from "react-intl";
import urlJoin from "url-join";

Expand Down Expand Up @@ -267,6 +269,65 @@ const useHandleAppFormUpdate = () => {
};
};

export interface ModalAction {
type: "modal";
payload: {
actionId: string;
title: string;
content: ReactNode;
size?: ContentSize;
};
}

export interface ModalState {
open: boolean;
title: string;
content: ReactNode;
size?: ContentSize;
}

const useHandleModalAction = () => {
const [modalState, setModalState] = useState<ModalState>({
open: false,
title: "",
content: "",
size: "sm",
});

return {
modalState,
openModal: (title: string, content: ReactNode, size?: ContentSize) => {
setModalState({
open: true,
title,
content,
size: size || "sm",
});
},
closeModal: () => {
setModalState(prev => ({
...prev,
open: false,
}));
},
handle: (action: ModalAction) => {
const { actionId, title, content, size } = action.payload;

debug(`Handling Modal action with ID: %s`, actionId);
debug(`Modal title: %s, size: %s`, title, size);

setModalState({
open: true,
title,
content,
size: size || "sm",
});

return createResponseStatus(actionId, true);
},
};
};

export const AppActionsHandler = {
useHandleNotificationAction,
useHandleUpdateRoutingAction,
Expand All @@ -275,4 +336,5 @@ export const AppActionsHandler = {
createResponseStatus,
useHandlePermissionRequest,
useHandleAppFormUpdate,
useHandleModalAction,
};
Loading