Skip to content

Commit

Permalink
Updated AutoTable to automatically deselect records and close after…
Browse files Browse the repository at this point in the history
… running Gadget bulk actions
  • Loading branch information
MillanWangGadget committed Sep 10, 2024
1 parent 3b9413f commit 63b4393
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 119 deletions.
5 changes: 5 additions & 0 deletions packages/react/.changeset/blue-bears-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@gadgetinc/react": patch
---

Updated `AutoTable` to automatically deselect records and close after running Gadget bulk actions
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable jest/valid-expect */
import React from "react";
import { ActionErrorMessage, ActionSuccessMessage } from "../../../../src/auto/polaris/PolarisAutoBulkActionModal.js";
import { PolarisAutoTable } from "../../../../src/auto/polaris/PolarisAutoTable.js";
import { api } from "../../../support/api.js";
import { PolarisWrapper } from "../../../support/auto.js";
Expand Down Expand Up @@ -98,7 +99,7 @@ describe("AutoTable - Bulk actions", () => {
cy.get(`button[aria-label="Actions"]`).should("not.exist");
});

it("Can run the bulkDelete action with the selected IDs", () => {
it.only("Can run the bulkDelete action with the selected IDs", () => {
selectRecordIds(["10", "11", "12"]);
openBulkAction("Delete");

Expand All @@ -113,23 +114,18 @@ describe("AutoTable - Bulk actions", () => {

cy.wait("@getWidgets").its("request.body.variables").should("deep.equal", { first: 50 }); // No search value

cy.contains("Action completed successfully");
cy.get("button").contains("Close").click();
cy.contains(ActionSuccessMessage);

// Now ensure that error response appears in the modal
selectRecordIds(["20", "21", "22"]);

cy.get(`input[id="Select-20"]`).eq(0).click();
cy.get(`input[id="Select-21"]`).eq(0).click();
cy.get(`input[id="Select-22"]`).eq(0).click();
openBulkAction("Delete");

mockBulkDeleteWidgets(bulkDeleteFailureResponse, "bulkDeleteWidgets2");

cy.get("button").contains("Run").click();
cy.wait("@bulkDeleteWidgets2");

cy.contains(bulkDeleteFailureMessage);
cy.contains(ActionErrorMessage);
});

describe.each([true, false])("Custom actions with promoted=%s", (promoted) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const IncludedActionParameters = {
args: {
model: api.autoTableTest,
actions: [
"delete",
"customAction",
{ label: "Alternate action label", action: "customAction" },
{
Expand Down
79 changes: 60 additions & 19 deletions packages/react/src/auto/polaris/PolarisAutoBulkActionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Banner, Button, ButtonGroup, Modal, Spinner, Text } from "@shopify/polaris";
import { Button, ButtonGroup, Modal, Spinner, Text, Toast } from "@shopify/polaris";
import React, { useCallback, useEffect, useMemo } from "react";
import type { TableRow } from "../../use-table/types.js";
import { useBulkAction } from "../../useBulkAction.js";
Expand All @@ -10,8 +10,34 @@ export const PolarisAutoBulkActionModal = (props: {
modelActionDetails?: ModelActionDetails;
ids: string[];
selectedRows: TableRow[];
clearSelection: () => void;
}) => {
const { model, modelActionDetails, ids, selectedRows } = props;
const [toastMessage, setToastMessage] = React.useState<string | undefined>(undefined);

return (
<>
{toastMessage && (
<Toast
content={toastMessage}
onDismiss={() => setToastMessage(undefined)}
duration={4500}
error={toastMessage.includes(ActionErrorMessage)}
/>
)}
<BulkActionModal {...props} setToastMessage={setToastMessage} />
</>
);
};

const BulkActionModal = (props: {
model: any;
modelActionDetails?: ModelActionDetails;
ids: string[];
selectedRows: TableRow[];
clearSelection: () => void;
setToastMessage: (message: string) => void;
}) => {
const { model, modelActionDetails, ids, selectedRows, clearSelection, setToastMessage } = props;

const [showModal, setShowModal] = React.useState(!!modelActionDetails);
const [actionName, setActionName] = React.useState(modelActionDetails?.apiIdentifier);
Expand All @@ -38,7 +64,15 @@ export const PolarisAutoBulkActionModal = (props: {
return (
<>
<Modal onClose={() => setShowModal(false)} title={modalTitle} open={showModal}>
<GadgetBulkActionModalContent model={model} modelActionDetails={modelActionDetails} ids={ids} close={closeModal} />
<GadgetBulkActionModalContent
model={model}
modelActionDetails={modelActionDetails}
actionLabel={modalTitle.replace("Bulk ", "")}
ids={ids}
close={closeModal}
clearSelection={clearSelection}
setToastMessage={setToastMessage}
/>
</Modal>
</>
);
Expand All @@ -47,8 +81,16 @@ export const PolarisAutoBulkActionModal = (props: {
/**
* Modal content for executing Gadget bulk actions
*/
const GadgetBulkActionModalContent = (props: { model: any; modelActionDetails: ModelActionDetails; ids: string[]; close: () => void }) => {
const { model, modelActionDetails, ids, close } = props;
const GadgetBulkActionModalContent = (props: {
model: any;
modelActionDetails: ModelActionDetails;
actionLabel: string;
ids: string[];
close: () => void;
clearSelection: () => void;
setToastMessage: (message: string) => void;
}) => {
const { model, modelActionDetails, actionLabel: actionName, ids, close, clearSelection, setToastMessage } = props;

const [hasRun, setHasRun] = React.useState(false);

Expand All @@ -57,27 +99,26 @@ const GadgetBulkActionModalContent = (props: { model: any; modelActionDetails: M
const [{ fetching, data, error }, runBulkAction] = useBulkAction(model[modelActionDetails.apiIdentifier], {});

const hasError = !!(error || (data && (data as any).success === false));
const errorMessage = error
? error.message
: data && (data as any)?.success === false
? (data as any)?.errors?.map((e: { message: string }) => e.message).join(", ")
: "";

const actionResultBanner = useMemo(
() => (hasError ? <Banner title={errorMessage} tone="critical" /> : <Banner title={ActionSuccessMessage} tone="success" />),
[hasError, errorMessage]
);

const runAction = useCallback(() => {
void runBulkAction(ids);
setHasRun(true);
}, [runBulkAction, ids]);
}, [runBulkAction, ids, clearSelection, close]);

// Automatically close the modal if the action is successful
useEffect(() => {
if (!fetching && hasRun) {
clearSelection();
close();
setToastMessage(hasError ? `${actionName}${ActionErrorMessage}` : `${actionName}${ActionSuccessMessage}`);
}
}, [fetching, hasRun, hasError, clearSelection, close, setToastMessage, actionName]);

return (
<>
<Modal.Section>
{fetching && <CenteredSpinner />}
{!fetching && (hasRun ? actionResultBanner : <RunActionConfirmationText count={ids.length} />)}
{!fetching && !hasRun && <RunActionConfirmationText count={ids.length} />}
</Modal.Section>
<Modal.Section>
<div style={{ float: "right", paddingBottom: "16px" }}>
Expand All @@ -103,8 +144,8 @@ const CenteredSpinner = () => (
</div>
);

const ActionCompletedMessage = `Action completed`;
const ActionSuccessMessage = `${ActionCompletedMessage} successfully`;
export const ActionSuccessMessage = ` completed`;
export const ActionErrorMessage = ` failed`;

const RunActionConfirmationText = (props: { count: number }) => {
const { count } = props;
Expand Down
190 changes: 98 additions & 92 deletions packages/react/src/auto/polaris/PolarisAutoTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
Box,
DataTable,
EmptySearchResult,
Frame,
IndexFilters,
IndexTable,
SkeletonBodyText,
useSetIndexFiltersMode,
} from "@shopify/polaris";

import pluralize from "pluralize";
import type { ReactNode } from "react";
import React, { useCallback, useMemo } from "react";
Expand Down Expand Up @@ -151,101 +153,105 @@ const PolarisAutoTableComponent = <

return (
<AutoTableContext.Provider value={[methods, refresh]}>
<BlockStack>
<PolarisAutoBulkActionModal
model={props.model}
modelActionDetails={selectedModelActionDetails}
ids={selection.recordIds}
selectedRows={selectedRows}
/>
{searchable && (
<IndexFilters
mode={mode}
setMode={setMode}
appliedFilters={[]}
filters={[]}
onClearAll={() => undefined}
tabs={[]}
canCreateNewView={false}
selected={1}
loading={fetching}
cancelAction={{ onAction: () => search.clear() }}
disabled={!!error}
// Search
queryValue={search.value}
onQueryChange={search.set}
onQueryClear={search.clear}
<Frame>
<BlockStack>
<PolarisAutoBulkActionModal
model={props.model}
modelActionDetails={selectedModelActionDetails}
ids={selection.recordIds}
selectedRows={selectedRows}
clearSelection={selection.clearAll}
/>
)}

{error && (
<Box paddingBlockStart="200" paddingBlockEnd="1000">
<Banner title={error.message} tone="critical" />
</Box>
)}
{searchable && (
<IndexFilters
mode={mode}
setMode={setMode}
appliedFilters={[]}
filters={[]}
onClearAll={() => undefined}
tabs={[]}
canCreateNewView={false}
selected={1}
loading={fetching}
cancelAction={{ onAction: () => search.clear() }}
disabled={!!error}
// Search
queryValue={search.value}
onQueryChange={search.set}
onQueryClear={search.clear}
/>
)}

<IndexTable
selectedItemsCount={selection.recordIds.length}
{...disablePaginatedSelectAllButton}
onSelectionChange={selection.onSelectionChange}
{...polarisTableProps}
promotedBulkActions={promotedBulkActions.length ? promotedBulkActions : undefined}
bulkActions={bulkActions.length ? bulkActions : undefined}
resourceName={resourceName}
emptyState={props.emptyState ?? <EmptySearchResult title={`No ${resourceName.plural} yet`} description={""} withIllustration />}
loading={fetching}
hasMoreItems={page.hasNextPage}
itemCount={
error
? 1 // Don't show the empty state if there's an error
: rows?.length ?? 0
}
pagination={
paginate
? {
hasNext: page.hasNextPage,
hasPrevious: page.hasPreviousPage,
onNext: page.goToNextPage,
onPrevious: page.goToPreviousPage,
}
: undefined
}
sortDirection={gadgetToPolarisDirection(sort.direction)}
sortColumnIndex={columns ? getColumnIndex(columns, sort.column) : undefined}
onSort={(headingIndex) => handleColumnSort(headingIndex)}
selectable={props.selectable === undefined ? bulkActionOptions.length !== 0 : props.selectable}
lastColumnSticky={props.lastColumnSticky}
hasZebraStriping={props.hasZebraStriping}
condensed={props.condensed}
>
{rows &&
columns &&
rows.map((row, index) => {
const rawRecord = rawRecords?.[index];
return (
<IndexTable.Row
key={row.id as string}
id={row.id as string}
position={index}
onClick={onClick ? onClickCallback(row, rawRecord) : undefined}
selected={selection.recordIds.includes(row.id as string)}
>
{columns.map((column) => (
<IndexTable.Cell key={column.identifier}>
<div style={{ maxWidth: "200px" }}>
{column.type == "CustomRenderer" ? (
(row[column.identifier] as ReactNode)
) : (
<PolarisAutoTableCellRenderer column={column} value={row[column.identifier] as ColumnValueType} />
)}
</div>
</IndexTable.Cell>
))}
</IndexTable.Row>
);
})}
</IndexTable>
</BlockStack>
{error && (
<Box paddingBlockStart="200" paddingBlockEnd="1000">
<Banner title={error.message} tone="critical" />
</Box>
)}

<IndexTable
selectedItemsCount={selection.recordIds.length}
{...disablePaginatedSelectAllButton}
onSelectionChange={selection.onSelectionChange}
{...polarisTableProps}
promotedBulkActions={promotedBulkActions.length ? promotedBulkActions : undefined}
bulkActions={bulkActions.length ? bulkActions : undefined}
resourceName={resourceName}
emptyState={props.emptyState ?? <EmptySearchResult title={`No ${resourceName.plural} yet`} description={""} withIllustration />}
loading={fetching}
hasMoreItems={page.hasNextPage}
itemCount={
error
? 1 // Don't show the empty state if there's an error
: rows?.length ?? 0
}
pagination={
paginate
? {
hasNext: page.hasNextPage,
hasPrevious: page.hasPreviousPage,
onNext: page.goToNextPage,
onPrevious: page.goToPreviousPage,
}
: undefined
}
sortDirection={gadgetToPolarisDirection(sort.direction)}
sortColumnIndex={columns ? getColumnIndex(columns, sort.column) : undefined}
onSort={(headingIndex) => handleColumnSort(headingIndex)}
selectable={props.selectable === undefined ? bulkActionOptions.length !== 0 : props.selectable}
lastColumnSticky={props.lastColumnSticky}
hasZebraStriping={props.hasZebraStriping}
condensed={props.condensed}
>
{rows &&
columns &&
rows.map((row, index) => {
const rawRecord = rawRecords?.[index];
return (
<IndexTable.Row
key={row.id as string}
id={row.id as string}
position={index}
onClick={onClick ? onClickCallback(row, rawRecord) : undefined}
selected={selection.recordIds.includes(row.id as string)}
>
{columns.map((column) => (
<IndexTable.Cell key={column.identifier}>
<div style={{ maxWidth: "200px" }}>
{column.type == "CustomRenderer" ? (
(row[column.identifier] as ReactNode)
) : (
<PolarisAutoTableCellRenderer column={column} value={row[column.identifier] as ColumnValueType} />
)}
</div>
</IndexTable.Cell>
))}
</IndexTable.Row>
);
})}
</IndexTable>
</BlockStack>
</Frame>
</AutoTableContext.Provider>
);
};
Expand Down

0 comments on commit 63b4393

Please sign in to comment.