Skip to content

Commit

Permalink
feat: add DevTools components
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-anderson committed Feb 20, 2024
1 parent ff08ac5 commit b1d60c5
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 0 deletions.
107 changes: 107 additions & 0 deletions src/components/DevTools/CacheManagerDevTool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import FormLabel from "@mui/material/FormLabel";
import Stack from "@mui/material/Stack";
import { apolloClient } from "@/app/ApolloProvider/apolloClient";
import { ActionsButtonGroup } from "@/components/Buttons/ActionsButtonGroup";
import { QUERIES } from "@/graphql/queries";
import { MOCK_WORK_ORDERS, MOCK_INVOICES, MOCK_CONTACTS } from "@/tests/mockItems";
import { DevToolContainer } from "./DevToolContainer";

export const CacheManagerDevTool = ({ handleCloseModal }: { handleCloseModal: () => void }) => {
// Shared onClick handler for the ActionsButtonGroup options:
const handleManageCache = async ({ target, action }: HandleManageCacheParams) => {
if (action === "Write") {
if (target !== "ALL") {
apolloClient.writeQuery(MOCK_CACHE_CONFIGS[target].WRITE_QUERY_ARGS as any);
} else {
// Write all queries to cache
apolloClient.writeQuery(MOCK_CACHE_CONFIGS.WorkOrders.WRITE_QUERY_ARGS);
apolloClient.writeQuery(MOCK_CACHE_CONFIGS.Invoices.WRITE_QUERY_ARGS);
apolloClient.writeQuery(MOCK_CACHE_CONFIGS.Contacts.WRITE_QUERY_ARGS);
}
} else if (action === "Clear") {
// id defaults to ROOT_QUERY; return DELETE sentinel object to remove the "field"
apolloClient.cache.modify({
broadcast: true,
fields:
target !== "ALL"
? { [MOCK_CACHE_CONFIGS[target].ROOT_QUERY_FIELD_NAME]: (_, { DELETE }) => DELETE }
: {
[MOCK_CACHE_CONFIGS.WorkOrders.ROOT_QUERY_FIELD_NAME]: (_, { DELETE }) => DELETE,
[MOCK_CACHE_CONFIGS.Invoices.ROOT_QUERY_FIELD_NAME]: (_, { DELETE }) => DELETE,
[MOCK_CACHE_CONFIGS.Contacts.ROOT_QUERY_FIELD_NAME]: (_, { DELETE }) => DELETE,
},
});
// Run garbage collection to remove the single-item cache refs
apolloClient.cache.gc();
await apolloClient.refetchQueries({ include: "all" });
}
handleCloseModal();
};

return (
<DevToolContainer
title="Manage Mocked Items in Apollo Cache"
subtitle="Note: Mocked items are not persisted on the back-end."
>
{HANDLE_MANAGE_CACHE_ACTIONS.map((action) => (
<Stack key={action} style={{ width: "10rem" }}>
<FormLabel style={{ display: "block" }}>{action} Mocks</FormLabel>
<ActionsButtonGroup
options={HANDLE_MANAGE_CACHE_TARGETS.map((target) => ({
label: target,
handleClick: () => handleManageCache({ action, target }),
isPrimary: target === "ALL",
}))}
/>
</Stack>
))}
</DevToolContainer>
);
};

const MOCK_CACHE_CONFIGS = {
WorkOrders: {
ROOT_QUERY_FIELD_NAME: "myWorkOrders",
WRITE_QUERY_ARGS: {
query: QUERIES.MY_WORK_ORDERS,
data: MOCK_WORK_ORDERS,
},
},
Invoices: {
ROOT_QUERY_FIELD_NAME: "myInvoices",
WRITE_QUERY_ARGS: {
query: QUERIES.MY_INVOICES_WITH_WORKORDER_DATA,
data: MOCK_INVOICES,
},
},
Contacts: {
ROOT_QUERY_FIELD_NAME: "myContacts",
WRITE_QUERY_ARGS: {
query: QUERIES.MY_CONTACTS,
data: { myContacts: Object.values(MOCK_CONTACTS) },
},
},
} as const satisfies Record<
string,
{
ROOT_QUERY_FIELD_NAME: string;
WRITE_QUERY_ARGS: {
query: object;
data: Record<string, unknown>;
};
}
>;

const HANDLE_MANAGE_CACHE_ACTIONS = ["Write", "Clear"] as const;

const HANDLE_MANAGE_CACHE_TARGETS = [
"ALL",
...(Object.keys(MOCK_CACHE_CONFIGS) as Array<keyof typeof MOCK_CACHE_CONFIGS>),
] as const;

