Introduce datagrid on staff members list view (#4044)

This commit is contained in:
Paweł Chyła 2023-08-03 09:52:39 +02:00 committed by GitHub
parent 4c43976270
commit f14ba5bcfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 458 additions and 323 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-dashboard": minor
---
Introduce datagrid on staff members list view

View file

@ -12,11 +12,7 @@ import {
STAFF_MEMBERS_LIST_SELECTORS,
} from "../elements/";
import { LOGIN_SELECTORS } from "../elements/account/login-selectors";
import {
MESSAGES,
TEST_ADMIN_USER,
urlList,
} from "../fixtures";
import { MESSAGES, TEST_ADMIN_USER, urlList } from "../fixtures";
import { userDetailsUrl } from "../fixtures/urlList";
import {
activatePlugin,
@ -28,6 +24,7 @@ import {
getMailActivationLinkForUserAndSubject,
inviteStaffMemberWithFirstPermission,
} from "../support/api/utils/";
import { ensureCanvasStatic } from "../support/customCommands/sharedElementsOperations/canvas";
import {
expectMainMenuAvailableSections,
expectWelcomeMessageIncludes,
@ -72,10 +69,11 @@ describe("Staff members", () => {
const firstName = faker.name.firstName();
const emailInvite = `${startsWith}${firstName}@example.com`;
cy.visit(urlList.staffMembers)
.expectSkeletonIsVisible()
.get(STAFF_MEMBERS_LIST_SELECTORS.inviteStaffMemberButton)
.click({ force: true });
cy.visit(urlList.staffMembers);
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
cy.get(STAFF_MEMBERS_LIST_SELECTORS.inviteStaffMemberButton).click({
force: true,
});
fillUpUserDetailsAndAddFirstPermission(firstName, lastName, emailInvite);
getMailActivationLinkForUser(emailInvite).then(urlLink => {
cy.clearSessionData().visit(urlLink);
@ -185,10 +183,11 @@ describe("Staff members", () => {
() => {
const firstName = faker.name.firstName();
const emailInvite = TEST_ADMIN_USER.email;
cy.visit(urlList.staffMembers)
.expectSkeletonIsVisible()
.get(STAFF_MEMBERS_LIST_SELECTORS.inviteStaffMemberButton)
.click({ force: true });
cy.visit(urlList.staffMembers);
ensureCanvasStatic(SHARED_ELEMENTS.dataGridTable);
cy.get(STAFF_MEMBERS_LIST_SELECTORS.inviteStaffMemberButton).click({
force: true,
});
fillUpOnlyUserDetails(firstName, lastName, emailInvite);
cy.get(INVITE_STAFF_MEMBER_FORM_SELECTORS.emailValidationMessage).should(
"be.visible",

View file

@ -3673,6 +3673,10 @@
"context": "header",
"string": "{webhookName} Details"
},
"OTDo9I": {
"context": "tab name",
"string": "All staff members"
},
"OVOU1z": {
"context": "section header",
"string": "Metadata"
@ -5133,10 +5137,6 @@
"context": "no address is set in draft order",
"string": "Not set"
},
"YJ4TXc": {
"context": "tab name",
"string": "All Staff Members"
},
"YKyNm9": {
"context": "label",
"string": "Gift Card"
@ -5386,9 +5386,6 @@
"context": "subheader",
"string": "Here is some information we gathered about your store"
},
"aDbrOK": {
"string": "Search Staff Member"
},
"aEc9Ar": {
"context": "gift card history message",
"string": "Gift card balance was reset by {resetBy}"
@ -7258,6 +7255,9 @@
"context": "attributes, section header",
"string": "Variant Selection Attributes"
},
"o68j+t": {
"string": "Search staff members..."
},
"o8S0Ac": {
"context": "usage limit uses left caption",
"string": "Uses left"

View file

@ -121,6 +121,7 @@ export const defaultListSettings: AppListViewSettings = {
},
[ListViews.STAFF_MEMBERS_LIST]: {
rowNumber: PAGINATE_BY,
columns: ["name", "email", "status"],
},
[ListViews.PERMISSION_GROUP_LIST]: {
rowNumber: PAGINATE_BY,

View file

@ -31,7 +31,7 @@ export const useFilterPresets = <
getUrl,
}: {
params: T;
reset: () => void;
reset?: () => void;
getUrl: () => string;
storageUtils: StorageUtils<string>;
}): UseFilterPresets => {
@ -47,7 +47,7 @@ export const useFilterPresets = <
: undefined;
const onPresetChange = (index: number) => {
reset();
reset?.();
const currentPresets = storageUtils.getFilterTabs();
const qs = new URLSearchParams(currentPresets[index - 1]?.data ?? "");
qs.append("activeTab", index.toString());
@ -65,7 +65,7 @@ export const useFilterPresets = <
}
storageUtils.deleteFilterTab(presetIdToDelete);
reset();
reset?.();
// When deleting the current tab, navigate to the All products
if (presetIdToDelete === selectedPreset || !selectedPreset) {

View file

@ -1,169 +0,0 @@
// @ts-strict-ignore
import ResponsiveTable from "@dashboard/components/ResponsiveTable";
import Skeleton from "@dashboard/components/Skeleton";
import TableCellHeader from "@dashboard/components/TableCellHeader";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import TableRowLink from "@dashboard/components/TableRowLink";
import { UserAvatar } from "@dashboard/components/UserAvatar";
import { StaffListQuery } from "@dashboard/graphql";
import { commonStatusMessages } from "@dashboard/intl";
import {
getUserInitials,
getUserName,
renderCollection,
} from "@dashboard/misc";
import {
StaffListUrlSortField,
staffMemberDetailsUrl,
} from "@dashboard/staff/urls";
import { ListProps, RelayToFlat, SortPage } from "@dashboard/types";
import { getArrowDirection } from "@dashboard/utils/sort";
import {
TableBody,
TableCell,
TableFooter,
TableHead,
} from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { Box, Text } from "@saleor/macaw-ui/next";
import clsx from "clsx";
import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
const useStyles = makeStyles(
{
colEmail: {
width: 400,
},
tableRow: {
cursor: "pointer",
},
wideColumn: {
width: "80%",
},
},
{ name: "StaffList" },
);
interface StaffListProps extends ListProps, SortPage<StaffListUrlSortField> {
staffMembers: RelayToFlat<StaffListQuery["staffUsers"]>;
}
const numberOfColumns = 2;
const StaffList: React.FC<StaffListProps> = props => {
const {
settings,
disabled,
onUpdateListSettings,
onSort,
sort,
staffMembers,
} = props;
const classes = useStyles(props);
const intl = useIntl();
return (
<ResponsiveTable>
<colgroup>
<col />
<col className={classes.colEmail} />
</colgroup>
<TableHead>
<TableRowLink>
<TableCellHeader
direction={
sort.sort === StaffListUrlSortField.name
? getArrowDirection(sort.asc)
: undefined
}
arrowPosition="right"
onClick={() => onSort(StaffListUrlSortField.name)}
className={classes.wideColumn}
>
<FormattedMessage
id="W32xfN"
defaultMessage="Name"
description="staff member full name"
/>
</TableCellHeader>
<TableCellHeader
direction={
sort.sort === StaffListUrlSortField.email
? getArrowDirection(sort.asc)
: undefined
}
onClick={() => onSort(StaffListUrlSortField.email)}
>
<FormattedMessage id="xxQxLE" defaultMessage="Email Address" />
</TableCellHeader>
</TableRowLink>
</TableHead>
<TableFooter>
<TableRowLink>
<TablePaginationWithContext
colSpan={numberOfColumns}
disabled={disabled}
settings={settings}
onUpdateListSettings={onUpdateListSettings}
/>
</TableRowLink>
</TableFooter>
<TableBody>
{renderCollection(
staffMembers,
staffMember => (
<TableRowLink
className={clsx({
[classes.tableRow]: !!staffMember,
})}
hover={!!staffMember}
href={staffMember && staffMemberDetailsUrl(staffMember.id)}
key={staffMember ? staffMember.id : "skeleton"}
>
<TableCell>
<Box display="flex" alignItems="center" gap={2}>
<UserAvatar
url={staffMember?.avatar?.url}
initials={getUserInitials(staffMember)}
/>
<Box display="flex" flexDirection="column">
<Text>{getUserName(staffMember) || <Skeleton />}</Text>
<Text
variant="caption"
data-test-id="staffStatusText"
color="textNeutralSubdued"
>
{staffMember?.isActive
? intl.formatMessage(commonStatusMessages.active)
: intl.formatMessage(commonStatusMessages.notActive)}
</Text>
</Box>
</Box>
</TableCell>
<TableCell>
<Text size="small" data-test-id="user-mail">
{staffMember?.email}
</Text>
</TableCell>
</TableRowLink>
),
() => (
<TableRowLink>
<TableCell colSpan={numberOfColumns}>
<FormattedMessage
id="xJQX5t"
defaultMessage="No staff members found"
/>
</TableCell>
</TableRowLink>
),
)}
</TableBody>
</ResponsiveTable>
);
};
StaffList.displayName = "StaffList";
export default StaffList;

View file

@ -1,2 +0,0 @@
export { default } from "./StaffList";
export * from "./StaffList";

View file

@ -0,0 +1,137 @@
import { useColumns } from "@dashboard/components/Datagrid/ColumnPicker/useColumns";
import Datagrid from "@dashboard/components/Datagrid/Datagrid";
import {
DatagridChangeStateContext,
useDatagridChangeState,
} from "@dashboard/components/Datagrid/hooks/useDatagridChange";
import { useEmptyColumn } from "@dashboard/components/Datagrid/hooks/useEmptyColumn";
import { TablePaginationWithContext } from "@dashboard/components/TablePagination";
import useNavigator from "@dashboard/hooks/useNavigator";
import { StaffMember, StaffMembers } from "@dashboard/staff/types";
import {
StaffListUrlSortField,
staffMemberDetailsUrl,
} from "@dashboard/staff/urls";
import { ListProps, SortPage } from "@dashboard/types";
import { Item } from "@glideapps/glide-data-grid";
import { Box, useTheme } from "@saleor/macaw-ui/next";
import React, { useCallback, useMemo } from "react";
import { useIntl } from "react-intl";
import {
createGetCellContent,
staffMemebersListStaticColumnsAdapter,
} from "./datagrid";
import { messages } from "./messages";
interface StaffListDatagridProps
extends ListProps,
SortPage<StaffListUrlSortField> {
staffMembers: StaffMembers;
}
export const StaffListDatagrid = ({
staffMembers,
settings,
sort,
disabled,
onSort,
onUpdateListSettings,
}: StaffListDatagridProps) => {
const datagridState = useDatagridChangeState();
const navigate = useNavigator();
const intl = useIntl();
const { themeValues } = useTheme();
const emptyColumn = useEmptyColumn();
const staffMemebersListStaticColumns = useMemo(
() => staffMemebersListStaticColumnsAdapter(intl, sort, emptyColumn),
[intl, sort, emptyColumn],
);
const onColumnChange = useCallback(
(picked: string[]) => {
if (onUpdateListSettings) {
onUpdateListSettings("columns", picked.filter(Boolean));
}
},
[onUpdateListSettings],
);
const { handlers, visibleColumns, recentlyAddedColumn } = useColumns({
selectedColumns: settings?.columns ?? [],
staticColumns: staffMemebersListStaticColumns,
onSave: onColumnChange,
});
const getCellContent = useCallback(
createGetCellContent({
staffMembers,
columns: visibleColumns,
intl,
currentTheme: themeValues,
}),
[staffMembers, intl, visibleColumns],
);
const handleRowClick = useCallback(
([_, row]: Item) => {
const rowData: StaffMember = staffMembers[row];
if (rowData) {
navigate(staffMemberDetailsUrl(rowData?.id));
}
},
[staffMembers],
);
const handleRowAnchor = useCallback(
([, row]: Item) => staffMemberDetailsUrl(staffMembers[row]?.id),
[staffMembers],
);
const handleHeaderClick = useCallback(
(col: number) => {
const columnName = visibleColumns[col].id as StaffListUrlSortField;
if (Object.values(StaffListUrlSortField).includes(columnName)) {
onSort(columnName);
}
},
[visibleColumns, onSort],
);
return (
<DatagridChangeStateContext.Provider value={datagridState}>
<Datagrid
readonly
loading={disabled}
rowMarkers="none"
columnSelect="single"
hasRowHover={true}
onColumnMoved={handlers.onMove}
onColumnResize={handlers.onResize}
verticalBorder={col => col > 1}
rows={staffMembers?.length ?? 0}
availableColumns={visibleColumns}
emptyText={intl.formatMessage(messages.empty)}
getCellContent={getCellContent}
getCellError={() => false}
selectionActions={() => null}
menuItems={() => []}
onRowClick={handleRowClick}
onHeaderClicked={handleHeaderClick}
rowAnchor={handleRowAnchor}
recentlyAddedColumn={recentlyAddedColumn}
/>
<Box paddingX={6}>
<TablePaginationWithContext
component="div"
settings={settings}
disabled={disabled}
onUpdateListSettings={onUpdateListSettings}
/>
</Box>
</DatagridChangeStateContext.Provider>
);
};

View file

@ -0,0 +1,104 @@
import { PLACEHOLDER } from "@dashboard/components/Datagrid/const";
import {
readonlyTextCell,
tagsCell,
thumbnailCell,
} from "@dashboard/components/Datagrid/customCells/cells";
import { AvailableColumn } from "@dashboard/components/Datagrid/types";
import { commonStatusMessages } from "@dashboard/intl";
import { getStatusColor, getUserName } from "@dashboard/misc";
import { StaffMember, StaffMembers } from "@dashboard/staff/types";
import { StaffListUrlSortField } from "@dashboard/staff/urls";
import { Sort } from "@dashboard/types";
import { getColumnSortDirectionIcon } from "@dashboard/utils/columns/getColumnSortDirectionIcon";
import { GridCell, Item } from "@glideapps/glide-data-grid";
import { ThemeTokensValues } from "@saleor/macaw-ui/next";
import { IntlShape } from "react-intl";
import { columnsMessages } from "./messages";
export const staffMemebersListStaticColumnsAdapter = (
intl: IntlShape,
sort: Sort<StaffListUrlSortField>,
emptyColumn: AvailableColumn,
) =>
[
emptyColumn,
{
id: "name",
title: intl.formatMessage(columnsMessages.name),
width: 400,
},
{
id: "status",
title: intl.formatMessage(columnsMessages.status),
width: 150,
},
{
id: "email",
title: intl.formatMessage(columnsMessages.email),
width: 400,
},
].map(column => ({
...column,
icon: getColumnSortDirectionIcon(sort, column.id),
}));
export const createGetCellContent =
({
staffMembers,
columns,
intl,
currentTheme,
}: {
staffMembers: StaffMembers;
columns: AvailableColumn[];
intl: IntlShape;
currentTheme: ThemeTokensValues;
}) =>
([column, row]: Item): GridCell => {
const rowData: StaffMember | undefined = staffMembers[row];
const columnId = columns[column]?.id;
if (!columnId || !rowData) {
return readonlyTextCell("");
}
switch (columnId) {
case "name":
return thumbnailCell(
getUserName(rowData) ?? "",
rowData?.avatar?.url ?? "",
{
cursor: "pointer",
},
);
case "status":
const isActive = rowData?.isActive;
const status = isActive
? intl.formatMessage(commonStatusMessages.active)
: intl.formatMessage(commonStatusMessages.notActive);
const statusColor = getStatusColor(isActive ? "success" : "error");
return tagsCell(
[
{
tag: status,
color:
currentTheme.colors.background[
statusColor as keyof typeof currentTheme.colors.background
],
},
],
[status],
{
readonly: true,
allowOverlay: false,
},
);
case "email":
return readonlyTextCell(rowData?.email ?? PLACEHOLDER);
default:
return readonlyTextCell("");
}
};

View file

@ -0,0 +1 @@
export * from "./StaffListDatagrid";

View file

@ -0,0 +1,24 @@
import { defineMessages } from "react-intl";
export const columnsMessages = defineMessages({
name: {
id: "W32xfN",
defaultMessage: "Name",
description: "staff member full name",
},
email: {
id: "xxQxLE",
defaultMessage: "Email Address",
},
status: {
id: "tzMNF3",
defaultMessage: "Status",
},
});
export const messages = defineMessages({
empty: {
id: "xJQX5t",
defaultMessage: "No staff members found",
},
});

View file

@ -1,12 +1,12 @@
// @ts-strict-ignore
import {
filterPageProps,
filterPresetsProps,
limits,
limitsReached,
pageListProps,
searchPageProps,
sortPageProps,
tabPageProps,
} from "@dashboard/fixtures";
import { StaffMemberStatus } from "@dashboard/graphql";
import { staffMembers } from "@dashboard/staff/fixtures";
@ -20,8 +20,8 @@ const props: StaffListPageProps = {
...pageListProps.default,
...searchPageProps,
...sortPageProps,
...tabPageProps,
...filterPageProps,
...filterPresetsProps,
filterOpts: {
status: {
active: false,
@ -35,6 +35,10 @@ const props: StaffListPageProps = {
sort: StaffListUrlSortField.name,
},
staffMembers,
settings: {
rowNumber: 10,
columns: ["name", "email", "status"],
},
};
const meta: Meta<typeof StaffListPage> = {

View file

@ -1,27 +1,25 @@
// @ts-strict-ignore
import { LimitsInfo } from "@dashboard/components/AppLayout/LimitsInfo";
import { ListFilters } from "@dashboard/components/AppLayout/ListFilters";
import { TopNav } from "@dashboard/components/AppLayout/TopNav";
import { Button } from "@dashboard/components/Button";
import FilterBar from "@dashboard/components/FilterBar";
import { FilterPresetsSelect } from "@dashboard/components/FilterPresetsSelect";
import { ListPageLayout } from "@dashboard/components/Layouts";
import LimitReachedAlert from "@dashboard/components/LimitReachedAlert";
import { configurationMenuUrl } from "@dashboard/configuration";
import { RefreshLimitsQuery, StaffListQuery } from "@dashboard/graphql";
import { RefreshLimitsQuery } from "@dashboard/graphql";
import { sectionNames } from "@dashboard/intl";
import { StaffMembers } from "@dashboard/staff/types";
import { StaffListUrlSortField } from "@dashboard/staff/urls";
import {
FilterPageProps,
FilterPagePropsWithPresets,
ListProps,
RelayToFlat,
SortPage,
TabPageProps,
} from "@dashboard/types";
import { hasLimits, isLimitReached } from "@dashboard/utils/limits";
import { Card } from "@material-ui/core";
import React from "react";
import { Box, Button, ChevronRightIcon } from "@saleor/macaw-ui/next";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import StaffList from "../StaffList/StaffList";
import { StaffListDatagrid } from "../StaffListDatagrid";
import {
createFilterStructure,
StaffFilterKeys,
@ -30,30 +28,33 @@ import {
export interface StaffListPageProps
extends ListProps,
FilterPageProps<StaffFilterKeys, StaffListFilterOpts>,
SortPage<StaffListUrlSortField>,
TabPageProps {
limits: RefreshLimitsQuery["shop"]["limits"];
staffMembers: RelayToFlat<StaffListQuery["staffUsers"]>;
FilterPagePropsWithPresets<StaffFilterKeys, StaffListFilterOpts>,
SortPage<StaffListUrlSortField> {
limits: RefreshLimitsQuery["shop"]["limits"] | undefined;
staffMembers: StaffMembers;
onAdd: () => void;
}
const StaffListPage: React.FC<StaffListPageProps> = ({
currentTab,
filterOpts,
initialSearch,
limits,
currencySymbol,
filterPresets,
selectedFilterPreset,
onAdd,
onAll,
onFilterChange,
onSearchChange,
onTabChange,
onTabDelete,
onTabSave,
tabs,
hasPresetsChanged,
onFilterPresetChange,
onFilterPresetDelete,
onFilterPresetPresetSave,
onFilterPresetUpdate,
onFilterPresetsAll,
...listProps
}) => {
const intl = useIntl();
const [isFilterPresetOpen, setFilterPresetOpen] = useState(false);
const structure = createFilterStructure(intl, filterOpts);
const reachedLimit = isLimitReached(limits, "staffUsers");
@ -63,7 +64,39 @@ const StaffListPage: React.FC<StaffListPageProps> = ({
<TopNav
href={configurationMenuUrl}
title={intl.formatMessage(sectionNames.staff)}
isAlignToRight={false}
withoutBorder
>
<Box
__flex={1}
display="flex"
justifyContent="space-between"
alignItems="center"
>
<Box display="flex">
<Box marginX={3} display="flex" alignItems="center">
<ChevronRightIcon />
</Box>
<FilterPresetsSelect
presetsChanged={hasPresetsChanged()}
onSelect={onFilterPresetChange}
onRemove={onFilterPresetDelete}
onUpdate={onFilterPresetUpdate}
savedPresets={filterPresets}
activePreset={selectedFilterPreset}
onSelectAll={onFilterPresetsAll}
onSave={onFilterPresetPresetSave}
isOpen={isFilterPresetOpen}
onOpenChange={setFilterPresetOpen}
selectAllLabel={intl.formatMessage({
id: "OTDo9I",
defaultMessage: "All staff members",
description: "tab name",
})}
/>
</Box>
<Box>
<Button
data-test-id="invite-staff-member"
disabled={reachedLimit}
@ -76,22 +109,24 @@ const StaffListPage: React.FC<StaffListPageProps> = ({
description="button"
/>
</Button>
</Box>
</Box>
</TopNav>
{hasLimits(limits, "staffUsers") && (
<LimitsInfo
text={intl.formatMessage(
<Box gridColumn="8" marginLeft={6} marginBottom={reachedLimit ? 0 : 3}>
{intl.formatMessage(
{
id: "9xlPgt",
defaultMessage: "{count}/{max} members",
description: "used staff users counter",
},
{
count: limits.currentUsage.staffUsers,
max: limits.allowedUsage.staffUsers,
count: limits?.currentUsage?.staffUsers ?? 0,
max: limits?.allowedUsage?.staffUsers ?? 0,
},
)}
/>
</Box>
)}
</TopNav>
{reachedLimit && (
<LimitReachedAlert
title={intl.formatMessage({
@ -107,28 +142,19 @@ const StaffListPage: React.FC<StaffListPageProps> = ({
</LimitReachedAlert>
)}
<Card>
<FilterBar
allTabLabel={intl.formatMessage({
id: "YJ4TXc",
defaultMessage: "All Staff Members",
description: "tab name",
})}
currentTab={currentTab}
filterStructure={structure}
<ListFilters<StaffFilterKeys>
currencySymbol={currencySymbol}
initialSearch={initialSearch}
searchPlaceholder={intl.formatMessage({
id: "aDbrOK",
defaultMessage: "Search Staff Member",
})}
tabs={tabs}
onAll={onAll}
onFilterChange={onFilterChange}
onSearchChange={onSearchChange}
onTabChange={onTabChange}
onTabDelete={onTabDelete}
onTabSave={onTabSave}
filterStructure={structure}
searchPlaceholder={intl.formatMessage({
id: "o68j+t",
defaultMessage: "Search staff members...",
})}
/>
<StaffList {...listProps} />
<StaffListDatagrid {...listProps} />
</Card>
</ListPageLayout>
);

View file

@ -9,7 +9,7 @@ export enum StaffFilterKeys {
}
export interface StaffListFilterOpts {
status: FilterOpts<StaffMemberStatus>;
status: FilterOpts<StaffMemberStatus | null>;
}
const messages = defineMessages({
@ -39,7 +39,7 @@ export function createFilterStructure(
...createOptionsField(
StaffFilterKeys.status,
intl.formatMessage(messages.status),
[opts.status.value],
[opts.status.value ?? ""],
false,
[
{

7
src/staff/types.ts Normal file
View file

@ -0,0 +1,7 @@
import { StaffListQuery } from "@dashboard/graphql";
import { RelayToFlat } from "@dashboard/types";
export type StaffMembers = RelayToFlat<
NonNullable<StaffListQuery["staffUsers"]>
>;
export type StaffMember = StaffMembers[number];

View file

@ -1,15 +1,13 @@
// @ts-strict-ignore
import { newPasswordUrl } from "@dashboard/auth/urls";
import DeleteFilterTabDialog from "@dashboard/components/DeleteFilterTabDialog";
import SaveFilterTabDialog, {
SaveFilterTabDialogFormData,
} from "@dashboard/components/SaveFilterTabDialog";
import SaveFilterTabDialog from "@dashboard/components/SaveFilterTabDialog";
import { useShopLimitsQuery } from "@dashboard/components/Shop/queries";
import { DEFAULT_INITIAL_SEARCH_DATA } from "@dashboard/config";
import {
useStaffListQuery,
useStaffMemberAddMutation,
} from "@dashboard/graphql";
import { useFilterPresets } from "@dashboard/hooks/useFilterPresets";
import useListSettings from "@dashboard/hooks/useListSettings";
import useNavigator from "@dashboard/hooks/useNavigator";
import useNotifier from "@dashboard/hooks/useNotifier";
@ -19,7 +17,6 @@ import usePaginator, {
PaginatorContext,
} from "@dashboard/hooks/usePaginator";
import { commonMessages } from "@dashboard/intl";
import { getStringOrPlaceholder } from "@dashboard/misc";
import usePermissionGroupSearch from "@dashboard/searches/usePermissionGroupSearch";
import { ListViews } from "@dashboard/types";
import createDialogActionHandlers from "@dashboard/utils/handlers/dialogActionHandlers";
@ -43,14 +40,10 @@ import {
staffMemberDetailsUrl,
} from "../../urls";
import {
deleteFilterTab,
getActiveFilters,
getFilterOpts,
getFilterQueryParam,
getFiltersCurrentTab,
getFilterTabs,
getFilterVariables,
saveFilterTab,
storageUtils,
} from "./filters";
import { getSortQueryVariables } from "./sort";
@ -89,27 +82,39 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
const [addStaffMember, addStaffMemberData] = useStaffMemberAddMutation({
onCompleted: data => {
if (data.staffCreate.errors.length === 0) {
if (data?.staffCreate?.errors?.length === 0) {
notify({
status: "success",
text: intl.formatMessage(commonMessages.savedChanges),
});
navigate(staffMemberDetailsUrl(data.staffCreate.user.id));
navigate(staffMemberDetailsUrl(data?.staffCreate?.user?.id ?? ""));
}
},
});
const paginationValues = usePaginator({
pageInfo: staffQueryData?.staffUsers.pageInfo,
pageInfo: staffQueryData?.staffUsers?.pageInfo,
paginationState,
queryString: params,
});
const handleSort = createSortHandler(navigate, staffListUrl, params);
const tabs = getFilterTabs();
const currentTab = getFiltersCurrentTab(params, tabs);
const {
hasPresetsChanged,
onPresetChange,
onPresetDelete,
onPresetSave,
onPresetUpdate,
selectedPreset,
presets,
getPresetNameToDelete,
setPresetIdToDelete,
} = useFilterPresets({
getUrl: staffListUrl,
params,
storageUtils,
});
const [changeFilters, resetFilters, handleSearchChange] =
createFilterHandlers({
@ -117,6 +122,7 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
getFilterQueryParam,
navigate,
params,
keepActiveTab: true,
});
const [openModal, closeModal] = createDialogActionHandlers<
@ -124,25 +130,6 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
StaffListUrlQueryParams
>(navigate, staffListUrl, params);
const handleTabChange = (tab: number) => {
navigate(
staffListUrl({
activeTab: tab.toString(),
...getFilterTabs()[tab - 1].data,
}),
);
};
const handleTabDelete = () => {
deleteFilterTab(currentTab);
navigate(staffListUrl());
};
const handleTabSave = (data: SaveFilterTabDialogFormData) => {
saveFilterTab(data.name, getActiveFilters(params));
handleTabChange(tabs.length + 1);
};
const {
loadMore: loadMorePermissionGroups,
search: searchPermissionGroups,
@ -171,55 +158,65 @@ export const StaffList: React.FC<StaffListProps> = ({ params }) => {
return (
<PaginatorContext.Provider value={paginationValues}>
<StaffListPage
currentTab={currentTab}
filterOpts={getFilterOpts(params)}
initialSearch={params.query || ""}
onSearchChange={handleSearchChange}
onFilterChange={changeFilters}
onAll={resetFilters}
onTabChange={handleTabChange}
onTabDelete={() => openModal("delete-search")}
onTabSave={() => openModal("save-search")}
tabs={tabs.map(tab => tab.name)}
onFilterPresetsAll={resetFilters}
onFilterPresetDelete={id => {
setPresetIdToDelete(id);
openModal("delete-search");
}}
selectedFilterPreset={selectedPreset}
onFilterPresetChange={onPresetChange}
onFilterPresetUpdate={onPresetUpdate}
hasPresetsChanged={hasPresetsChanged}
onFilterPresetPresetSave={() => openModal("save-search")}
filterPresets={presets.map(preset => preset.name)}
disabled={loading || addStaffMemberData.loading || limitOpts.loading}
limits={limitOpts.data?.shop.limits}
limits={limitOpts.data?.shop?.limits}
settings={settings}
sort={getSortParams(params)}
staffMembers={mapEdgesToItems(staffQueryData?.staffUsers)}
staffMembers={mapEdgesToItems(staffQueryData?.staffUsers) ?? []}
onAdd={() => openModal("add")}
onUpdateListSettings={updateListSettings}
onSort={handleSort}
/>
<StaffAddMemberDialog
availablePermissionGroups={mapEdgesToItems(
searchPermissionGroupsOpts?.data?.search,
)}
availablePermissionGroups={
mapEdgesToItems(searchPermissionGroupsOpts?.data?.search) ?? []
}
confirmButtonState={addStaffMemberData.status}
initialSearch=""
disabled={loading}
errors={addStaffMemberData.data?.staffCreate.errors || []}
errors={addStaffMemberData.data?.staffCreate?.errors || []}
open={params.action === "add"}
onClose={closeModal}
onConfirm={handleStaffMemberAdd}
fetchMorePermissionGroups={{
hasMore: searchPermissionGroupsOpts.data?.search.pageInfo.hasNextPage,
hasMore:
searchPermissionGroupsOpts.data?.search?.pageInfo?.hasNextPage ??
false,
loading: searchPermissionGroupsOpts.loading,
onFetchMore: loadMorePermissionGroups,
}}
onSearchChange={searchPermissionGroups}
/>
<SaveFilterTabDialog
open={params.action === "save-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabSave}
onSubmit={onPresetSave}
/>
<DeleteFilterTabDialog
open={params.action === "delete-search"}
confirmButtonState="default"
onClose={closeModal}
onSubmit={handleTabDelete}
tabName={getStringOrPlaceholder(tabs[currentTab - 1]?.name)}
onSubmit={onPresetDelete}
tabName={getPresetNameToDelete()}
/>
</PaginatorContext.Provider>
);

View file

@ -1,10 +1,9 @@
// @ts-strict-ignore
import {
FilterElement,
FilterElementRegular,
} from "@dashboard/components/Filter";
import { StaffMemberStatus, StaffUserInput } from "@dashboard/graphql";
import { findValueInEnum, maybe } from "@dashboard/misc";
import { findValueInEnum } from "@dashboard/misc";
import {
StaffFilterKeys,
StaffListFilterOpts,
@ -28,8 +27,10 @@ export function getFilterOpts(
): StaffListFilterOpts {
return {
status: {
active: maybe(() => params.status !== undefined, false),
value: maybe(() => findValueInEnum(params.status, StaffMemberStatus)),
active: params?.status !== undefined ?? false,
value: params?.status
? findValueInEnum(params.status, StaffMemberStatus)
: null,
},
};
}
@ -60,8 +61,7 @@ export function getFilterQueryParam(
}
}
export const { deleteFilterTab, getFilterTabs, saveFilterTab } =
createFilterTabUtils<StaffListUrlFilters>(STAFF_FILTERS_KEY);
export const storageUtils = createFilterTabUtils<string>(STAFF_FILTERS_KEY);
export const { areFiltersApplied, getActiveFilters, getFiltersCurrentTab } =
createFilterUtils<StaffListUrlQueryParams, StaffListUrlFilters>(

View file

@ -1,9 +1,10 @@
// @ts-strict-ignore
import { UserSortField } from "@dashboard/graphql";
import { StaffListUrlSortField } from "@dashboard/staff/urls";
import { createGetSortQueryVariables } from "@dashboard/utils/sort";
export function getSortQueryField(sort: StaffListUrlSortField): UserSortField {
export function getSortQueryField(
sort: StaffListUrlSortField,
): UserSortField | undefined {
switch (sort) {
case StaffListUrlSortField.name:
return UserSortField.LAST_NAME;

View file

@ -68,7 +68,7 @@ interface SortingInput<T extends string> {
}
type GetSortQueryField<TUrlField extends string, TSortField extends string> = (
sort: TUrlField,
) => TSortField;
) => TSortField | undefined;
type GetSortQueryVariables<
TSortField extends string,
TParams extends Record<any, any>,