Skip to content

Commit

Permalink
Merge pull request #22 from bcgov/feature/more-editing-iterative-impr…
Browse files Browse the repository at this point in the history
…ovements

More Editing Improvements
  • Loading branch information
timwekkenbc authored Aug 13, 2024
2 parents a004bc0 + f71bcf3 commit c8dc42a
Show file tree
Hide file tree
Showing 18 changed files with 225 additions and 41 deletions.
18 changes: 15 additions & 3 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ enum ACTION_STATUS {
DELETE = "delete",
}

const PAGE_SIZE = 15;

export default function Admin() {
const [isLoading, setIsLoading] = useState(true);
const [initialRules, setInitialRules] = useState<RuleInfo[]>([]);
const [rules, setRules] = useState<RuleInfo[]>([]);
const [currentPage, setCurrentPage] = useState(0);

const getOrRefreshRuleList = async () => {
// Get rules that are already defined in the DB
Expand All @@ -40,8 +43,8 @@ export default function Admin() {
};

const deleteRule = async (index: number) => {
const newRules = [...rules];
newRules.splice(index, 1);
const deletionIndex = (currentPage - 1) * PAGE_SIZE + index;
const newRules = [...rules.slice(0, deletionIndex), ...rules.slice(deletionIndex + 1, rules.length)];
setRules(newRules);
};

Expand All @@ -67,6 +70,11 @@ export default function Admin() {
return [...updatedEntries, ...deletedEntries];
};

const updateCurrPage = (page: number, pageSize: number) => {
// Keep track of current page so we can delete via splice properly
setCurrentPage(page);
};

// Save all rule updates to the API/DB
const saveAllRuleUpdates = async () => {
setIsLoading(true);
Expand Down Expand Up @@ -147,7 +155,11 @@ export default function Admin() {
{isLoading ? (
<p>Loading...</p>
) : (
<Table columns={columns} dataSource={rules.map((rule, key) => ({ key, ...rule }))} />
<Table
columns={columns}
pagination={{ pageSize: PAGE_SIZE, onChange: updateCurrPage }}
dataSource={rules.map((rule, key) => ({ key, ...rule }))}
/>
)}
</>
);
Expand Down
38 changes: 38 additions & 0 deletions app/components/ErrorBoundary/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";
import { Alert } from "antd";
import React from "react";

interface ErrorBoundaryState {
hasError: boolean;
errorMessage?: string;
}

class ErrorBoundary extends React.Component<{ children: React.ReactNode }, ErrorBoundaryState> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI.
return { hasError: true, errorMessage: error.message };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log to any new error reporting service here
console.error("Uncaught error:", error, errorInfo);
}

render() {
if (this.state.hasError) {
// Fallback UI
return (
<Alert message={<b>Something went wrong.</b>} description={this.state.errorMessage} type="error" showIcon />
);
}

return this.props.children;
}
}

export default ErrorBoundary;
1 change: 1 addition & 0 deletions app/components/ErrorBoundary/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./ErrorBoundary";
13 changes: 6 additions & 7 deletions app/components/RuleHeader/RuleHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";
import { useState, useRef, useEffect } from "react";
import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { usePathname } from "next/navigation";
import { Button, Flex, Tag } from "antd";
import { HomeOutlined, EyeOutlined, EditOutlined, CheckOutlined } from "@ant-design/icons";
import { RuleInfo } from "@/app/types/ruleInfo";
Expand All @@ -16,7 +15,6 @@ export default function RuleHeader({
ruleInfo: RuleInfo;
version?: string;
}) {
const router = useRouter();
const pathname = usePathname();

const [savedTitle, setSavedTitle] = useState("");
Expand Down Expand Up @@ -58,7 +56,8 @@ export default function RuleHeader({
};

const switchVersion = (versionToSwitchTo: string) => {
router.push(`${pathname}?version=${versionToSwitchTo}&_=${new Date().getTime()}`);
// Use window.locaiton.href instead of router.push so that we can detect page changes for "unsaved changes" popup
window.location.href = `${pathname}?version=${versionToSwitchTo}&_=${new Date().getTime()}`;
};

const formatVersionText = (text: string) => {
Expand All @@ -79,9 +78,9 @@ export default function RuleHeader({
<div className={styles.headerContainer} style={{ background: versionColor }}>
<Flex justify="space-between" className={styles.headerWrapper}>
<Flex gap="middle" align="center" flex={isEditingTitle ? "1" : "none"} className={styles.headerContent}>
<Link href="/" className={styles.homeButton}>
<a href="/" className={styles.homeButton}>
<HomeOutlined />
</Link>
</a>
<Flex flex={1} vertical>
<h1
onClick={startEditingTitle}
Expand Down Expand Up @@ -111,7 +110,7 @@ export default function RuleHeader({
<Tag color={versionColor}>{formatVersionText(version)}</Tag>
</Flex>
<Flex gap="small" align="end">
{version !== RULE_VERSION.published && (
{ruleInfo.isPublished && version !== RULE_VERSION.published && (
<Button onClick={() => switchVersion("published")} icon={<EyeOutlined />} type="dashed">
Published
</Button>
Expand Down
4 changes: 0 additions & 4 deletions app/components/RuleManager/RuleManager.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,3 @@
min-height: 500px;
border: 1px solid #d9d9d9;
}

.spinner {
min-height: 500px;
}
18 changes: 15 additions & 3 deletions app/components/RuleManager/RuleManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { postDecision, getRuleMap } from "../../utils/api";
import { RuleInfo } from "@/app/types/ruleInfo";
import { RuleMap } from "@/app/types/rulemap";
import { Scenario } from "@/app/types/scenario";
import useLeaveScreenPopup from "@/app/hooks/useLeaveScreenPopup";
import { DEFAULT_RULE_CONTENT } from "@/app/constants/defaultRuleContent";
import SavePublish from "../SavePublish";
import ScenariosManager from "../ScenariosManager";
Expand Down Expand Up @@ -50,6 +51,14 @@ export default function RuleManager({
const [simulation, setSimulation] = useState<Simulation>();
const [simulationContext, setSimulationContext] = useState<Record<string, any>>();
const [resultsOfSimulation, setResultsOfSimulation] = useState<Record<string, any> | null>();
const { setHasUnsavedChanges } = useLeaveScreenPopup();

const updateRuleContent = (updatedRuleContent: DecisionGraphType) => {
if (ruleContent !== updatedRuleContent) {
setHasUnsavedChanges(true);
setRuleContent(updatedRuleContent);
}
};

useEffect(() => {
setRuleContent(initialRuleContent);
Expand All @@ -70,6 +79,7 @@ export default function RuleManager({
if (canBeSchemaMapped()) {
updateRuleMap();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ruleContent]);

const resetContextAndResults = () => {
Expand Down Expand Up @@ -117,7 +127,7 @@ export default function RuleManager({

if (!ruleContent) {
return (
<Spin tip="Loading graph..." size="large" className={styles.spinner}>
<Spin tip="Loading graph..." size="large" className="spinner">
<div className="content" />
</Spin>
);
Expand All @@ -126,11 +136,13 @@ export default function RuleManager({
return (
<Flex gap="large" vertical>
<div className={styles.rulesWrapper}>
{editing && <SavePublish ruleInfo={ruleInfo} ruleContent={ruleContent} />}
{editing && (
<SavePublish ruleInfo={ruleInfo} ruleContent={ruleContent} setHasSaved={() => setHasUnsavedChanges(false)} />
)}
<RuleViewerEditor
jsonFilename={jsonFile}
ruleContent={ruleContent}
setRuleContent={setRuleContent}
updateRuleContent={updateRuleContent}
contextToSimulate={simulationContext}
setContextToSimulate={setSimulationContext}
simulation={simulation}
Expand Down
8 changes: 4 additions & 4 deletions app/components/RuleViewerEditor/RuleViewerEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getScenariosByFilename } from "../../utils/api";
interface RuleViewerEditorProps {
jsonFilename: string;
ruleContent: DecisionGraphType;
setRuleContent: (updateGraph: DecisionGraphType) => void;
updateRuleContent: (updateGraph: DecisionGraphType) => void;
contextToSimulate?: Record<string, any> | null;
setContextToSimulate: (results: Record<string, any>) => void;
simulation?: Simulation;
Expand All @@ -31,7 +31,7 @@ interface RuleViewerEditorProps {
export default function RuleViewerEditor({
jsonFilename,
ruleContent,
setRuleContent,
updateRuleContent,
contextToSimulate,
setContextToSimulate,
simulation,
Expand Down Expand Up @@ -132,7 +132,7 @@ export default function RuleViewerEditor({
id={id}
isSelected={selected}
name={data?.name}
isEditable={false}
isEditable={isEditable}
/>
),
},
Expand Down Expand Up @@ -170,7 +170,7 @@ export default function RuleViewerEditor({
onReactFlowInit={reactFlowInit}
panels={panels}
components={additionalComponents}
onChange={(updatedGraphValue) => setRuleContent(updatedGraphValue)}
onChange={(updatedGraphValue) => updateRuleContent(updatedGraphValue)}
disabled={!isEditable}
/>
</JdmConfigProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export default function LinkRuleComponent({ specification, id, isSelected, name,
}, [openRuleDrawer]);

useEffect(() => {
updateRuleContent(goRulesJSONFilename);
}, []);
if (goRulesJSONFilename) {
updateRuleContent(goRulesJSONFilename);
}
}, [goRulesJSONFilename]);

const showRuleDrawer = () => {
setOpenRuleDrawer(true);
Expand Down Expand Up @@ -101,7 +103,7 @@ export default function LinkRuleComponent({ specification, id, isSelected, name,
)}
</>
) : (
<Spin tip="Loading rules..." size="large" className={styles.spinner}>
<Spin tip="Loading rules..." size="large" className="spinner">
<div className="content" />
</Spin>
)}
Expand Down
10 changes: 7 additions & 3 deletions app/components/SavePublish/SavePublish.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Modal, Button, Flex, message } from "antd";
import { Modal, Button, Flex, message, App } from "antd";
import { SaveOutlined, UploadOutlined } from "@ant-design/icons";
import { RuleInfo } from "@/app/types/ruleInfo";
import { updateRuleData } from "@/app/utils/api";
Expand All @@ -10,9 +10,10 @@ import styles from "./SavePublish.module.css";
interface SavePublishProps {
ruleInfo: RuleInfo;
ruleContent: object;
setHasSaved: () => void;
}

export default function SavePublish({ ruleInfo, ruleContent }: SavePublishProps) {
export default function SavePublish({ ruleInfo, ruleContent, setHasSaved }: SavePublishProps) {
const { _id: ruleId, goRulesJSONFilename: filePath, reviewBranch } = ruleInfo;

const [openNewReviewModal, setOpenNewReviewModal] = useState(false);
Expand All @@ -29,13 +30,14 @@ export default function SavePublish({ ruleInfo, ruleContent }: SavePublishProps)
message.error("Failed to save draft");
}
setIsSaving(false);
setHasSaved();
};

const createOrUpdateReview = async (newReviewBranch?: string, reviewDescription: string = "") => {
setIsSaving(true);
setIsSendingToReview(true);
// Save before sending to review
await updateRuleData(ruleId, { ruleDraft: { content: ruleContent } });
await updateRuleData(ruleId, { ruleDraft: { content: ruleContent, reviewBranch: "test" } });
// Prompt for new review branch details if they don't exist
const branch = currReviewBranch || newReviewBranch;
if (!branch) {
Expand All @@ -54,8 +56,10 @@ export default function SavePublish({ ruleInfo, ruleContent }: SavePublishProps)
console.error("Unable to update/create review");
message.error("Unable to update/create review");
}

setIsSaving(false);
setIsSendingToReview(false);
setHasSaved();
};

const createNewReview = ({
Expand Down
24 changes: 24 additions & 0 deletions app/hooks/useLeaveScreenPopup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useState, useEffect } from "react";

export default function useLeaveScreenPopup() {
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

useEffect(() => {
const handleBeforeUnload = (event: { preventDefault: () => void; returnValue: string }) => {
if (hasUnsavedChanges) {
// Standard way to trigger the confirmation dialog
event.preventDefault();
// Chrome requires returnValue to be set
event.returnValue = "";
}
};
// Attach the event listener
window.addEventListener("beforeunload", handleBeforeUnload);
// Cleanup function to remove the event listener
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [hasUnsavedChanges]);

return { setHasUnsavedChanges };
}
9 changes: 7 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { AntdRegistry } from "@ant-design/nextjs-registry";
import { ConfigProvider } from "antd";
import { App, ConfigProvider } from "antd";
import theme from "./styles/themeConfig";
import ErrorBoundary from "./components/ErrorBoundary";
import "./styles/globals.css";
import styles from "./styles/layout.module.css";

Expand All @@ -19,7 +20,11 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
<body className={inter.className}>
<AntdRegistry>
<ConfigProvider theme={theme}>
<div className={styles.layoutWrapper}>{children}</div>
<ErrorBoundary>
<App>
<div className={styles.layoutWrapper}>{children}</div>
</App>
</ErrorBoundary>
</ConfigProvider>
</AntdRegistry>
</body>
Expand Down
Loading

0 comments on commit c8dc42a

Please sign in to comment.