export type CacheManagerOperationType = (typeof HANDLE_MANAGE_CACHE_ACTIONS)[number];
export type CacheManagerTarget = (typeof HANDLE_MANAGE_CACHE_TARGETS)[number];
export type HandleManageCacheParams = {
action: CacheManagerOperationType;
target: CacheManagerTarget;
};
40 changes: 40 additions & 0 deletions src/components/DevTools/DevToolContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Box from "@mui/material/Box";
import Text, { typographyClasses } from "@mui/material/Typography";

export const DevToolContainer = ({ title, subtitle, children }: DevToolContainerProps) => (
<Box
sx={({ palette }) => ({
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
borderTop: `1px solid ${palette.divider}`,
padding: "1rem",
[`& > .${typographyClasses.body2}`]: {
textAlign: "center",
lineHeight: 1.4,
opacity: 0.75,
},
})}
>
<Text variant="h6">{title}</Text>
{subtitle && <Text variant="body2">{subtitle}</Text>}
<div
style={{
width: "100%",
display: "flex",
justifyContent: "space-evenly",
gap: "1rem",
marginTop: "1rem",
}}
>
{children}
</div>
</Box>
);

export type DevToolContainerProps = {
title: string;
subtitle?: string;
children: React.ReactNode;
};
97 changes: 97 additions & 0 deletions src/components/DevTools/DevTools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useState } from "react";
import { styled, alpha } from "@mui/material/styles";
import Button from "@mui/material/Button";
import Modal from "@mui/material/Modal";
import Paper from "@mui/material/Paper";
import Text from "@mui/material/Typography";
import { RainbowBorderBox } from "@/components/Containers/RainbowBorderBox";
import { CacheManagerDevTool } from "./CacheManagerDevTool";
import { devToolsElementIDs } from "./elementIDs";

export const DevTools = () => {
const [isModalOpen, setIsModalOpen] = useState(false);

const handleOpenModal = () => setIsModalOpen(true);
const handleCloseModal = () => setIsModalOpen(false);

return (
<>
<RainbowBorderBox>
<Button
onClick={handleOpenModal}
variant="outlined"
size="small"
sx={({ palette, variables }) => {
// Setting bg-color is necessary to ensure the rainbow-bg is not visible on the button
const backgroundColor = variables.isMobilePageLayout
? palette.background.default
: palette.background.paper;
return {
// Ensure the button can be seen in light mode:
...(palette.mode === "light" && { color: palette.text.primary }),
backgroundColor,
"&:hover": { backgroundColor: alpha(backgroundColor, 0.875) },
};
}}
>
Open Dev Tools
</Button>
</RainbowBorderBox>
<Modal open={isModalOpen} onClose={handleCloseModal} aria-label="Fixit Dev Tools">
<StyledPaper id={devToolsElementIDs.modalRoot}>
<Paper id={devToolsElementIDs.modalHeader} elevation={4}>
<Text variant="h4" style={{ fontSize: "1.65rem" }}>
✨ Fixit Dev Tools ✨
</Text>
<Text variant="body2" style={{ textAlign: "center", lineHeight: 1.4, opacity: 0.75 }}>
These tools will help you take Fixit for a spin - have fun!
</Text>
</Paper>
<div id={devToolsElementIDs.modalContent}>
<CacheManagerDevTool handleCloseModal={handleCloseModal} />
</div>
</StyledPaper>
</Modal>
</>
);
};

// Exported as default for React lazy loading
export default DevTools;

const StyledPaper = styled(Paper)(({ theme: { palette } }) => ({
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
minWidth: "clamp(328px, 35vw, 26rem)",
maxWidth: "600px",
border: `3px solid ${palette.divider}`,
borderRadius: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",

// HEADER
[`& > #${devToolsElementIDs.modalHeader}`]: {
width: "100%",
borderRadius: "0.3rem 0.3rem 0 0",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1rem",
padding: "1rem",
borderBottom: `1px solid ${palette.divider}`,
},

// CONTENT
[`& > #${devToolsElementIDs.modalContent}`]: {
flexGrow: 1,
width: "100%",
display: "flex",
gap: "1rem",
flexDirection: "column",
alignItems: "center",
padding: "0 0 0.5rem 0",
},
}));
8 changes: 8 additions & 0 deletions src/components/DevTools/elementIDs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Element IDs for `DevTools` components (src/components/DevTools/).
*/
export const devToolsElementIDs = {
modalRoot: "dev-tools__modal-root",
modalHeader: "dev-tools__modal-header",
modalContent: "dev-tools__modal-content",
} as const;
4 changes: 4 additions & 0 deletions src/components/DevTools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./DevTools";

// re-exported as default for lazy loading (see AppBar):
export { DevTools as default } from "./DevTools";

0 comments on commit b1d60c5

Please sign in to comment.