Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert React components to functions #581

Open
wants to merge 85 commits into
base: main
Choose a base branch
from

Conversation

ilong4rennes
Copy link

@ilong4rennes ilong4rennes commented Dec 6, 2024

What:

  • Converted React class components to function components for 48 out of the 49 files are listed in issue Convert React components to functions #571. Updates include replacing class-based lifecycle methods with React hooks and ensuring the components maintain the same behavior as before.
  • The remaining file, admin/src/Models/CollectionNavigation.js, contains a CollectionNavigation class that serves as a base class for multiple other components. To maintain consistency and avoid introducing incomplete changes, we opted to leave this class unchanged for now. After investigating related dependencies, a new issue has been created to address this component separately (Refactor CollectionNavigation and Related Components to Function Components #582 ).

Why:

Modernizing the codebase to align with current React best practices. Function components are more concise, easier to read, and enable the use of modern React features like hooks.

Testing:

  • ESLint passes during make test
  • Manual Testing UI: Verified the UI functionality of all updated components. Screenshots of the results are included in the related PRs, available at this link.

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced new functional components for various forms and inputs, enhancing performance and maintainability.
    • Added context management for group and member data, improving state sharing across components.
  • Bug Fixes

    • Streamlined state management and subscription handling across multiple components, ensuring better performance and responsiveness.
  • Refactor

    • Refactored multiple components from class-based to functional components using React hooks, simplifying code structure and enhancing readability.
    • Updated component signatures and export statements to align with the new functional component structure.

These changes improve the overall user experience by making the application more responsive and easier to maintain.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 55

🧹 Outside diff range and nitpick comments (60)
admin/src/Components/RequestPasswordReset.jsx (1)

Line range hint 13-27: Enhance form validation and maintain language consistency

The success message is in English while the form UI is in Swedish. Consider:

  1. Moving the validation message to Swedish for consistency
  2. Adding email format validation when applicable

Here's a suggested improvement:

 if (!user_identification) {
-    showError("You need to fill your email or member number.");
+    showError("Du måste fylla i din e-post eller ditt medlemsnummer.");
     return;
 }

 auth.requestPasswordReset(user_identification).then(() => {
     showSuccess(
-        "Link to password reset will be sent to your email shortly.",
+        "En länk för att återställa lösenordet kommer att skickas till din e-post.",
     );
     browserHistory.push("/");
 });
admin/src/Membership/KeyEdit.jsx (5)

1-9: LGTM! Consider grouping related imports

The imports are comprehensive and include all necessary dependencies. Consider grouping them into:

  1. React and routing
  2. Components
  3. Models and utilities
 import React, { useCallback, useEffect, useRef, useState } from "react";
 import { useParams, withRouter } from "react-router";
+
 import DateTimeInput from "../Components/DateTimeInput";
 import TextInput from "../Components/TextInput";
 import Textarea from "../Components/Textarea";
+
 import Key from "../Models/Key";
 import { browserHistory } from "../browser_history";
 import { confirmModal } from "../message";

10-13: Consider memoizing the Key model instance

The Key model instance is created on every render. Consider using useMemo to optimize this:

-    const keyRef = useRef(Key.get(key_id));
+    const keyRef = useRef(React.useMemo(() => Key.get(key_id), [key_id]));

15-22: Add keyRef to effect dependencies

The effect depends on keyRef but it's not included in the dependency array. While it's unlikely to cause issues since useRef is stable, it's best practice to include it:

-    }, []);
+    }, [keyRef]);

79-79: Fix typo in placeholder text

There's a typo in the placeholder text:

-                                placeholder="Det är valfritt att lägga in en kommenter av nyckeln"
+                                placeholder="Det är valfritt att lägga in en kommentar av nyckeln"

84-90: Add ARIA label to delete button

The delete button using an anchor tag should have proper accessibility attributes:

     <a
         className="uk-button uk-button-danger uk-float-left"
         onClick={onDelete}
+        role="button"
+        aria-label="Ta bort nyckel"
     >
admin/src/Membership/MemberBoxSpans.jsx (2)

15-22: Consider memoizing the collection initialization

The collection is being initialized on every render. Consider using useMemo to optimize performance.

-const collection = new Collection({
+const collection = React.useMemo(() => new Collection({
    type: Span,
    url: `/membership/member/${props.match.params.member_id}/spans`,
    pageSize: 0,
    includeDeleted: true,
-});
+}), [props.match.params.member_id]);

54-60: Modernize promise handling in deleteItem

Consider using async/await for better readability and error handling.

-const deleteItem = (item) =>
-    confirmModal(item.deleteConfirmMessage())
-        .then(() => item.del())
-        .then(
-            () => collection.fetch(),
-            () => null,
-        );
+const deleteItem = async (item) => {
+    try {
+        await confirmModal(item.deleteConfirmMessage());
+        await item.del();
+        await collection.fetch();
+    } catch (error) {
+        console.error('Failed to delete item:', error);
+    }
+};
admin/src/Membership/MemberBoxPermissions.js (2)

6-10: Add PropTypes validation for the Row component

Consider adding PropTypes to validate the item prop structure, which will improve component documentation and runtime type checking during development.

import React from "react";
+import PropTypes from 'prop-types';
import CollectionTable from "../Components/CollectionTable";
import Collection from "../Models/Collection";
import Permission from "../Models/Permission";

const Row = ({ item }) => (
    <tr>
        <td>{item.permission}</td>
    </tr>
);

+Row.propTypes = {
+    item: PropTypes.shape({
+        permission: PropTypes.string.isRequired
+    }).isRequired
+};

20-30: Consider adding loading and error states

The component could benefit from handling loading and error states for a better user experience.

 function MemberBoxPermissions(props) {
     const member_id = props.match.params.member_id;
+    const [isLoading, setIsLoading] = useState(true);
+    const [error, setError] = useState(null);
     
     const columns = [{ title: "Behörighet" }];
     return (
         <div className="uk-margin-top">
+            {error && <div className="uk-alert uk-alert-danger">{error}</div>}
+            {isLoading && <div className="uk-spinner" />}
             <CollectionTable
                 emptyMessage="Medlemmen har inga behörigheter"
                 rowComponent={Row}
                 collection={collection}
                 columns={columns}
+                onError={setError}
+                onLoadingChange={setIsLoading}
             />
         </div>
     );
 }
admin/src/Membership/MemberExport.jsx (2)

1-6: LGTM! Consider more descriptive state variable names.

The conversion to a function component and hooks implementation looks good. However, consider renaming the generic state variable to something more specific like exportStatus for better clarity.

-    const [state, setState] = useState("none");
+    const [exportStatus, setExportStatus] = useState("none");

Line range hint 8-48: Consider several improvements to the export functionality.

The export logic works but could benefit from the following enhancements:

  1. Add error handling:
 get({ url: "/membership/member/all_with_membership" }).then((data) => {
     // ... existing code ...
+}).catch((error) => {
+    setExportStatus("error");
+    console.error("Failed to export members:", error);
 });
  1. Extract CSV generation to a utility function:
+const formatMembersAsCsv = (members) => {
+    const headers = ["Member Number", "First Name", "Last Name", "Email", 
+                    "Labaccess End Date", "Membership End Date"];
+    const rows = [headers.join(",")];
+    
+    return rows.concat(members.map(member => [
+        member.member_number,
+        member.firstname,
+        member.lastname,
+        member.email,
+        member.membership.labaccess_end || "",
+        member.membership.membership_end || ""
+    ].join(","))).join("\n");
+};
  1. Consider using a more robust loading state enum:
-const [exportStatus, setExportStatus] = useState("none");
+const [exportStatus, setExportStatus] = useState("idle");
+// Valid states: "idle" | "loading" | "success" | "error"
admin/src/Sales/GiftCardShow.js (2)

16-22: Consider memoizing the Collection instance

The gift_cardRows Collection instance is recreated on every render. Consider using useMemo to optimize performance.

-    const gift_cardRows = new Collection({
+    const gift_cardRows = React.useMemo(() => new Collection({
         type: GiftCardRow,
         url: `/webshop/gift-card/${id}/products`,
         pageSize: 0,
         expand: "product",
-    });
+    }), [id]);

66-74: Simplify currency calculation

Consider extracting the price calculation logic to improve readability and maintainability.

+    const calculateUnitPrice = (amount, quantity) => (100 * amount) / quantity;
+
     // In the render:
     <td className="uk-text-right">
-        <Currency
-            value={
-                (100 * item.amount) /
-                item.product_quantity
-            }
-        />{" "}
+        <Currency value={calculateUnitPrice(item.amount, item.product_quantity)} />{" "}
         kr
     </td>
admin/src/Components/GroupForm.js (3)

8-18: Consider enhancing subscription error handling.

While the subscription logic is correctly implemented with proper cleanup, it could benefit from additional error handling.

Consider this enhancement:

 useEffect(() => {
+    if (!group?.subscribe) {
+        console.warn('Group object is missing or does not support subscriptions');
+        return;
+    }
     const unsubscribe = group.subscribe(() => {
+        try {
             setSaveDisabled(!group.canSave());
+        } catch (error) {
+            console.error('Error updating save button state:', error);
+            setSaveDisabled(true);
+        }
     });

     return () => {
         unsubscribe();
     };
 }, [group]);

21-21: Replace non-descriptive className.

The className "meep" doesn't follow naming conventions and lacks meaning.

-<div className="meep">
+<div className="group-form-container">

24-28: Extract form submission handler for better maintainability.

Consider extracting the inline form handler to a named function for better readability and testability.

+const handleSubmit = (e) => {
+    e.preventDefault();
+    onSave();
+};

 <form
     className="uk-form uk-margin-bottom"
-    onSubmit={(e) => {
-        e.preventDefault();
-        onSave();
-    }}
+    onSubmit={handleSubmit}
 >
admin/src/Membership/MemberBoxOrders.js (3)

Line range hint 8-21: Consider adding PropTypes and explicit prop destructuring

While the component looks good, consider these improvements for better maintainability and type safety:

+import PropTypes from 'prop-types';
+
-const Row = ({ item }) => {
+const Row = ({ item: { id, created_at, status, amount } }) => {
     return (
         <tr>
             <td>
-                <Link to={`/sales/order/${item.id}`}>{item.id}</Link>
+                <Link to={`/sales/order/${id}`}>{id}</Link>
             </td>
             <td>
-                <DateTimeShow date={item.created_at} />
+                <DateTimeShow date={created_at} />
             </td>
-            <td>{item.status}</td>
-            <td className="uk-text-right">{item.amount} kr</td>
+            <td>{status}</td>
+            <td className="uk-text-right">{amount} kr</td>
         </tr>
     );
 };
+
+Row.propTypes = {
+    item: PropTypes.shape({
+        id: PropTypes.number.isRequired,
+        created_at: PropTypes.string.isRequired,
+        status: PropTypes.string.isRequired,
+        amount: PropTypes.number.isRequired,
+    }).isRequired,
+};

30-35: Move columns definition outside component

The columns array is static and should be defined outside the component to prevent recreations on each render.

+const COLUMNS = [
+    { title: "Order" },
+    { title: "Skapad" },
+    { title: "Status" },
+    { title: "Belopp" },
+];
+
 function MemberBoxOrders(props) {
     // ...
-    const columns = [
-        { title: "Order" },
-        { title: "Skapad" },
-        { title: "Status" },
-        { title: "Belopp" },
-    ];

39-44: Add error boundary and loading state handling

The CollectionTable usage could benefit from explicit error and loading state handling.

     <CollectionTable
         emptyMessage="Ingar ordrar"
         rowComponent={Row}
         collection={collection}
-        columns={columns}
+        columns={COLUMNS}
+        errorMessage="Failed to load orders"
+        loadingMessage="Loading orders..."
     />
admin/src/Sales/ProductEdit.jsx (3)

11-17: Consider adding TypeScript or PropTypes for better maintainability.

The component setup is clean and follows React best practices. However, adding type annotations would improve code maintainability and catch potential issues earlier.

-const ProductEdit = () => {
+const ProductEdit: React.FC = () => {

47-48: Enhance loading state feedback.

Consider providing more informative loading feedback to improve user experience.

-    if (!product) {
-        return <div>Loading...</div>;
+    if (!product) {
+        return (
+            <div className="loading-container">
+                <span className="loading-spinner" />
+                <p>Loading product details...</p>
+            </div>
+        );
     }

53-53: Consider implementing internationalization.

The hardcoded Swedish text "Redigera produkt" should be moved to a translation system for better maintainability and future localization support.

admin/src/Components/Login.jsx (2)

10-21: Consider enhancing form validation and error handling

While the basic validation works, consider these improvements:

  1. Add email format validation for username when it's an email
  2. Add password strength validation
  3. Handle authentication errors from auth.login

Here's a suggested implementation:

 const login = (e) => {
     e.preventDefault();
     const username = usernameRef.current.value;
     const password = passwordRef.current.value;

     if (!username || !password) {
         showError("Du måste fylla i email/medlemsnummer och lösenord");
         return;
     }

-    auth.login(username, password);
+    // Add email validation when username contains @
+    if (username.includes('@') && !isValidEmail(username)) {
+        showError("Ogiltig e-postadress");
+        return;
+    }
+
+    // Add password validation
+    if (password.length < 8) {
+        showError("Lösenordet måste vara minst 8 tecken långt");
+        return;
+    }
+
+    // Handle authentication errors
+    auth.login(username, password).catch(error => {
+        showError("Inloggningen misslyckades: " + error.message);
+    });
 };

23-83: Enhance form accessibility

The form has good basic accessibility, but could be improved with:

  1. ARIA labels
  2. Error message announcements
  3. Loading state indication during authentication

Here's a suggested implementation:

 <form
     className="uk-panel uk-panel-box uk-form"
     onSubmit={login}
+    aria-label="Login form"
 >
     <div className="uk-form-row">
-        <h2>Logga in</h2>
+        <h2 id="login-heading">Logga in</h2>
     </div>
     <div className="uk-form-row">
         <div className="uk-form-icon">
             <i className="uk-icon-user" />
             <input
                 ref={usernameRef}
                 className="uk-form-large uk-form-width-large"
                 type="text"
                 placeholder="Email/Medlemsnummer"
                 autoComplete="username"
+                aria-label="Email eller medlemsnummer"
+                aria-required="true"
             />
         </div>
     </div>
admin/src/Sales/ProductAdd.js (2)

9-13: Consider using useMemo for product instantiation

The Product instance is recreated on every render. While this might not impact performance significantly, it's a good practice to memoize object instantiations.

 const ProductAdd = () => {
-    const product = new Product();
+    const product = React.useMemo(() => new Product(), []);
     const history = useHistory();

20-26: Consider UI/UX improvements

A few suggestions to enhance the component:

  1. Consider internationalizing the "Skapa produkt" text
  2. Add loading indication during save operation
  3. Add PropTypes validation for ProductForm props
+import PropTypes from 'prop-types';
 import React from "react";
 
 const ProductAdd = () => {
     // ... existing code ...
 
     return (
         <div>
             <h2>Skapa produkt</h2>
-            <ProductForm product={product} onSave={handleSave} />
+            <ProductForm 
+                product={product} 
+                onSave={handleSave}
+                disabled={isLoading} 
+            />
         </div>
     );
 };
 
+ProductForm.propTypes = {
+    product: PropTypes.instanceOf(Product).isRequired,
+    onSave: PropTypes.func.isRequired,
+    disabled: PropTypes.bool,
+};
admin/src/Membership/MemberBoxNewMessage.js (1)

1-3: Consider modernizing the routing implementation

The component mixes older routing patterns (withRouter) with modern hooks (useParams). Consider fully migrating to the modern React Router hooks API for consistency.

-import { withRouter } from "react-router";
-import { useParams } from "react-router-dom";
+import { useParams, useNavigate } from "react-router-dom";

-export default withRouter(MemberBoxMessages);
+export default MemberBoxMessages;
admin/src/Membership/MembershipPeriodsInput.js (4)

74-82: Handle potential errors in Promise.all

If any of the promises in Promise.all(promises) rejects, the .then block won't execute, and errors may not be handled properly. Consider adding a .catch block to handle potential errors and provide feedback to the user.

Apply this diff to handle errors:

 Promise.all(promises)
     .then(() => {
         props.spans.fetch();

         post({
             url: `/webshop/member/${props.member_id}/ship_labaccess_orders`,
             expectedDataStatus: "ok",
         });
+    })
+    .catch((error) => {
+        // Handle error, e.g., display an error message to the user
+        console.error("Error saving spans:", error);
+    });

85-91: Unnecessary return false in onSubmit handler

After calling e.preventDefault(), returning false is unnecessary in React's onSubmit handler. This return statement can be safely removed to clean up the code.

Apply this diff to simplify the onSubmit handler:

 <form
     className="uk-form"
     onSubmit={(e) => {
         e.preventDefault();
         onSave();
-        return false;
     }}
 >

93-102: Accessibility improvement for form controls

Ensure that form controls have associated labels for better accessibility. The checkbox input has a label, which is good, but consider adding aria-labelledby or ensuring that the label properly associates with the input.

Verify that the label's htmlFor matches the id of the input:

 <label className="uk-label" htmlFor="showHistoric">
     Visa historiska
 </label>
 <input
     id="showHistoric"
     className="uk-checkbox"
     type="checkbox"
     checked={showHistoric}
     onChange={(e) => setShowHistoric(e.target.checked)}
 />

This ensures that screen readers can correctly associate the label with the checkbox.


111-115: Button should have a type attribute

The <button> element should have an explicit type attribute. Without it, the button defaults to "submit", which is appropriate here, but it's good practice to specify it explicitly for clarity.

Apply this diff to add the type attribute:

 <button
     disabled={saveDisabled}
     className="uk-button uk-button-success uk-float-right"
+    type="submit"
 >
     <i className="uk-icon-save" /> Spara
 </button>
admin/src/Components/ProductForm.jsx (2)

Line range hint 65-91: Include handleRemoveAction in the dependency array of useCallback

The renderAction function uses handleRemoveAction, but it's not included in the dependency array of useCallback. This could lead to stale closures if handleRemoveAction changes.

Apply this diff to include the dependency:

             [],
+            [handleRemoveAction],
         );

30-51: Adjust useEffect dependencies to prevent unnecessary re-renders

Including selectedActionType in the dependency array of useEffect may cause handleProductChange to run more often than necessary, leading to potential performance issues or unintended behavior. Since selectedActionType is updated inside handleProductChange, it's better to exclude it from the dependencies.

Apply this diff to adjust the dependencies:

-        }, [product, selectedActionType]);
+        }, [product]);
🧰 Tools
🪛 Biome (1.9.4)

[error] 35-35: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

admin/src/Components/FileInput.jsx (2)

128-141: Note: Progress bar functionality is commented out

The code responsible for displaying the progress bar during file uploads is commented out. If progress feedback is still required, consider re-implementing it using state variables and functional component patterns.


8-64: Suggestion: Remove excessive commented-out code

Multiple blocks of code are commented out throughout the component. This can make the code harder to read and maintain. Consider removing unused code or, if you plan to reference it later, moving it to documentation or a separate file.

Also applies to: 66-89, 95-100, 146-173

admin/src/Membership/MemberBoxKeys.js (5)

Line range hint 28-31: Replace <a> tag with <button> for better accessibility

Using an <a> tag without an href attribute and attaching an onClick handler can cause accessibility issues. It's recommended to use a <button> element for actions that trigger JavaScript functions.

Apply this change:

-<a onClick={deleteKey} className="removebutton">
+<button type="button" onClick={deleteKey} className="removebutton">
    <i className="uk-icon-trash" />
-</a>
+</button>

Line range hint 9-17: Add error handling for item.del() in deleteKey function

The deleteKey function currently does not handle errors that may occur during item.del(). If deletion fails, the user may not be aware of the issue. Please add error handling to inform the user.

Apply this change to include error handling:

 const deleteKey = () => {
     return confirmModal(item.deleteConfirmMessage())
         .then(() => item.del())
         .then(() => {
             collection.fetch();
         })
+        .catch((error) => {
+            // Handle the error, e.g., show a message to the user
+            console.error("Error deleting key:", error);
+            // Optionally, display an error message to the user
+        });
 };

110-115: Add type="submit" to the form button

Explicitly setting the button type to "submit" improves form behavior and accessibility.

Apply this change:

 <button
     className="uk-button uk-button-success uk-float-right"
     disabled={!saveEnabled}
+    type="submit"
 >
     <i className="uk-icon-save" /> Skapa nyckel
 </button>

83-83: Rename or remove the className "meep"

The className "meep" may be a placeholder or a leftover from development. Consider renaming it to a more descriptive name or removing it if it's not needed.


127-127: Remove unnecessary member_id argument in Row invocation

The Row component only accepts collection as an argument, but member_id is also being passed. Since member_id is not used within Row, consider removing it to avoid confusion.

Apply this change:

- rowComponent={Row(collectionRef.current, member_id)}
+ rowComponent={Row(collectionRef.current)}
admin/src/Components/CollectionTable.js (1)

Line range hint 94-99: Replace _.range with native JavaScript methods

Using native JavaScript methods can reduce dependencies and improve performance. Replace _.range with Array.from to generate the sequence of page numbers.

Apply this diff to use native methods:

-{_.range(1, page.count + 1).map((i) => {
+{Array.from({ length: page.count }, (_, i) => i + 1).map((i) => {
     const distance = Math.abs(i - page.index);
     if (distance === 0) {
         return (
             <li key={i} className="uk-active">
admin/src/Sales/AccountingBox.jsx (1)

4-4: Add PropTypes validation

Consider adding PropTypes to validate the children prop for better type safety and documentation.

import React from "react";
import { NavItem } from "../nav";
+import PropTypes from 'prop-types';

export default function AccountingBox({ children }) {
    // ... component implementation ...
}

+AccountingBox.propTypes = {
+    children: PropTypes.node
+};
admin/src/Membership/GroupAdd.js (1)

7-27: Consider implementing loading state

Both MemberAdd and GroupAdd components could benefit from a loading state during save operations.

Consider implementing a common pattern for handling loading states during save operations across all form components. This could involve:

  1. Adding a loading state using useState
  2. Disabling the form during save
  3. Showing a loading indicator
  4. Implementing a common error handling pattern

Would you like me to provide an example implementation?

admin/src/Membership/SpanShow.jsx (1)

22-27: Add type checking for data values

The component should handle potential undefined or null values when displaying data.

 {_.keys(data).map((key) => (
     <div key={key}>
         <dt>{key}:</dt>
-        <dd>{data[key]}</dd>
+        <dd>{data[key] ?? 'N/A'}</dd>
     </div>
 ))}
admin/src/Membership/MemberBoxMemberData.jsx (1)

31-31: Consider using useNavigate instead of withRouter

Since this is a functional component, consider using the useNavigate hook instead of the withRouter HOC for better consistency with modern React patterns.

-import { withRouter } from "react-router";
+import { useNavigate } from "react-router-dom";

 function MemberBoxMemberData() {
+    const navigate = useNavigate();
     // ... rest of the component
 }

-export default withRouter(MemberBoxMemberData);
+export default MemberBoxMemberData;
admin/src/Membership/GroupBoxEditInfo.jsx (1)

19-27: Consider memoizing callback functions.

The inline callbacks in the GroupForm props could cause unnecessary re-renders if GroupForm implements shouldComponentUpdate/React.memo.

Consider using useCallback:

+import React, { useContext, useCallback } from "react";
...
+    const handleSave = useCallback(() => group.save(), [group]);
+    const handleDelete = useCallback(() => {
+        confirmModal(group.deleteConfirmMessage())
+            .then(() => group.del())
+            .then(() => {
+                router.push("/membership/groups/");
+            })
+            .catch(() => null);
+    }, [group, router]);

     return (
         <div className="uk-margin-top">
             <GroupForm
                 group={group}
-                onSave={() => group.save()}
-                onDelete={() => {
-                    confirmModal(group.deleteConfirmMessage())
-                        .then(() => group.del())
-                        .then(() => {
-                            router.push("/membership/groups/");
-                        })
-                        .catch(() => null);
-                }}
+                onSave={handleSave}
+                onDelete={handleDelete}
             />
admin/src/Components/SearchBox.jsx (1)

23-23: Consider memoizing the onChange handler.

The inline onChange handler creates a new function on each render.

Consider using useCallback:

+import React, { useRef, useCallback } from "react";

 const SearchBox = ({ value, handleChange }) => {
     const searchRef = useRef(null);
+    const onChange = useCallback((e) => handleChange(e.target.value), [handleChange]);

     return (
         // ...
-                            onChange={(e) => handleChange(e.target.value)}
+                            onChange={onChange}
         // ...
admin/src/Settings/AccessTokenList.jsx (1)

21-35: Extract and memoize the row component.

The inline row component is recreated on every render and could impact performance.

Consider extracting and memoizing:

+const AccessTokenRow = React.memo(({ item }) => (
+    <tr key={item.access_token}>
+        <td>
+            {auth.getAccessToken() === item.access_token ? (
+                <i className="uk-icon-check" />
+            ) : null}
+        </td>
+        <td>{item.access_token}</td>
+        <td>{item.browser}</td>
+        <td>{item.ip}</td>
+        <td>
+            <DateTimeShow date={item.expires} />
+        </td>
+    </tr>
+));

 export default function AccessTokenList() {
     // ...
-            rowComponent={({ item }) => (
-                <tr key={item.access_token}>
-                    // ...
-                </tr>
-            )}
+            rowComponent={AccessTokenRow}
     // ...
admin/src/Components/DateTimeInput.jsx (1)

24-34: Consider making the disabled state configurable

The input is hardcoded as disabled, which might be too restrictive. Consider making it a prop to allow for editable scenarios.

+const DateTimeInput = ({ model, name, title, icon, placeholder, disabled = true }) => {
 const input = (
     <input
         type="text"
         name={name}
         id={name}
-        disabled={true}
+        disabled={disabled}
         value={value}
         placeholder={placeholder}
         className="uk-form-width-large"
     />
 );
admin/src/Components/CheckboxInput.jsx (2)

33-35: Optimize handleChange with useCallback

The handleChange function should be memoized to prevent unnecessary rerenders of child components.

-const handleChange = () => {
+const handleChange = useCallback(() => {
     model[name] = !model[name];
-};
+}, [model, name]);

4-13: Add prop-types validation

Add PropTypes to validate the component's props and improve development-time error catching.

+import PropTypes from 'prop-types';
 
 const CheckboxInput = ({
     model,
     name,
     title,
     icon,
     disabled,
     formrow,
     tabIndex,
     label,
 }) => {
     // ... component implementation
 };
 
+CheckboxInput.propTypes = {
+    model: PropTypes.object.isRequired,
+    name: PropTypes.string.isRequired,
+    title: PropTypes.string.isRequired,
+    icon: PropTypes.string,
+    disabled: PropTypes.bool,
+    formrow: PropTypes.bool,
+    tabIndex: PropTypes.number,
+    label: PropTypes.bool
+};
admin/src/Components/CategoryPeriodsInput.js (1)

22-26: Move categoryTitle object outside component

The categoryTitle object is static and doesn't depend on props or state. Moving it outside the component would prevent unnecessary recreation on each render.

+const categoryTitle = {
+    labaccess: "Labaccess",
+    special_labaccess: "Access till lokalen (styrelsen etc)",
+    membership: "Medlemsskap",
+};

 const CategoryPeriodsInput = ({ categoryPeriods, showHistoric }) => {
-    const categoryTitle = {
-        labaccess: "Labaccess",
-        special_labaccess: "Access till lokalen (styrelsen etc)",
-        membership: "Medlemsskap",
-    };
admin/src/Components/TextInput.jsx (1)

6-8: Consider combining related state

The state variables value, selected, and isDirty are closely related. Consider combining them into a single state object for better maintainability.

-    const [value, setValue] = useState(null);
-    const [selected, setSelected] = useState(false);
-    const [isDirty, setIsDirty] = useState(false);
+    const [inputState, setInputState] = useState({
+        value: null,
+        selected: false,
+        isDirty: false
+    });
admin/src/Membership/MemberBox.jsx (1)

34-76: Consider memoizing child components

The component renders multiple NavItem components with static props. Consider memoizing them to prevent unnecessary re-renders.

+const MemberNavItems = React.memo(({ member_id }) => (
+    <ul className="uk-tab">
+        <NavItem to={`/membership/members/${member_id}/key-handout`}>
+            Medlemsintroduktion
+        </NavItem>
+        {/* ... other NavItems ... */}
+    </ul>
+));

 return (
     <MemberContext.Provider value={memberRef.current}>
         <div>
             <h2>
                 Medlem #{member_number}: {firstname} {lastname}
             </h2>
-            <ul className="uk-tab">
-                {/* ... NavItems ... */}
-            </ul>
+            <MemberNavItems member_id={member_id} />
             {children}
         </div>
     </MemberContext.Provider>
 );
admin/src/Components/DatePeriodInput.js (1)

19-27: Consider using a more immutable approach for period updates

The handleDayChange function directly mutates the period object. Consider updating the period object through a setter method if available, to maintain better encapsulation and predictability.

 const handleDayChange = (date, type) => {
     if (type === "start") {
-        period.start = date || null;
+        period.setStart?.(date || null) ?? (period.start = date || null);
         setStart(date || null);
     } else if (type === "end") {
-        period.end = date || null;
+        period.setEnd?.(date || null) ?? (period.end = date || null);
         setEnd(date || null);
     }
 };
admin/src/Components/MessageForm.jsx (2)

107-118: Consider memoizing onChange handlers

The onChange handlers for TextInput and Textarea are recreated on every render. Consider memoizing them with useCallback.

+const handleBodyChange = useCallback(() => {
+    setBodyLength(message.body.length);
+}, [message.body.length]);

 <TextInput
     model={message}
     name="subject"
     title="Ärende"
-    onChange={() => setBodyLength(message.body.length)}
+    onChange={handleBodyChange}
 />
 <Textarea
     model={message}
     name="body"
     title="Meddelande"
-    onChange={() => setBodyLength(message.body.length)}
+    onChange={handleBodyChange}
 />

98-101: Consider using a setter method for updating recipients

Direct mutation of the message object should be avoided. Consider using a setter method if available.

 onChange={(values) => {
-    message.recipients = values;
+    message.setRecipients?.(values) ?? (message.recipients = values);
     setRecipients(values);
 }}
admin/src/app.jsx (1)

171-235: Consider memoizing the router structure

While the implementation is correct, the nested router structure could benefit from memoization to prevent unnecessary re-renders of the entire route tree.

+const AppRoutes = React.memo(() => (
+    <Switch>
+        <Route exact path="/" component={Dashboard} />
+        <Route path="/membership" component={Membership} />
+        <Route path="/sales" component={Sales} />
+        <Route path="/messages" component={Messages} />
+        <Route path="/statistics" component={Statistics} />
+        <Route path="/settings" component={Settings} />
+        <Route path="/quiz" component={Quiz} />
+        <Route path="/boxTerminator" component={BoxTerminator} />
+        <Route component={Page404} />
+    </Switch>
+));

 const App = () => {
     // ... state and effect ...
     return (
         <Router history={browserHistory}>
             <Switch>
                 <Route path="/logout" component={Logout} />
                 <Route path="/request-password-reset" component={RequestPasswordReset} />
                 <Route path="/password-reset" component={PasswordReset} />
                 <Route path="*">
                     {isLoggedIn && (
                         <div style={{ marginBottom: "2em" }}>
                             <Nav nav={nav} />
                             <div className="uk-container uk-container-center uk-margin-top">
                                 <div className="uk-grid">
                                     <div className="uk-width-medium-1-4">
                                         <SideNav nav={nav} />
                                     </div>
                                     <div className="uk-width-medium-3-4">
-                                        <Switch>
-                                            <Route exact path="/" component={Dashboard} />
-                                            <Route path="/membership" component={Membership} />
-                                            <Route path="/sales" component={Sales} />
-                                            <Route path="/messages" component={Messages} />
-                                            <Route path="/statistics" component={Statistics} />
-                                            <Route path="/settings" component={Settings} />
-                                            <Route path="/quiz" component={Quiz} />
-                                            <Route path="/boxTerminator" component={BoxTerminator} />
-                                            <Route component={Page404} />
-                                        </Switch>
+                                        <AppRoutes />
                                     </div>
                                 </div>
                             </div>
                         </div>
                     )}
                     {!isLoggedIn && <Login />}
                 </Route>
             </Switch>
         </Router>
     );
 };
admin/src/Components/CountryDropdown.jsx (1)

1071-1095: Consider memoizing the countries list

The countries list is being recreated on every render. Consider memoizing it with useMemo since it only depends on the continent data.

+const useCountriesList = (model, name) => {
+    return React.useMemo(() => {
+        const countries = [];
+        continents.forEach((continent) => {
+            countries.push(
+                <li key={continent.name} className="uk-nav-header">
+                    {continent.name}
+                </li>
+            );
+            continent.countries.forEach((country) => {
+                countries.push(
+                    <li key={country.code}>
+                        <a
+                            onClick={(e) => {
+                                model[name] = e.target.dataset.country;
+                            }}
+                            data-country={country.code}
+                            className="uk-dropdown-close"
+                        >
+                            <span className={"flag flag-" + country.code} />{" "}
+                            {country.name}
+                        </a>
+                    </li>
+                );
+            });
+        });
+        return countries;
+    }, [model, name]);
+};

 const CountryDropdown = ({ model, name }) => {
     const [country, setCountry] = useState("");
     // ... effect hook ...
-    let countries = [];
-    continents.forEach((continent) => {
-        // ... countries list creation ...
-    });
+    const countries = useCountriesList(model, name);
     return (
         // ... render ...
     );
 };
🧰 Tools
🪛 Biome (1.9.4)

[error] 1084-1084: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4a54d9f and 4604cd1.

📒 Files selected for processing (49)
  • admin/src/Components/CategoryPeriodsInput.js (1 hunks)
  • admin/src/Components/CheckboxInput.jsx (1 hunks)
  • admin/src/Components/CollectionTable.js (5 hunks)
  • admin/src/Components/CountryDropdown.jsx (2 hunks)
  • admin/src/Components/DatePeriodInput.js (1 hunks)
  • admin/src/Components/DateTimeInput.jsx (1 hunks)
  • admin/src/Components/FileInput.jsx (1 hunks)
  • admin/src/Components/GroupForm.js (1 hunks)
  • admin/src/Components/KeyHandoutForm.jsx (5 hunks)
  • admin/src/Components/Login.jsx (1 hunks)
  • admin/src/Components/Logout.jsx (1 hunks)
  • admin/src/Components/MemberForm.js (1 hunks)
  • admin/src/Components/MessageForm.jsx (3 hunks)
  • admin/src/Components/ProductForm.jsx (2 hunks)
  • admin/src/Components/RequestPasswordReset.jsx (2 hunks)
  • admin/src/Components/SearchBox.jsx (1 hunks)
  • admin/src/Components/SelectInput.jsx (1 hunks)
  • admin/src/Components/TextInput.jsx (1 hunks)
  • admin/src/Components/Textarea.jsx (1 hunks)
  • admin/src/Contexts/GroupContext.jsx (1 hunks)
  • admin/src/Membership/GroupAdd.js (1 hunks)
  • admin/src/Membership/GroupBox.jsx (1 hunks)
  • admin/src/Membership/GroupBoxEditInfo.jsx (1 hunks)
  • admin/src/Membership/GroupBoxMembers.jsx (3 hunks)
  • admin/src/Membership/GroupBoxPermissions.jsx (1 hunks)
  • admin/src/Membership/KeyEdit.jsx (1 hunks)
  • admin/src/Membership/KeyHandout.jsx (1 hunks)
  • admin/src/Membership/MemberAdd.js (1 hunks)
  • admin/src/Membership/MemberBox.jsx (1 hunks)
  • admin/src/Membership/MemberBoxGroups.js (1 hunks)
  • admin/src/Membership/MemberBoxKeys.js (2 hunks)
  • admin/src/Membership/MemberBoxMemberData.jsx (1 hunks)
  • admin/src/Membership/MemberBoxNewMessage.js (1 hunks)
  • admin/src/Membership/MemberBoxOrders.js (2 hunks)
  • admin/src/Membership/MemberBoxPermissions.js (1 hunks)
  • admin/src/Membership/MemberBoxSpans.jsx (2 hunks)
  • admin/src/Membership/MemberExport.jsx (2 hunks)
  • admin/src/Membership/MembershipPeriodsInput.js (1 hunks)
  • admin/src/Membership/SpanShow.jsx (1 hunks)
  • admin/src/Messages/MessageAdd.js (1 hunks)
  • admin/src/Messages/MessageShow.js (1 hunks)
  • admin/src/Sales/AccountingBox.jsx (1 hunks)
  • admin/src/Sales/GiftCardShow.js (1 hunks)
  • admin/src/Sales/OrderShow.js (1 hunks)
  • admin/src/Sales/ProductAdd.js (1 hunks)
  • admin/src/Sales/ProductEdit.jsx (1 hunks)
  • admin/src/Settings/AccessTokenList.jsx (1 hunks)
  • admin/src/Settings/ServiceTokenList.jsx (2 hunks)
  • admin/src/app.jsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • admin/src/Contexts/GroupContext.jsx
🧰 Additional context used
🪛 Biome (1.9.4)
admin/src/Membership/GroupBoxMembers.jsx

[error] 6-6: Do not shadow the global "Date" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

admin/src/Components/Textarea.jsx

[error] 53-53: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/TextInput.jsx

[error] 57-58: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Messages/MessageShow.js

[error] 58-58: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 62-62: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

admin/src/Components/ProductForm.jsx

[error] 35-35: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

admin/src/Components/SelectInput.jsx

[error] 53-53: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Membership/MembershipPeriodsInput.js

[error] 61-68: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/CountryDropdown.jsx

[error] 1084-1084: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (35)
admin/src/Components/RequestPasswordReset.jsx (2)

29-77: LGTM! Well-structured form with good accessibility

The JSX structure follows best practices:

  • Proper form semantics
  • Accessibility support with autoComplete
  • Consistent styling with UK-UIkit

1-2: Consider future React Router updates

The comment about retaining withRouter for compatibility suggests this might need future updates. React Router v6 has deprecated withRouter in favor of hooks like useNavigate and useLocation.

Let's check the React Router version in use:

✅ Verification successful

Retain withRouter for now - widespread usage detected

The withRouter HOC is used consistently across multiple components in the admin interface, with React Router v5.2.0 currently in use. Removing it from this component would require a coordinated migration effort across all components. The comment about compatibility is appropriate and should be retained.

  • Found 19 components using withRouter including key navigation and form components
  • Current React Router version (v5.2.0) has withRouter as a supported feature
  • Any migration should be done as a separate, coordinated effort
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check package.json for React Router version
fd -t f "package.json" -x cat {} | jq '.dependencies["react-router"], .dependencies["react-router-dom"]'

Length of output: 144


Script:

#!/bin/bash
# Check for withRouter usage across the codebase
rg "withRouter" -A 2 -B 2

Length of output: 7991

admin/src/Membership/MemberBoxSpans.jsx (1)

Line range hint 1-14: LGTM! Clean conversion to functional component

The imports are well-organized and the component declaration follows React functional component best practices.

admin/src/Membership/MemberBoxPermissions.js (1)

23-28: Verify CollectionTable component interface

Let's verify if CollectionTable supports the suggested error and loading state handling.

admin/src/Membership/MemberExport.jsx (1)

Line range hint 4-72: Verify complete conversion from class component.

The conversion to a function component looks complete, but let's verify no class-based patterns were missed.

✅ Verification successful

The file admin/src/Membership/MemberExport.jsx shows no signs of class-based component patterns or lifecycle methods. The component is properly implemented as a function component using hooks (useState) for state management. The search results from other files in the codebase are not relevant to our verification task.

Component conversion is complete and correct.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining class-based patterns in this file
ast-grep --pattern 'class $_ extends React.Component {
  $$$
}'

# Search for lifecycle method names that might need conversion
rg -i 'componentdid|componentwill|getderived' admin/src/Membership/MemberExport.jsx

Length of output: 25175

admin/src/Sales/GiftCardShow.js (1)

1-9: LGTM! Clean conversion to functional component

The imports and component declaration follow React best practices for functional components.

admin/src/Components/GroupForm.js (3)

1-7: LGTM! Clean imports and modern component declaration.

The component has been properly converted to a functional component with destructured props, maintaining the withRouter HOC.


1-75: Overall implementation looks good with room for improvements.

The conversion from class to functional component has been done correctly, maintaining the core functionality while leveraging React hooks appropriately. The suggested improvements above would further enhance the component's robustness and maintainability, but the current implementation is solid.


67-69: Consider internationalizing button text.

The button text is currently hardcoded in Swedish. Consider using an internationalization solution for better maintainability and accessibility.

Let's check if there's an i18n solution already in place:

admin/src/Membership/MemberBoxOrders.js (1)

39-44: Verify CollectionTable component interface

Let's verify that the CollectionTable component supports all the props we're passing and using.

✅ Verification successful

Based on the search results, I can see that the CollectionTable component is widely used across the application and is defined in admin/src/Components/CollectionTable.js. Let's examine its implementation to verify the props interface:


CollectionTable component interface is correctly used

The implementation in MemberBoxOrders.js correctly uses the CollectionTable component. The component accepts and expects the following props which are all being properly provided:

  • emptyMessage: String for empty state message
  • rowComponent: Component for rendering each row
  • collection: Collection instance for data management
  • columns: Array defining table columns
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for CollectionTable component definition and prop types
ast-grep --pattern 'class CollectionTable extends $_ {
  $$$
}' || ast-grep --pattern 'const CollectionTable = $_'

# Search for PropTypes definition
rg -A 10 'CollectionTable.propTypes = {' 

Length of output: 230


Script:

#!/bin/bash
# Search for CollectionTable component with better pattern matching
ast-grep --pattern 'class CollectionTable' || ast-grep --pattern 'function CollectionTable'

# Search for CollectionTable component with ripgrep
rg -A 10 'CollectionTable' --type js

# Search for files containing CollectionTable
fd CollectionTable

Length of output: 41890


Script:

#!/bin/bash
# Get the CollectionTable component implementation
cat admin/src/Components/CollectionTable.js

Length of output: 6041

admin/src/Sales/ProductEdit.jsx (2)

1-10: LGTM! Clean transition to modern React imports.

The imports have been properly updated to use React hooks and modern routing hooks from react-router-dom.


64-64: LGTM! Clean export statement.

The export has been properly updated to remove the withRouter HOC, aligning with the modern hooks-based approach.

admin/src/Components/Login.jsx (2)

1-8: LGTM! Clean conversion to function component

The conversion from class to function component follows React best practices, with proper use of the useRef hook for form input references.


1-86: Verify component behavior after conversion

Let's verify that no functionality was lost during the class-to-function component conversion.

✅ Verification successful

Component behavior verified after conversion

The functional Login component maintains the same behavior as expected:

  • It correctly uses the auth.login method from the Auth class with username and password
  • The error handling through showError is preserved
  • Form submission and refs usage are implemented correctly
  • No other components extend or depend on Login's implementation
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for tests related to the Login component
rg -l "Login" --glob "**/*test*.*" --glob "**/*spec*.*"

# Search for any remaining class components that might extend from this one
ast-grep --pattern 'class $_ extends Login'

Length of output: 101


Script:

#!/bin/bash
# Let's verify the auth.login usage and implementation
ast-grep --pattern 'auth.login($$$)'

# Check the auth implementation
rg -l "login.*=.*function" --type js --type jsx

# Look for any previous implementation of Login component
rg -B5 -A5 "class Login.*{" --type js --type jsx

# Check for any direct imports of Login component
rg "import.*Login.*from"

Length of output: 585


Script:

#!/bin/bash
# Let's fix the previous regex issues and check for class implementation
rg -B5 -A5 "class Login"

# Check auth implementation with correct file type
rg -l "login.*=.*function" --type ts --type js

# Check the auth module implementation
rg -A 5 "login.*=.*function" admin/src/auth.js

# Look at the implementation in public/ts/login.ts
cat public/ts/login.ts

Length of output: 1021


Script:

#!/bin/bash
# Let's check the auth module content
cat admin/src/auth.js

# Check for any usage of the Login component in tests
rg -l "Login" --type test

# Look for the auth module's login function usage
rg -A 3 "auth\.login" --type js --type jsx --type ts

Length of output: 5351

admin/src/Sales/ProductAdd.js (2)

1-8: LGTM! Clean import declarations

The migration from browserHistory to useHistory hook aligns with modern React Router practices.


1-27: Verify component behavior after hooks migration

The conversion from class to function component looks good and aligns with the PR objectives. The component maintains its core functionality while adopting modern React patterns.

✅ Verification successful

Component migration verified successfully

The conversion of ProductAdd to a functional component is correct and consistent with the codebase's modernization efforts. Both ProductForm and ProductEdit components are already using hooks and functional patterns. While there are still some class components in the Sales directory, they extend CollectionNavigation (a custom class) rather than React.Component, which is outside the scope of this specific component's migration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify no remaining class-based components in the Sales directory
ast-grep --pattern 'class $_ extends React.Component {
  $$$
}'  admin/src/Sales/

Length of output: 82


Script:

#!/bin/bash
# Let's try a broader search for class components in the Sales directory
rg "extends (React\.)?Component" admin/src/Sales/ -l

# Also search for potential class component patterns
rg "class.*{" admin/src/Sales/ -l

# Check all files in Sales directory to understand the structure
fd . admin/src/Sales/ -e js

Length of output: 558


Script:

#!/bin/bash
# Let's fix the regex and check for class components in all Sales files
rg "class.*\{" admin/src/Sales/ -A 2

# Also check for any React.Component or Component imports
rg "import.*Component.*from 'react'" admin/src/Sales/

# Let's also verify the ProductForm component since it's used in ProductAdd
rg -l "ProductForm" admin/src/

Length of output: 3514


Script:

#!/bin/bash
# Let's check if these class components extend React.Component or CollectionNavigation
rg "import.*CollectionNavigation" admin/src/Sales/

# Check ProductForm.jsx to verify it's not a class component
cat admin/src/Components/ProductForm.jsx

# Check ProductEdit.jsx since it also uses ProductForm
cat admin/src/Sales/ProductEdit.jsx

Length of output: 13005

admin/src/Membership/MemberBoxNewMessage.js (2)

9-33: Implementation looks good with suggested improvements

The conversion from class component to function component is well-executed and maintains the core functionality. Consider implementing the suggested improvements for error handling and routing modernization, but the current implementation is acceptable.


26-30: Verify MessageForm component contract

Please verify that the MessageForm component properly handles these exact props, especially the hardcoded recipientSelect={false}.

✅ Verification successful

MessageForm component props are correctly used

The verification shows that the MessageForm component in admin/src/Components/MessageForm.jsx properly accepts and handles all three props:

  • recipientSelect - controls recipient selection visibility
  • message - message data object
  • onSave - callback for form submission

The component is used consistently across the codebase, with recipientSelect={false} in MemberBoxNewMessage (for single recipient) and recipientSelect={true} in MessageAdd (for multiple recipients).

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check MessageForm component implementation and prop types
ast-grep --pattern 'const MessageForm = ({ recipientSelect, message, onSave }) => {
  $$$
}'

# Alternative class implementation
ast-grep --pattern 'class MessageForm extends $_ {
  $$$
}'

Length of output: 156


Script:

#!/bin/bash
# Search for MessageForm component definition with broader patterns
ast-grep --pattern 'MessageForm = $_'

# Search for MessageForm component with ripgrep for more context
rg "MessageForm" -A 10 -B 2

# Search for PropTypes validation
rg "MessageForm.propTypes"

Length of output: 10895

admin/src/Membership/MembershipPeriodsInput.js (1)

47-55: Consider checking the result of cp.merge()

The cp.merge() function is called, but its return value isn't checked. If merge() can fail or return a meaningful value, consider handling it appropriately.

Please ensure that cp.merge() behaves as expected and handle any errors if necessary.

admin/src/Components/FileInput.jsx (2)

91-93: Approved: clearUpload function is correctly implemented

The clearUpload function properly resets the filename state.


102-144: Approved: Rendering logic is correct

The JSX rendering logic for displaying the upload interface and handling the filename state is correctly implemented.

admin/src/Sales/OrderShow.js (1)

11-107: Successful refactoring to functional component using hooks

The component has been successfully converted from a class-based component to a functional component using React hooks. The use of useParams, useState, and useEffect effectively replaces the constructor and lifecycle methods. The overall structure is cleaner, and the functionality is preserved.

admin/src/Settings/ServiceTokenList.jsx (2)

7-7: Verify the uniqueness of item.access_token as a key.

In the Row component, you're using item.access_token as the key for the <tr> element. Ensure that access_token is unique for each item to prevent potential issues with React's key mechanism.


29-48: Good job refactoring to functional components with hooks.

The transition from a class-based component to a functional component using useState and useEffect is well-executed. The code is clean and follows React best practices.

admin/src/Components/Logout.jsx (1)

6-12: LGTM!

The Logout component correctly handles user logout and redirects to the home page. The use of useEffect with an empty dependency array ensures the side effect runs once on component mount.

admin/src/Membership/KeyHandout.jsx (1)

6-12: LGTM!

The KeyHandout component effectively uses useContext to access MemberContext and passes the member prop to KeyHandoutForm. The conversion to a functional component is correctly implemented.

admin/src/Membership/GroupBoxEditInfo.jsx (1)

7-14: LGTM! Well-structured function component with proper context handling.

The conversion to a function component is well implemented with proper error handling for missing context.

admin/src/Components/SearchBox.jsx (1)

3-4: LGTM! Clean conversion to function component.

The component properly uses useRef and destructured props.

admin/src/Components/DatePeriodInput.js (1)

9-17: LGTM! Clean subscription management

The useEffect hook correctly manages the subscription lifecycle with proper cleanup and dependency array.

admin/src/Components/MessageForm.jsx (1)

37-47: LGTM! Clean subscription management

The useEffect hook correctly manages the subscription lifecycle with proper cleanup and dependency array.

admin/src/Components/MemberForm.js (3)

10-18: LGTM! Clean subscription management

The useEffect hook correctly manages the subscription lifecycle with proper cleanup and dependency array.


7-147: LGTM! Clean conversion to function component

The component has been successfully converted to a function component while maintaining its original functionality. The form structure and validation logic have been preserved.


130-136: ⚠️ Potential issue

Add type="button" to delete button

The delete button should have type="button" to prevent accidental form submission, as it's inside a form element.

 <a
+    type="button"
     className="uk-button uk-button-danger uk-float-left"
     onClick={onDelete}
 >
     <i className="uk-icon-trash" /> Ta bort medlem
 </a>

Likely invalid or redundant comment.

admin/src/app.jsx (1)

164-169: LGTM! Clean conversion to hooks

The conversion from class component to functional component is well-implemented:

  • Proper usage of useState for managing login state
  • Correct implementation of useEffect with cleanup for auth change subscription
  • Dependencies array is properly specified
admin/src/Components/CountryDropdown.jsx (1)

1047-1055: LGTM! Well-implemented effect hook

The effect hook correctly:

  • Manages subscription lifecycle
  • Handles cleanup properly
  • Has appropriate dependencies

import React from "react";
import Key from "../Models/Key";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useParams, withRouter } from "react-router";
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider removing withRouter HOC

Since you're already using the useParams hook, the withRouter HOC is unnecessary. React Router v6 recommends using hooks instead of HOCs:

-import { useParams, withRouter } from "react-router";
+import { useParams } from "react-router";

 // ... component code ...

-export default withRouter(KeyEdit);
+export default KeyEdit;

Also applies to: 106-106

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any comment on this? I think the Rabbit might be correct here as well. In that case, you can remove the withRouter import and its usage on line 107

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emanuelen5, you are correct. Since we're already using the useParams hook from React Router, the withRouter higher-order component is unnecessary and can be removed. Removing the withRouter import and its usage on line 107 will simplify the component and align it with React Router v6 best practices.


✏️ Learnings added
Learnt from: emanuelen5
PR: makerspace/makeradmin#581
File: admin/src/Membership/KeyEdit.jsx:2-2
Timestamp: 2024-12-09T22:05:33.832Z
Learning: In React components using React Router v6, if hooks like `useParams` are used, the `withRouter` higher-order component is unnecessary and should be removed to simplify the code.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +24 to +35
const onSave = useCallback(() => {
keyRef.current.save();
}, []);

const onSave = () => this.key.save();
const onDelete = useCallback(() => {
confirmModal(keyRef.current.deleteConfirmMessage())
.then(() => keyRef.current.del())
.then(() => {
browserHistory.push("/membership/keys/");
})
.catch(() => null);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to onSave

While onDelete has proper error handling, onSave might benefit from similar treatment:

     const onSave = useCallback(() => {
-        keyRef.current.save();
+        keyRef.current.save()
+            .then(() => {
+                // Consider showing a success message
+            })
+            .catch((error) => {
+                // Handle save errors
+                console.error('Failed to save key:', error);
+            });
     }, []);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +113 to +119
<a
onClick={() => deleteItem(item)}
className="removebutton"
>
<i className="uk-icon-trash" />
</a>
</td>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve accessibility for delete button

The delete button needs accessibility improvements.

 <a
     onClick={() => deleteItem(item)}
     className="removebutton"
+    href="#"
+    role="button"
+    aria-label={`Delete span ${item.id}`}
 >
     <i className="uk-icon-trash" />
 </a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a
onClick={() => deleteItem(item)}
className="removebutton"
>
<i className="uk-icon-trash" />
</a>
</td>
<a
onClick={() => deleteItem(item)}
className="removebutton"
href="#"
role="button"
aria-label={`Delete span ${item.id}`}
>
<i className="uk-icon-trash" />
</a>
</td>

Comment on lines 24 to 52
useEffect(() => {
// Fetch pending actions
const fetchPendingActions = async () => {
const response = await get({
url: `/membership/member/${props.match.params.member_id}/pending_actions`,
});
});
}
const sumPendingLabAccessDays = response.data.reduce(
(acc, value) => {
if (value.action.action === ADD_LABACCESS_DAYS)
return acc + value.action.value;
return acc;
},
0,
);
setPendingLabAccessDays(sumPendingLabAccessDays);
};

fetchPendingActions();

componentDidMount() {
this.unsubscribe = this.collection.subscribe(({ items }) => {
this.setState({ items });
// Subscribe to collection
const unsubscribe = collection.subscribe(() => {
// No need to handle the subscription data since it's unused
});
}

componentWillUnmount() {
this.unsubscribe();
}
// Cleanup on component unmount
return () => {
unsubscribe();
};
}, [props.match.params.member_id]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve useEffect implementation

  1. The effect combines two separate concerns (data fetching and subscription).
  2. The collection dependency is missing from the dependency array.
  3. The fetchPendingActions function could be memoized.

Consider splitting into two separate effects:

+const fetchPendingActions = React.useCallback(async () => {
+    const response = await get({
+        url: `/membership/member/${props.match.params.member_id}/pending_actions`,
+    });
+    const sumPendingLabAccessDays = response.data.reduce(
+        (acc, value) => {
+            if (value.action.action === ADD_LABACCESS_DAYS)
+                return acc + value.action.value;
+            return acc;
+        },
+        0,
+    );
+    setPendingLabAccessDays(sumPendingLabAccessDays);
+}, [props.match.params.member_id]);

+useEffect(() => {
+    fetchPendingActions();
+}, [fetchPendingActions]);

+useEffect(() => {
+    const unsubscribe = collection.subscribe(() => {
+        // No need to handle the subscription data since it's unused
+    });
+    return unsubscribe;
+}, [collection]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
// Fetch pending actions
const fetchPendingActions = async () => {
const response = await get({
url: `/membership/member/${props.match.params.member_id}/pending_actions`,
});
});
}
const sumPendingLabAccessDays = response.data.reduce(
(acc, value) => {
if (value.action.action === ADD_LABACCESS_DAYS)
return acc + value.action.value;
return acc;
},
0,
);
setPendingLabAccessDays(sumPendingLabAccessDays);
};
fetchPendingActions();
componentDidMount() {
this.unsubscribe = this.collection.subscribe(({ items }) => {
this.setState({ items });
// Subscribe to collection
const unsubscribe = collection.subscribe(() => {
// No need to handle the subscription data since it's unused
});
}
componentWillUnmount() {
this.unsubscribe();
}
// Cleanup on component unmount
return () => {
unsubscribe();
};
}, [props.match.params.member_id]);
const fetchPendingActions = React.useCallback(async () => {
const response = await get({
url: `/membership/member/${props.match.params.member_id}/pending_actions`,
});
const sumPendingLabAccessDays = response.data.reduce(
(acc, value) => {
if (value.action.action === ADD_LABACCESS_DAYS)
return acc + value.action.value;
return acc;
},
0,
);
setPendingLabAccessDays(sumPendingLabAccessDays);
}, [props.match.params.member_id]);
useEffect(() => {
fetchPendingActions();
}, [fetchPendingActions]);
useEffect(() => {
const unsubscribe = collection.subscribe(() => {
// No need to handle the subscription data since it's unused
});
return unsubscribe;
}, [collection]);

Comment on lines +12 to 19
function MemberBoxPermissions(props) {
const member_id = props.match.params.member_id;
const collection = new Collection({
type: Permission,
url: `/membership/member/${member_id}/permissions`,
pageSize: 0,
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add validation for member_id and optimize Collection instantiation

The member_id extraction and Collection instantiation could be improved for better error handling and performance.

+import { useMemo } from 'react';

 function MemberBoxPermissions(props) {
     const member_id = props.match.params.member_id;
+    
+    if (!member_id) {
+        return <div className="uk-alert uk-alert-danger">Invalid member ID</div>;
+    }
 
-    const collection = new Collection({
+    const collection = useMemo(() => new Collection({
         type: Permission,
         url: `/membership/member/${member_id}/permissions`,
         pageSize: 0,
-    });
+    }), [member_id]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function MemberBoxPermissions(props) {
const member_id = props.match.params.member_id;
const collection = new Collection({
type: Permission,
url: `/membership/member/${member_id}/permissions`,
pageSize: 0,
});
import { useMemo } from 'react';
function MemberBoxPermissions(props) {
const member_id = props.match.params.member_id;
if (!member_id) {
return <div className="uk-alert uk-alert-danger">Invalid member ID</div>;
}
const collection = useMemo(() => new Collection({
type: Permission,
url: `/membership/member/${member_id}/permissions`,
pageSize: 0,
}), [member_id]);

Comment on lines +17 to +26
useEffect(() => {
const unsubscribe = model.subscribe(() => {
setValue(model[name] === "" ? false : model[name]);
setIsDirty(model.isDirty(name));
});

componentWillUnmount() {
this.unsubscribe();
}
return () => {
unsubscribe();
};
}, [model, name]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize model subscription updates

The subscription callback should use functional updates to ensure state updates are based on the latest values.

 useEffect(() => {
     const unsubscribe = model.subscribe(() => {
-        setValue(model[name] === "" ? false : model[name]);
-        setIsDirty(model.isDirty(name));
+        setValue(Boolean(model[name]));
+        setIsDirty(prev => {
+            const newDirty = model.isDirty(name);
+            return prev !== newDirty ? newDirty : prev;
+        });
     });

     return () => {
         unsubscribe();
     };
 }, [model, name]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const unsubscribe = model.subscribe(() => {
setValue(model[name] === "" ? false : model[name]);
setIsDirty(model.isDirty(name));
});
componentWillUnmount() {
this.unsubscribe();
}
return () => {
unsubscribe();
};
}, [model, name]);
useEffect(() => {
const unsubscribe = model.subscribe(() => {
setValue(Boolean(model[name]));
setIsDirty(prev => {
const newDirty = model.isDirty(name);
return prev !== newDirty ? newDirty : prev;
});
});
return () => {
unsubscribe();
};
}, [model, name]);

Comment on lines +56 to +59
onChange={(event) =>
(model[name] =
event.target.value === "" ? null : event.target.value)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor assignment in onChange handler

The current implementation assigns directly to model[name] within the expression, which is flagged by static analysis. Consider refactoring to make the code more explicit and easier to understand.

-            onChange={(event) =>
-                (model[name] =
-                    event.target.value === "" ? null : event.target.value)
-            }
+            onChange={(event) => {
+                const newValue = event.target.value === "" ? null : event.target.value;
+                model[name] = newValue;
+            }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onChange={(event) =>
(model[name] =
event.target.value === "" ? null : event.target.value)
}
onChange={(event) => {
const newValue = event.target.value === "" ? null : event.target.value;
model[name] = newValue;
}}
🧰 Tools
🪛 Biome (1.9.4)

[error] 57-58: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

Comment on lines +18 to +30
useEffect(() => {
const member = memberRef.current;
const unsubscribe = member.subscribe(() => {
setMemberState({
member_number: member.member_number,
firstname: member.firstname,
lastname: member.lastname,
}),
);
}
});
});
console.log(children);

componentWillUnmount() {
this.unsubscribe();
}
return () => unsubscribe();
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Refactor useEffect implementation

Several issues need to be addressed in the useEffect:

  1. Remove the console.log statement
  2. Add member_id to the dependency array
  3. Initialize memberRef inside useEffect
     useEffect(() => {
-        const member = memberRef.current;
+        memberRef.current = Member.get(member_id);
+        const member = memberRef.current;
         const unsubscribe = member.subscribe(() => {
             setMemberState({
                 member_number: member.member_number,
                 firstname: member.firstname,
                 lastname: member.lastname,
             });
         });
-        console.log(children);

         return () => unsubscribe();
-    }, []);
+    }, [member_id]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const member = memberRef.current;
const unsubscribe = member.subscribe(() => {
setMemberState({
member_number: member.member_number,
firstname: member.firstname,
lastname: member.lastname,
}),
);
}
});
});
console.log(children);
componentWillUnmount() {
this.unsubscribe();
}
return () => unsubscribe();
}, []);
useEffect(() => {
memberRef.current = Member.get(member_id);
const member = memberRef.current;
const unsubscribe = member.subscribe(() => {
setMemberState({
member_number: member.member_number,
firstname: member.firstname,
lastname: member.lastname,
});
});
return () => unsubscribe();
}, [member_id]);

}
function MemberBox({ children }) {
const { member_id } = useParams();
const memberRef = useRef(Member.get(member_id));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move Member instance creation to useEffect

Creating the Member instance during component initialization could lead to stale data if member_id changes. Consider moving it inside useEffect.

-    const memberRef = useRef(Member.get(member_id));
+    const memberRef = useRef(null);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1083 to +1085
onClick={(e) =>
(model[name] = e.target.dataset.country)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Refactor assignment in click handler

The assignment within the click handler expression can lead to confusion and potential bugs. Consider extracting it to a separate function.

-onClick={(e) =>
-    (model[name] = e.target.dataset.country)
-}
+onClick={(e) => {
+    const handleCountrySelect = (country) => {
+        model[name] = country;
+    };
+    handleCountrySelect(e.target.dataset.country);
+}}

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Biome (1.9.4)

[error] 1084-1084: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

@emanuelen5
Copy link
Member

emanuelen5 commented Dec 7, 2024

The ruff test failed on main, which is why it failed on this PR as well. However, I've fixed it now on main (https://github.com/makerspace/makeradmin/compare/097ee72dce2585a503c942b19eaf10a84094e752%5E..fe136f9aa9873118307998495c2efccf6cb1677d), so you can merge from main to get that passing.

@emanuelen5 emanuelen5 self-requested a review December 7, 2024 19:54
@emanuelen5 emanuelen5 added the legacy Old stuff that might need updating or refactoring label Dec 7, 2024
@emanuelen5
Copy link
Member

emanuelen5 commented Dec 7, 2024

Thank you very much for the contributions!

I haven't looked through all pages, but I noted that there are a few regressions:

@HalfVoxel
Copy link
Contributor

What a great contribution <3! Amazing work!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (2)
admin/src/Membership/MembershipPeriodsInput.js (1)

98-130: Enhance form UX and accessibility

Consider the following improvements:

  1. Add loading state during save operation
  2. Improve accessibility with aria-labels
  3. Add error feedback UI
+    const [isSaving, setIsSaving] = useState(false);
+
     return (
         <form
             className="uk-form"
             onSubmit={(e) => {
                 e.preventDefault();
+                setIsSaving(true);
-                onSave();
+                onSave()
+                    .finally(() => setIsSaving(false));
                 return false;
             }}
         >
             <label className="uk-label" htmlFor="showHistoric">
                 Visa historiska
             </label>
             <input
                 id="showHistoric"
                 className="uk-checkbox"
                 type="checkbox"
+                aria-label="Show historic entries"
                 checked={showHistoric}
                 onChange={(e) => setShowHistoric(e.target.checked)}
             />
             {/* ... */}
             <button
-                disabled={saveDisabled}
+                disabled={saveDisabled || isSaving}
                 className="uk-button uk-button-success uk-float-right"
+                aria-busy={isSaving}
             >
-                <i className="uk-icon-save" /> Spara
+                <i className="uk-icon-save" /> {isSaving ? 'Saving...' : 'Spara'}
             </button>
         </form>
     );
admin/src/Membership/MemberBoxSpans.jsx (1)

17-17: Clarify the use of setItems without items in state

You are destructuring useState as const [, setItems] = useState([]);, effectively ignoring the items state variable. If items is not needed elsewhere in the component, consider the following:

  • Remove the state variable altogether if it's not used.
  • Add a comment explaining that setItems is used to trigger re-renders when the collection changes.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4604cd1 and eaccc32.

📒 Files selected for processing (2)
  • admin/src/Membership/MemberBoxSpans.jsx (2 hunks)
  • admin/src/Membership/MembershipPeriodsInput.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
admin/src/Membership/MembershipPeriodsInput.js

[error] 67-74: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (5)
admin/src/Membership/MembershipPeriodsInput.js (2)

65-75: Refactor arrow function assignment

The assignment within the arrow function makes the code less readable and was flagged by static analysis.

🧰 Tools
🪛 Biome (1.9.4)

[error] 67-74: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


1-134: Verify fixes for reported UI regressions

The reported issues with infinite network requests and blank pages could be related to the identified problems in state management and effect dependencies. Let's verify the component's behavior:

🧰 Tools
🪛 Biome (1.9.4)

[error] 67-74: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Membership/MemberBoxSpans.jsx (3)

29-44: Refactor useEffect to separate concerns and add missing dependencies

This comment is similar to a previous review comment.

The current useEffect combines data fetching and lacks necessary dependencies, which can lead to unexpected behaviors. Consider the following refactoring:

  1. Memoize fetchPendingActions using useCallback to prevent unnecessary re-creation of the function on every render.
  2. Separate concerns by splitting into two useEffect hooks: one for fetching data and another for handling side effects.
  3. Add fetchPendingActions to the dependency array to ensure the effect is triggered appropriately.

Apply the following changes:

+const fetchPendingActions = React.useCallback(() => {
+    get({ url: `/membership/member/${memberId}/pending_actions` }).then(
+        (r) => {
+            const sumPendingLabaccessDays = r.data.reduce(
+                (acc, value) => {
+                    if (value.action.action === ADD_LABACCESS_DAYS)
+                        return acc + value.action.value;
+                    return acc;
+                },
+                0,
+            );
+            setPendingLabaccessDays(sumPendingLabaccessDays);
+        },
+    );
+}, [memberId]);

-useEffect(() => {
-    get({ url: `/membership/member/${memberId}/pending_actions` }).then(
-        (r) => {
-            const sum_pending_labaccess_days = r.data.reduce(
-                (acc, value) => {
-                    if (value.action.action === ADD_LABACCESS_DAYS)
-                        return acc + value.action.value;
-                    return acc;
-                },
-                0,
-            );
-            setPendingLabaccessDays(sum_pendingLabaccessDays);
-        },
-    );
-}, [memberId]);
+useEffect(() => {
+    fetchPendingActions();
+}, [fetchPendingActions]);

47-54: Include collectionRef in the dependency array of useEffect

This is similar to a previous review comment.

The useEffect hook for subscribing to collection changes has an empty dependency array, which can cause issues if collectionRef changes. Include collectionRef.current in the dependencies to ensure proper subscription management.

Apply this diff:

 useEffect(() => {
     const unsubscribe = collectionRef.current.subscribe(({ items }) => {
         setItems(items);
     });
     return () => {
         unsubscribe();
     };
-}, []);
+}, [collectionRef]);

115-120: Improve accessibility for the delete button

This comment is a duplicate of a previous review comment.

To enhance accessibility, add the href, role, and aria-label attributes to the delete button. This ensures that assistive technologies can interpret the button correctly.

Apply this diff:

 <a
     onClick={() => deleteItem(item)}
     className="removebutton"
+    href="#"
+    role="button"
+    aria-label={`Delete span ${item.id}`}
 >
     <i className="uk-icon-trash" />
 </a>

Comment on lines +12 to +18
const categoryPeriodsListRef = useRef([
new CategoryPeriods({ category: "labaccess" }),
new CategoryPeriods({ category: "membership" }),
new CategoryPeriods({ category: "special_labaccess" }),
]);

const categoryPeriodsList = categoryPeriodsListRef.current;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider using useState instead of useRef for categoryPeriodsList

The current implementation using useRef for categoryPeriodsList creates mutable state outside of React's control. This could be contributing to the infinite network requests mentioned in the PR feedback. Consider using useState instead:

-    const categoryPeriodsListRef = useRef([
-        new CategoryPeriods({ category: "labaccess" }),
-        new CategoryPeriods({ category: "membership" }),
-        new CategoryPeriods({ category: "special_labaccess" }),
-    ]);
-
-    const categoryPeriodsList = categoryPeriodsListRef.current;
+    const [categoryPeriodsList] = useState(() => [
+        new CategoryPeriods({ category: "labaccess" }),
+        new CategoryPeriods({ category: "membership" }),
+        new CategoryPeriods({ category: "special_labaccess" }),
+    ]);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +83 to +93
// post({
// url: `/webshop/member/${member_id}/ship_labaccess_orders`,
// expectedDataStatus: "ok",
// });
post({
url: `/webshop/member/${member_id}/ship_labaccess_orders`,
headers: { "Content-Type": "application/json" },
payload: {
/* any required data */
},
expectedDataStatus: "ok",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clean up commented code and improve error handling

  1. Remove the commented-out code block
  2. Add proper error handling for the POST request
-            // post({
-            //     url: `/webshop/member/${member_id}/ship_labaccess_orders`,
-            //     expectedDataStatus: "ok",
-            // });
             post({
                 url: `/webshop/member/${member_id}/ship_labaccess_orders`,
                 headers: { "Content-Type": "application/json" },
                 payload: {
-                    /* any required data */
+                    member_id,
+                    spans: addSpans.map(s => s.id)
                 },
                 expectedDataStatus: "ok",
+            }).catch(error => {
+                console.error('Failed to ship labaccess orders:', error);
+                // Add appropriate error handling UI feedback
             });

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +27 to +47
useEffect(() => {
const unsubscribes = [];

// Subscribe to spans updates
unsubscribes.push(
spans.subscribe(({ items }) => {
categoryPeriodsList.forEach((periods) =>
periods.replace(filterPeriods(items, periods.category)),
);
}),
);
this.categoryPeriodsList.forEach((cp) => {
this.unsubscribe.push(
cp.subscribe(() =>
this.setState({ saveDisabled: !this.canSave() }),
),
);
});
}

componentWillUnmount() {
this.unsubscribe.forEach((u) => u());
}
// Subscribe to categoryPeriods updates
categoryPeriodsList.forEach((cp) => {
unsubscribes.push(cp.subscribe(() => setSaveDisabled(!canSave())));
});

render() {
const { showHistoric, saveDisabled } = this.state;
const { member_id, spans } = this.props;
return () => {
unsubscribes.forEach((u) => u());
};
}, [spans, categoryPeriodsList, canSave]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize useEffect dependencies and subscription management

The current implementation has several potential issues:

  1. Including categoryPeriodsList in the dependency array could cause unnecessary effect reruns
  2. The subscription setup could benefit from better error handling
     useEffect(() => {
         const unsubscribes = [];
+        let isSubscribed = true;  // Add mounted check
 
         // Subscribe to spans updates
         unsubscribes.push(
             spans.subscribe(({ items }) => {
+                if (!isSubscribed) return;  // Prevent updates after unmount
                 categoryPeriodsList.forEach((periods) =>
                     periods.replace(filterPeriods(items, periods.category)),
                 );
             }),
         );
 
         // Subscribe to categoryPeriods updates
         categoryPeriodsList.forEach((cp) => {
+            if (!isSubscribed) return;  // Prevent updates after unmount
             unsubscribes.push(cp.subscribe(() => setSaveDisabled(!canSave())));
         });
 
         return () => {
+            isSubscribed = false;
             unsubscribes.forEach((u) => u());
         };
-    }, [spans, categoryPeriodsList, canSave]);
+    }, [spans, canSave]); // Remove categoryPeriodsList from dependencies
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const unsubscribes = [];
// Subscribe to spans updates
unsubscribes.push(
spans.subscribe(({ items }) => {
categoryPeriodsList.forEach((periods) =>
periods.replace(filterPeriods(items, periods.category)),
);
}),
);
this.categoryPeriodsList.forEach((cp) => {
this.unsubscribe.push(
cp.subscribe(() =>
this.setState({ saveDisabled: !this.canSave() }),
),
);
});
}
componentWillUnmount() {
this.unsubscribe.forEach((u) => u());
}
// Subscribe to categoryPeriods updates
categoryPeriodsList.forEach((cp) => {
unsubscribes.push(cp.subscribe(() => setSaveDisabled(!canSave())));
});
render() {
const { showHistoric, saveDisabled } = this.state;
const { member_id, spans } = this.props;
return () => {
unsubscribes.forEach((u) => u());
};
}, [spans, categoryPeriodsList, canSave]);
useEffect(() => {
const unsubscribes = [];
let isSubscribed = true; // Add mounted check
// Subscribe to spans updates
unsubscribes.push(
spans.subscribe(({ items }) => {
if (!isSubscribed) return; // Prevent updates after unmount
categoryPeriodsList.forEach((periods) =>
periods.replace(filterPeriods(items, periods.category)),
);
}),
);
// Subscribe to categoryPeriods updates
categoryPeriodsList.forEach((cp) => {
if (!isSubscribed) return; // Prevent updates after unmount
unsubscribes.push(cp.subscribe(() => setSaveDisabled(!canSave())));
});
return () => {
isSubscribed = false;
unsubscribes.forEach((u) => u());
};
}, [spans, canSave]); // Remove categoryPeriodsList from dependencies

Comment on lines +33 to +41
const sum_pending_labaccess_days = r.data.reduce(
(acc, value) => {
if (value.action.action === ADD_LABACCESS_DAYS)
return acc + value.action.value;
return acc;
},
0,
);
setPendingLabaccessDays(sum_pending_labaccess_days);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inconsistent variable naming convention

The variable sum_pending_labaccess_days uses snake_case, whereas the rest of the codebase predominantly uses camelCase. For consistency and readability, it's recommended to use camelCase for variable names in JavaScript.

Apply this diff to rename sum_pending_labaccess_days to sumPendingLabaccessDays:

 const sumPendingLabaccessDays = r.data.reduce(
     (acc, value) => {
         if (value.action.action === ADD_LABACCESS_DAYS)
             return acc + value.action.value;
         return acc;
     },
     0,
 );
-setPendingLabaccessDays(sum_pending_labaccess_days);
+setPendingLabaccessDays(sumPendingLabaccessDays);

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (1)
admin/src/Sales/OrderShow.js (1)

37-46: Optimize the subscription callback with useCallback

The subscription callback should be memoized to prevent unnecessary re-renders and potential subscription churn.

Consider applying this optimization:

+ const handleOrderUpdate = useCallback(() => {
+     const { member_id } = order;
+     setMemberId(member_id);
+ }, [order]);

  useEffect(() => {
-     const unsubscribe = order.subscribe(() => {
-         const { member_id } = order;
-         setMemberId(member_id);
-     });
+     const unsubscribe = order.subscribe(handleOrderUpdate);

      return () => {
          unsubscribe();
      };
- }, [order]);
+ }, [order, handleOrderUpdate]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between eaccc32 and ce5f2b0.

📒 Files selected for processing (1)
  • admin/src/Sales/OrderShow.js (1 hunks)
🔇 Additional comments (4)
admin/src/Sales/OrderShow.js (4)

1-11: LGTM! Clean imports and component declaration

The conversion to a functional component follows React best practices with proper hook imports and clean prop destructuring.


54-56: Handle potential null value for memberId to prevent invalid links

If memberId is null or undefined when the component first renders, the <Link> component will generate an invalid URL.


79-79: Prevent potential division by zero when calculating item price

The price calculation could result in a division by zero error if item.count is zero.


61-89: Verify CollectionTable component integration

Let's verify that the CollectionTable component properly handles the collection props and row rendering.

Also applies to: 93-114

✅ Verification successful

CollectionTable integration is properly implemented

The implementation in OrderShow.js correctly uses the CollectionTable component, matching the expected props interface and usage patterns found across the codebase:

  • The required props are properly provided:
    • collection={orderRows} - for data source
    • columns array with correct structure
    • emptyMessage for empty state
    • rowComponent with correct render implementation

The component usage aligns with the standard patterns seen in 25+ other files across the codebase, and the table structure (headers, rows, currency formatting) follows the established conventions.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check CollectionTable implementation and usage patterns

# Find CollectionTable implementation
echo "Searching for CollectionTable implementation..."
ast-grep --pattern 'export $_ CollectionTable' -f js,jsx,ts,tsx

# Find other usages of CollectionTable for comparison
echo "Searching for CollectionTable usage patterns..."
rg -A 5 'CollectionTable' --type js --type jsx --type tsx

Length of output: 539


Script:

#!/bin/bash
# Let's try a different approach to find CollectionTable implementation and usage

# Find CollectionTable implementation
echo "Searching for CollectionTable implementation..."
ast-grep --pattern 'const CollectionTable = $_'

# Find CollectionTable usage with ripgrep
echo "Searching for CollectionTable usage patterns..."
rg "CollectionTable" -A 5

# Find potential files containing CollectionTable
echo "Finding files that might contain CollectionTable..."
fd "CollectionTable" --type f

Length of output: 48464

Comment on lines +70 to +88
rowComponent={({ item }) => (
<tr>
<td>
<Link to={`/sales/product/${item.product_id}`}>
{item.name}
</Link>
</td>
<td className="uk-text-right">
<Currency
value={(100 * item.amount) / item.count}
/>{" "}
kr
</td>
<td>{item.count}</td>
<td className="uk-text-right">
<Currency value={100 * item.amount} /> kr
</td>
</tr>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add key props to table row components

The row components passed to CollectionTable should have key props for optimal rendering performance.

Apply these changes:

- rowComponent={({ item }) => (
+ rowComponent={({ item, key }) => (
-   <tr>
+   <tr key={key}>

This change should be applied to both CollectionTable instances.

Also applies to: 102-113

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (3)
admin/src/Messages/MessageAdd.js (2)

14-19: Add error handling and consider i18n

The save operation lacks error handling and uses hardcoded Swedish text. Consider:

  1. Adding error handling
  2. Implementing internationalization for messages
 const onSend = () => {
     message.save().then(() => {
         router.push("/messages");
         notifySuccess("Ditt meddelande har skickats");
-    });
+    })
+    .catch((error) => {
+        notifyError("Failed to send message");
+        console.error("Failed to save message:", error);
+    });
 };

21-30: Add prop-types and document MessageForm props

The component would benefit from proper prop-types validation and documentation of the MessageForm props interface.

+import PropTypes from 'prop-types';
+
 function MessageAdd(props) {
     // ... component logic ...
 }
+
+MessageAdd.propTypes = {
+    router: PropTypes.shape({
+        push: PropTypes.func.isRequired,
+    }).isRequired,
+};
+
+// Document MessageForm props interface
+/**
+ * @typedef {Object} MessageFormProps
+ * @property {boolean} recipientSelect - Whether to show recipient selection
+ * @property {Message} message - Message instance to edit
+ * @property {Function} onSave - Callback when save is requested
+ */
admin/src/Messages/MessageShow.js (1)

27-55: Consider optimizing the update handler

The update handler could be memoized to prevent unnecessary recreations and potential race conditions.

Consider this optimization:

+    const updateMessage = useMemo(() => () => {
-    const updateMessage = () => {
         setMessage({
             body: messageInstance.body,
             // ... other fields
         });
-    };
+    }, [messageInstance]);

     useEffect(() => {
-        // Call the function once to initialize state
         updateMessage();
         
         const unsubscribe = messageInstance.subscribe(updateMessage);
         return () => unsubscribe();
     }, [messageInstance, updateMessage]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 6b3976a and d69424c.

📒 Files selected for processing (3)
  • admin/src/Membership/SpanShow.jsx (1 hunks)
  • admin/src/Messages/MessageAdd.js (1 hunks)
  • admin/src/Messages/MessageShow.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
admin/src/Messages/MessageShow.js

[error] 96-96: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 100-100: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (7)
admin/src/Messages/MessageAdd.js (1)

14-19: Add cleanup for async operations

The component should handle potential unmounting during the save operation to prevent memory leaks or state updates on unmounted components.

admin/src/Messages/MessageShow.js (4)

1-5: LGTM! Clean imports setup

The imports are well-organized and include all necessary dependencies for the functional component implementation.


93-101: ⚠️ Potential issue

Avoid using dangerouslySetInnerHTML to prevent XSS vulnerabilities.

Using dangerouslySetInnerHTML without sanitization exposes the application to cross-site scripting (XSS) attacks.

The previous review's suggestion to use DOMPurify is still valid and should be implemented to secure the application.

🧰 Tools
🪛 Biome (1.9.4)

[error] 96-96: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 100-100: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


48-54: Verify subscription cleanup across converted components

Given the reported issues with infinite network requests in various pages, ensure that subscription cleanup is working correctly.

Let's check the subscription cleanup pattern across converted components:

✅ Verification successful

Based on the comprehensive search results, I can now generate the final response:

Subscription cleanup is properly implemented across components

The codebase shows consistent and correct implementation of subscription cleanup:

  • All functional components properly use useEffect with cleanup functions
  • Class components correctly implement componentWillUnmount
  • The subscription cleanup pattern follows React best practices:
    • Subscriptions are created in useEffect/componentDidMount
    • Cleanup functions are properly returned/implemented
    • Unsubscribe functions are called in cleanup
    • Dependencies are correctly specified in the dependency arrays
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for subscription cleanup patterns in converted components
rg -A 5 'subscribe.*\(' admin/src/
# Look for useEffect cleanup
ast-grep --pattern 'useEffect(() => {
  $$$
  return () => {
    $$$
  };
}, [$$$]);'

Length of output: 51302


6-25: 🛠️ Refactor suggestion

Add error handling for message retrieval

The component should handle cases where the message retrieval fails or the message doesn't exist. Consider adding error state and handling.

Let's check if there are any error boundaries or error handling patterns in the codebase:

Consider implementing error handling:

 function MessageShow(props) {
     const { id } = props.match.params;
+    const [error, setError] = useState(null);
     const messageInstance = useMemo(() => {
+        try {
             return Message.get(id);
+        } catch (err) {
+            setError(err);
+            return null;
+        }
     }, [id]);
admin/src/Membership/SpanShow.jsx (2)

10-18: ⚠️ Potential issue

Fix infinite requests and add error handling

The current implementation could cause infinite network requests and doesn't handle errors properly.

 useEffect(() => {
+    let isSubscribed = true;
+    setIsLoading(true);
+
+    // Initial data fetch
+    spanInstance.fetch().catch(error => {
+        if (isSubscribed) {
+            setError(error);
+            setIsLoading(false);
+        }
+    });
+
     const unsubscribe = spanInstance.subscribe(() => {
+        if (!isSubscribed) return;
         try {
             setData(spanInstance.saved);
+            setIsLoading(false);
+            setError(null);
         } catch (error) {
+            setError(error);
+            console.error('Error updating span data:', error);
         }
     });

     return () => {
+        isSubscribed = false;
         unsubscribe();
     };
-}, [spanInstance]);
+}, [spanInstance, spanInstance.saved]);

1-32: Verify related components and models

Let's check for any related components or models that might affect the SpanShow component's behavior.

✅ Verification successful

The subscription pattern in SpanShow is correctly implemented

The subscription pattern used in SpanShow.jsx follows the same pattern as seen in other components (MemberBoxSpans, SpanList) and properly handles:

  • Subscription setup using useEffect
  • Cleanup through the unsubscribe function
  • Proper dependency tracking with spanInstance
  • Consistent state management with useState

The Span model is correctly imported and used, and the component is properly integrated into the routing system. No issues were found in the implementation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for related components and models
# Expected: Find components that might affect SpanShow

# Look for components importing SpanShow
rg -l "import.*SpanShow"

# Look for components using the Span model
rg -l "import.*Span.*from.*Models/Span"

# Check for any similar subscription patterns that might have the same issues
ast-grep --pattern 'useEffect(() => {
  $$$
  subscribe($$$)
  $$$
})'

Length of output: 368


Script:

#!/bin/bash
# Let's examine the subscription patterns in the related files

# Check SpanList.jsx for similar subscription patterns
rg "subscribe" admin/src/Membership/SpanList.jsx -B 2 -A 2

# Check MemberBoxSpans.jsx for similar patterns
rg "subscribe" admin/src/Membership/MemberBoxSpans.jsx -B 2 -A 2

# Check the Span model implementation
rg "subscribe" admin/src/Models/Span.js -B 2 -A 2

# Look at the Routes.jsx to see how SpanShow is used
cat admin/src/Membership/Routes.jsx

Length of output: 4383


Script:

#!/bin/bash
# Let's check the Span model implementation and its usage pattern
cat admin/src/Models/Span.js

# Also check MemberBoxSpans to understand the complete usage context
cat admin/src/Membership/MemberBoxSpans.jsx

# And SpanList to see the list implementation
cat admin/src/Membership/SpanList.jsx

Length of output: 13061

Comment on lines +1 to +8
import React, { useMemo } from "react";
import { withRouter } from "react-router";
import MessageForm from "../Components/MessageForm";
import Message from "../Models/Message";
import { notifySuccess } from "../message";
import { withRouter } from "react-router";

class MessageAdd extends React.Component {
constructor(props) {
super(props);
this.message = new Message();
}
function MessageAdd(props) {
const { router } = props;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider upgrading to modern React Router

The use of withRouter HOC and router prop is deprecated in newer versions of React Router. Consider upgrading to use the useNavigate hook instead.

-import { withRouter } from "react-router";
+import { useNavigate } from "react-router-dom";

-function MessageAdd(props) {
-    const { router } = props;
+function MessageAdd() {
+    const navigate = useNavigate();

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +10 to +11
// Create a new Message instance
const message = useMemo(() => new Message(), []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace useMemo with useState for mutable state

The useMemo hook is unnecessary here as the Message instance is mutable state. Use useState instead to properly manage the message state and its updates.

-// Create a new Message instance
-const message = useMemo(() => new Message(), []);
+const [message, setMessage] = useState(() => new Message());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Create a new Message instance
const message = useMemo(() => new Message(), []);
const [message, setMessage] = useState(() => new Message());

Comment on lines +20 to +32
return (
<div>
<h2>Medlemsperiod {data.span_id}</h2>
<dl className="uk-description-list">
{_.keys(data).map((key) => (
<div key={key}>
<dt>{key}:</dt>
<dd>{data[key]}</dd>
</div>
))}
</dl>
</div>
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add data validation and field formatting

The current implementation doesn't validate data or format fields properly, which could cause rendering issues.

 return (
     <div>
-        <h2>Medlemsperiod {data.span_id}</h2>
+        <h2>Medlemsperiod {data?.span_id || 'N/A'}</h2>
         <dl className="uk-description-list">
-            {_.keys(data).map((key) => (
+            {Object.entries(data || {}).map(([key, value]) => (
                 <div key={key}>
                     <dt>{key}:</dt>
-                    <dd>{data[key]}</dd>
+                    <dd>{value?.toString() || 'N/A'}</dd>
                 </div>
             ))}
         </dl>
     </div>
 );

Also, consider adding PropTypes for type checking:

import PropTypes from 'prop-types';

SpanShow.propTypes = {
    match: PropTypes.shape({
        params: PropTypes.shape({
            span_id: PropTypes.string.isRequired
        }).isRequired
    }).isRequired
};

Comment on lines +6 to +8
const { span_id } = props.match.params;
const spanInstance = useMemo(() => Span.get(span_id), [span_id]);
const [data, setData] = useState({});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Initialize data state with loading indicator

The empty initial state {} could cause the blank page issue. Consider adding a loading state and error handling.

-const [data, setData] = useState({});
+const [data, setData] = useState(null);
+const [isLoading, setIsLoading] = useState(true);
+const [error, setError] = useState(null);

+if (isLoading) return <div>Loading...</div>;
+if (error) return <div>Error: {error.message}</div>;
+if (!data) return null;

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1 to +5
import React, { useEffect, useMemo, useState } from "react";
import * as _ from "underscore";
import Span from "../Models/Span";

class SpanShow extends React.Component {
constructor(props) {
super(props);
const { span_id } = props.match.params;
this.span = Span.get(span_id);
this.state = { data: {} };
}

componentDidMount() {
this.unsubscribe = this.span.subscribe(() =>
this.setState({ data: this.span.saved }),
);
}
function SpanShow(props) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Missing error handling and incorrect Base model usage causing infinite requests

The verification reveals several critical issues that explain the reported regressions:

  • The Span model extends Base but doesn't implement proper subscription/data fetching methods. The get() method used in SpanShow is inherited from Base but not visible in the implementation.
  • SpanShow component uses spanInstance.saved without checking if the data is actually loaded or if there are any errors.
  • No error handling exists in the subscription effect, which could lead to unhandled promise rejections.
  • The component doesn't handle loading states, potentially causing the blank page issue.

Key locations needing attention:

  • admin/src/Models/Span.js: Missing proper data fetching implementation
  • admin/src/Membership/SpanShow.jsx: Missing error handling and loading states
  • Need to verify Base model implementation for potential subscription issues
🔗 Analysis chain

Verify the Span model import path and implementation

Let's verify the Span model implementation to ensure it properly handles subscriptions and data fetching.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the Span model implementation
# Expected: Find the Span model class with proper subscription and data fetching logic

# Look for the Span model implementation
ast-grep --pattern 'class Span {
  $$$
  get($_) {
    $$$
  }
  $$$
  subscribe($_) {
    $$$
  }
  $$$
}'

# Check for any references to span.saved
rg -A 5 'span\.saved'

Length of output: 133


Script:

#!/bin/bash
# Let's try a broader search for the Span model and its usage

# Find the Span model file
fd "Span.js" -t f

# Look for class definition with a more relaxed pattern
ast-grep --pattern 'class Span'

# Search for Span-related imports
rg "import.*Span.*from"

# Search for Span usage patterns
rg "new Span\(" -A 3

Length of output: 1478


Script:

#!/bin/bash
# Let's examine the Span model implementation and its usage in SpanShow

# Check the Span model implementation
cat admin/src/Models/Span.js

# Look for subscription or data fetching patterns in SpanShow
cat admin/src/Membership/SpanShow.jsx

# Search for any error handling patterns in SpanShow
rg "catch|error" admin/src/Membership/SpanShow.jsx

Length of output: 6554

Copy link
Member

@emanuelen5 emanuelen5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good! I've reviewed about halfway. I had some comments.

period.subscribe(() => {
this.setState({ start: period.start, end: period.end });
useEffect(() => {
// Synchronize state with period on subscription updates
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

componentWillUnmount() {
this.unsubscribe();
}
// useEffect to handle subscription
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

this.setState({ page, items, loading: false }),
);
}
// Destructure props
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Copy link
Member

@emanuelen5 emanuelen5 Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove all comments in this file

model={keyRef.current}
name="created_at"
title="Skapad"
disabled={true}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disabled does not exist on the DateTimeInput class, you can remove it here and below

</div>
);
if (!group) {
return <div>Group not found</div>; // Handle missing context gracefully
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Comment on lines +24 to +38
useEffect(() => {
get({ url: "/membership/permission" }).then((data) => {
this.options = data.data;
this.setState({ showOptions: this.filterOptions() });
const fetchedOptions = data.data;
setOptions(fetchedOptions);
setShowOptions(filterOptions(fetchedOptions));
});
}

componentDidMount() {
this.unsubscribe = this.collection.subscribe(() =>
this.setState({ showOptions: this.filterOptions() }),
);
}
const unsubscribe = collection.subscribe(() => {
setShowOptions(filterOptions(options));
});

componentWillUnmount() {
this.unsubscribe();
}
return () => {
unsubscribe();
};
}, []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any comment on this? I can't see any problem on http://localhost:8009/membership/groups/1/permissions, but perhaps the Rabbit has a point?

import React from "react";
import Key from "../Models/Key";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useParams, withRouter } from "react-router";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any comment on this? I think the Rabbit might be correct here as well. In that case, you can remove the withRouter import and its usage on line 107

);
}
</div>
);
}

export default withRouter(KeyEdit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can remove withRouter here as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stopped reviewing here. Will continue later

@emanuelen5
Copy link
Member

emanuelen5 commented Dec 9, 2024

Thank you very much for the contributions!

I haven't looked through all pages, but I noted that there are a few regressions:

* [x]  The member spans page is broken: http://localhost:8009/membership/members/1/spans
  Before:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543480-612f71d3-11e3-4d1d-acf2-9ea425687212.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzQ4MC02MTJmNzFkMy0xMWUzLTRkMWQtYWNmMi05ZWE0MjU2ODcyMTIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZTk5NjVkNWFkMTYxYzY0NDM3YmY1MmZlMDNmMjA3NWU4YTZjMjE2NDgwY2ExMzI3MTY3ZGUyM2UwNzQyODYyNyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.PwZBGC77OpMAG9LkXQoInEqNXsVvrS8U6raVCHE9RqE)
  Now: The page is blank with the following errors in the logs ![image](https://private-user-images.githubusercontent.com/7572427/393542894-702a9354-a904-4de2-9d97-deffc3ba0055.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0Mjg5NC03MDJhOTM1NC1hOTA0LTRkZTItOWQ5Ny1kZWZmYzNiYTAwNTUucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9N2U5MWIwZTRmOGI1ZGU3M2VjMTE3MzUzODhjNGYyZmE5YTlmMTQ2NmRiMzYxN2RmMzczMGE0ZDI5YTFiYmUyOCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.GvloMzREIiEHuy69c__LaSPkF4yJBp6X_9dAq_SB_T8)

* [x]  The span info page, http://localhost:8009/membership/spans/1, causes infinite network requests, DoS:ing my computer.

* [x]  The span info page, http://localhost:8009/membership/spans/1, does not render correctly - the fields are not filled in.
  Before:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543548-7ad74a50-c4a0-441b-9892-e214e0cfa962.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzU0OC03YWQ3NGE1MC1jNGEwLTQ0MWItOTg5Mi1lMjE0ZTBjZmE5NjIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZWMwMTQ5YTI0NDNiM2E2NjcyNDk0MDgxOTZlMDc5NTEyNzMzN2Y0ZjhkOGYzOGY2NTE2NGJiZjZlZTEyMzZkMSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.73BspnoWEVALK5sZf_7FkRnFB6P5hAtyXIqjLJzRBNI)
  Now:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543617-7bf7eb8f-c5f7-4a70-b902-0b6be99e9787.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzYxNy03YmY3ZWI4Zi1jNWY3LTRhNzAtYjkwMi0wYjZiZTk5ZTk3ODcucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZDgxOGRlNDRiNDYxNGY3YmJkYmZiM2E0YzRiYTFjYmRkMDFhYzQ2ZGI0N2UxZjA4ODgxNDcyMjE5OTc2YjRlZSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.bXfFJ4xCCJwkqdCK1Klliv5K-phRFBOn2zZGmf-F-Z8)

* [x]  The email log page is blank: http://localhost:8009/messages/1
  Before:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543427-1427cf3d-ef8b-49a1-9bfc-74fcc009a593.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzQyNy0xNDI3Y2YzZC1lZjhiLTQ5YTEtOWJmYy03NGZjYzAwOWE1OTMucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YTlkMjlhNjVkYWYwNzY2MGU1ZWIwYmQ5YWMyMjA0MGZiODNhMWY0YjNmNTAyYmEwNjcwYzc0NTUwNDk1YzIwZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.RHb_8zXrojR6U1yYnVtF1m_aazmNmUC5t29NqALXsng)
  Now:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543677-774b3b3c-e2de-44f4-8310-606be772207c.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzY3Ny03NzRiM2IzYy1lMmRlLTQ0ZjQtODMxMC02MDZiZTc3MjIwN2MucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MDY1NTc4YWUwN2M2ZjA3YWJiYjVkYWRjMzQ3ZWUzOWE3ODFiOTJjMGE1ZTc3M2UwNDE0YzE5MDkwZGM1NzFiOSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.cI66bat1RkyczP6wnlfrUuRtxMcjH8Ua2MZoAcYZJa8)

* [x]  The order page http://localhost:8009/sales/order/1 causes infinite network requests.

* [x]  The order page http://localhost:8009/sales/order/1 does not show the correct member who performed the purchase.
  Before:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543836-16d669ce-87b5-4e91-ae46-add8f731bd22.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0MzgzNi0xNmQ2NjljZS04N2I1LTRlOTEtYWU0Ni1hZGQ4ZjczMWJkMjIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NmUwMGZmY2UyZDhkNTQ0NTQwMzExNzg5YjMyNmI0Y2NlYzQ3MjMzYzI4MjZjZTA1NzE4NTQyNDU1ZjcyMjc4MCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.mObT6k2C6K-AZw2yQng7C-t-5Sf7pgbZ5WmgF1c_phs)
  Now:
  ![image](https://private-user-images.githubusercontent.com/7572427/393543790-a2be89bd-c56b-4e22-ac60-98aa2725d616.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0Mzc5MC1hMmJlODliZC1jNTZiLTRlMjItYWM2MC05OGFhMjcyNWQ2MTYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZTdjNmJmYmI1M2NlMjAwYzk5NTYyNmUyYjczOThiMDdjNmM5MjRmMWY0ZTkzZjY1MmVkNzQyMDdhMmVkZDI2ZCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.h_d2NluoHxaBUKgN4V-fA615wKdmdnE3p3rmHXYybBQ)

* [x]  The gift-card page, http://localhost:8009/sales/gift-card/1, causes infinite network requests.

* [x]  The gift-card page does not render correctly.
  Before:
  ![image](https://private-user-images.githubusercontent.com/7572427/393544044-7bb3251e-e05c-406b-a1a3-002b0522ed59.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0NDA0NC03YmIzMjUxZS1lMDVjLTQwNmItYTFhMy0wMDJiMDUyMmVkNTkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YmI4YTVlOTI1MWZhZTg5MzFmMWMwM2JkZGNlM2ZhMmE5MGMxZDc3ZThjYjI4NWNkNGQwYjBhYmU5NzM4NmQwZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.onj0WS0OdV0sNgP-S_drMMjuH4wnZqVfmI-LipDjRZA)
  Now:
  ![image](https://private-user-images.githubusercontent.com/7572427/393544010-b29acc3a-80fd-4def-bced-34db47b84451.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzM3NzUxNjcsIm5iZiI6MTczMzc3NDg2NywicGF0aCI6Ii83NTcyNDI3LzM5MzU0NDAxMC1iMjlhY2MzYS04MGZkLTRkZWYtYmNlZC0zNGRiNDdiODQ0NTEucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI0MTIwOSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNDEyMDlUMjAwNzQ3WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZTlmMzkxMmRkMjkxYjdkMmNhNDYwMTQwOWE2NTUzMzE4MjFlN2IyZjFjMWUyNTU1MjE3Nzg1MGM5MjQ3N2FkYiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.-sfxLOA-Hdr3dckbVb2Kf2rPHI7eFhKXyKcK-HBaXeE)

#581 (comment)

All of these regressions seem fixed now, thank you!

@emanuelen5 emanuelen5 self-requested a review December 9, 2024 22:12
@ilong4rennes
Copy link
Author

Thank you so much for your detailed feedback, will address these issues later.

@ilong4rennes
Copy link
Author

Hi, sorry for the delay in response, I've updated the code according to your feedback.

  • deleted comments, unused hooks, removed unnecessary withRouter
  • changed from e.preventDefault(); to return false;
  • turned into a controlled component in RequestPasswordReset.jsx

I have not address these two comments:

  1. rename it to DateTimeDisplay in KeyEdit.jsx, I will make a separate PR later
  2. GroupBoxPermissions.jsx: I'll revisit coderabbit comment and update this later.

Thanks again!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Outside diff range and nitpick comments (17)
admin/src/Components/RequestPasswordReset.jsx (2)

14-29: Consider improving error handling and message consistency.

Two suggestions for improvement:

  1. The error message is in English while the UI is in Swedish. Consider translating it to maintain consistency.
  2. Add error handling for the API call to handle potential failures.

Consider updating the code as follows:

 const submit = (e) => {
     e.preventDefault();

     if (!userIdentification) {
-        showError("You need to fill your email or member number.");
+        showError("Du måste fylla i din e-post eller ditt medlemsnummer.");
         return;
     }

-    auth.requestPasswordReset(userIdentification).then(() => {
-        showSuccess(
-            "Link to password reset will be sent to your email shortly.",
-        );
-        browserHistory.push("/");
-    });
+    auth.requestPasswordReset(userIdentification)
+        .then(() => {
+            showSuccess(
+                "En länk för att återställa lösenordet kommer att skickas till din e-post.",
+            );
+            browserHistory.push("/");
+        })
+        .catch((error) => {
+            showError("Det gick inte att skicka återställningslänken. Försök igen senare.");
+        });
 };

31-79: Consider adding accessibility improvements.

The form structure is clean, but could benefit from accessibility enhancements.

Consider these improvements:

 <div className="uk-form-row">
     <div className="uk-form-icon">
         <i className="uk-icon-user" />
         <input
+            aria-label="Email eller medlemsnummer"
             value={userIdentification}
             onChange={handleInputChange}
             className="uk-form-large uk-form-width-large"
             type="text"
             placeholder="Email/Medlemsnummer"
             autoComplete="username"
         />
     </div>
 </div>
admin/src/Components/Textarea.jsx (1)

9-22: Consider optimizing the useEffect dependencies

The current implementation re-creates the subscription whenever props.model or props.name changes. Consider destructuring these values outside useEffect to avoid unnecessary re-subscriptions.

-    useEffect(() => {
-        const { model, name } = props;
+    const { model, name } = props;
+    useEffect(() => {
         const handleModelChange = () => {
             setValue(model[name] === null ? "" : model[name]);
             setIsDirty(model.isDirty(name));
         };
 
         const unsubscribe = model.subscribe(handleModelChange);
+        handleModelChange(); // Initialize the state
 
         return () => {
             unsubscribe();
         };
-    }, [props.model, props.name]);
+    }, [model, name]);
admin/src/Components/MessageForm.jsx (2)

Line range hint 49-74: Optimize loadOptions function with useCallback

The loadOptions function is recreated on every render. Consider memoizing it with useCallback to improve performance, especially since it's passed as a prop to the Async component.

+    const loadOptions = useCallback((inputValue, callback) => {
-    const loadOptions = (inputValue, callback) => {
         Promise.all([
             // ... existing code ...
         ]);
-    };
+    }, []);

98-101: Simplify recipients onChange handler

The current implementation updates both the model and local state. Since the model subscription will trigger a state update, the manual state update is redundant.

             onChange={(values) => {
                 message.recipients = values;
-                setRecipients(values);
             }}
admin/src/Components/MemberForm.js (1)

73-78: Reconsider number type for zipcode field

Using type="number" for zipcodes can be problematic as it:

  • Removes leading zeros
  • Prevents entering spaces or dashes
  • Shows spinner controls which are unnecessary for postal codes
-                        type="number"
+                        type="text"
+                        pattern="[0-9 -]*"
admin/src/Components/CollectionTable.js (4)

Line range hint 71-115: Improve pagination accessibility and performance

The pagination implementation has room for improvements:

  1. Missing ARIA labels for better accessibility
  2. Inline function creation in onClick could cause unnecessary re-renders
  3. Missing keyboard navigation support

Consider these improvements:

 <ul className="uk-pagination" style={{ clear: "both" }}>
     {_.range(1, page.count + 1).map((i) => {
         const distance = Math.abs(i - page.index);
         if (distance === 0) {
             return (
-                <li key={i} className="uk-active">
-                    <span>{i}</span>
+                <li key={i} className="uk-active" aria-current="page">
+                    <span aria-label={`Current Page, Page ${i}`}>{i}</span>
                 </li>
             );
         } else if (
             distance <= show_count ||
             i === 1 ||
             i === page.count
         ) {
             return (
                 <li key={i}>
                     <a
+                        role="button"
+                        tabIndex={0}
+                        aria-label={`Go to Page ${i}`}
                         onClick={() => {
                             setLoading(true);
                             if (onPageNav) onPageNav(i);
                         }}
+                        onKeyPress={(e) => {
+                            if (e.key === 'Enter' || e.key === ' ') {
+                                setLoading(true);
+                                if (onPageNav) onPageNav(i);
+                            }
+                        }}
                     >
                         {i}
                     </a>
                 </li>
             );
         }
     })}
 </ul>

117-124: Consider adding user feedback for delete operations

While the delete operation handles the basic flow, it would be beneficial to provide feedback to users about the success or failure of the operation.

Consider adding a toast notification:

 const deleteItem = (collection, item) => {
     return confirmModal(item.deleteConfirmMessage())
         .then(() => item.del())
         .then(
-            () => collection.fetch(),
+            () => {
+                collection.fetch();
+                // Assuming you have a notification system
+                props.showNotification?.({
+                    message: 'Item successfully deleted',
+                    type: 'success'
+                });
+            },
-            () => null,
+            (error) => {
+                props.showNotification?.({
+                    message: 'Failed to delete item',
+                    type: 'error'
+                });
+                return null;
+            }
         );
 };

157-167: Enhance table accessibility

The table is missing important accessibility attributes that would make it more usable for screen readers.

Consider adding these accessibility improvements:

 <table
     className={
         "uk-table uk-table-condensed uk-table-striped uk-table-hover" +
         (loading ? " backboneTableLoading" : "")
     }
+    role="grid"
+    aria-busy={loading}
+    aria-label="Collection Data Table"
 >
-    <thead>
+    <thead role="rowgroup">
-        <tr>{headers}</tr>
+        <tr role="row">{headers}</tr>
     </thead>
-    <tbody>{rows}</tbody>
+    <tbody role="rowgroup">{rows}</tbody>
 </table>

172-173: Internationalize loading message

The loading message is hardcoded in Swedish. Consider using an internationalization system for better maintainability and localization support.

-<i className="uk-icon-refresh uk-icon-spin" /> Hämtar data...
+<i className="uk-icon-refresh uk-icon-spin" /> {t('common.loading')}
admin/src/Components/FileInput.jsx (1)

40-40: Add PropTypes for better development experience

Consider adding PropTypes to document and validate the component's props.

+import PropTypes from 'prop-types';

 const FileInput = ({ onFileSelect, acceptedFileTypes, maxFileSize }) => {
   // ... component implementation
 };

+FileInput.propTypes = {
+  onFileSelect: PropTypes.func,
+  acceptedFileTypes: PropTypes.string,
+  maxFileSize: PropTypes.number
+};

 export default FileInput;
admin/src/Membership/KeyEdit.jsx (1)

1-105: Well-executed component conversion with room for enhancement

The conversion from class to function component is well done, properly utilizing React hooks and maintaining the original functionality. Consider these architectural improvements for future iterations:

  1. Add a custom hook for key management to encapsulate the subscription logic
  2. Implement proper error boundaries for error handling
  3. Consider adding loading states for async operations
admin/src/Components/DatePeriodInput.js (2)

1-5: Add PropTypes validation for the period prop.

Consider adding PropTypes to validate the shape and required properties of the period prop, which would help catch potential issues during development.

import React, { useEffect, useState } from "react";
import DayPickerInput from "react-day-picker/DayPickerInput";
import { formatUtcDate, parseUtcDate, utcToday } from "../utils";
+import PropTypes from 'prop-types';

const DatePeriodInput = ({ period }) => {

Add at the bottom:

+DatePeriodInput.propTypes = {
+  period: PropTypes.shape({
+    start: PropTypes.instanceOf(Date),
+    end: PropTypes.instanceOf(Date),
+    subscribe: PropTypes.func.isRequired,
+    isDirty: PropTypes.func.isRequired,
+    isValid: PropTypes.func.isRequired
+  }).isRequired
+};

31-74: Enhance accessibility and reduce code duplication.

  1. The DayPickerInput components share similar props and structure. Consider extracting a reusable component.
  2. Add proper ARIA labels for better accessibility.
+const DateInput = ({ value, onChange, isDirty, isValid, isHistoric }) => (
+    <span className={isDirty ? "ma-changed" : ""}>
+        <DayPickerInput
+            inputProps={{
+                size: 10,
+                className: "uk-input" + (isValid ? "" : " uk-form-danger"),
+                style: {
+                    marginTop: "2px",
+                    ...(isHistoric && historicStyle),
+                },
+                "aria-label": `${isDirty ? "Modified " : ""}date input`
+            }}
+            placeholder="YYYY-MM-DD"
+            value={value || ""}
+            parseDate={parseUtcDate}
+            formatDate={formatUtcDate}
+            onDayChange={onChange}
+        />
+    </span>
+);

 return (
     <span>
-        {/* Input for start date */}
-        <span className={period.isDirty("start") ? "ma-changed" : ""}>
-            <DayPickerInput
-                inputProps={{...}}
-                placeholder="YYYY-MM-DD"
-                value={start || ""}
-                parseDate={parseUtcDate}
-                formatDate={formatUtcDate}
-                onDayChange={(date) => handleDayChange(date, "start")}
-            />
-        </span>
+        <DateInput
+            value={start}
+            onChange={(date) => handleDayChange(date, "start")}
+            isDirty={period.isDirty("start")}
+            isValid={period.isValid("start")}
+            isHistoric={start && start < today}
+        />
         &nbsp;-&nbsp;
-        {/* Input for end date */}
-        <span className={period.isDirty("end") ? "ma-changed" : ""}>
-            <DayPickerInput {...similar props} />
-        </span>
+        <DateInput
+            value={end}
+            onChange={(date) => handleDayChange(date, "end")}
+            isDirty={period.isDirty("end")}
+            isValid={period.isValid("end")}
+            isHistoric={end && end < today}
+        />
     </span>
 );
admin/src/Components/SelectInput.jsx (1)

11-22: Consider enhancing data fetching with abort controller

While the data fetching logic is correct, it could benefit from proper cleanup and error handling.

Consider applying this improvement:

 useEffect(() => {
+    const controller = new AbortController();
     if (props.dataSource) {
         let newOptions = [];
         if (props.nullOption) {
             newOptions = [props.nullOption];
         }
-        get({ url: props.dataSource, params: { page_size: 0 } }).then(
+        get({ 
+            url: props.dataSource, 
+            params: { page_size: 0 },
+            signal: controller.signal 
+        }).then(
             (data) => setOptions(newOptions.concat(data.data)),
-            () => null,
+            (error) => {
+                if (!error.name === 'AbortError') {
+                    console.error('Failed to fetch options:', error);
+                }
+            }
         );
     }
+    return () => controller.abort();
 }, [props.dataSource, props.nullOption]);
admin/src/Components/ProductForm.jsx (2)

96-99: Simplify form submission handling

The current implementation uses both return false and e.preventDefault() which is redundant.

Choose one approach:

 onSubmit={(e) => {
-    onSave();
-    return false;
+    e.preventDefault();
+    onSave();
 }}

Or:

 onSubmit={(e) => {
     onSave();
     return false;
 }}

101-259: Consider extracting common fieldset patterns

The form has multiple fieldsets with similar structures. Consider extracting a reusable FormSection component to reduce repetition and improve maintainability.

Example implementation:

const FormSection = ({ icon, title, children }) => (
    <fieldset className="uk-margin-top">
        <legend>
            <i className={`uk-icon-${icon}`} /> {title}
        </legend>
        {children}
    </fieldset>
);

Usage:

<FormSection icon="shopping-cart" title="Produkt">
    <TextInput model={product} name="name" title="Produktnamn" />
    {/* ... other inputs */}
</FormSection>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d69424c and f82d9d8.

📒 Files selected for processing (13)
  • admin/src/Components/CollectionTable.js (5 hunks)
  • admin/src/Components/DatePeriodInput.js (1 hunks)
  • admin/src/Components/FileInput.jsx (1 hunks)
  • admin/src/Components/MemberForm.js (1 hunks)
  • admin/src/Components/MessageForm.jsx (3 hunks)
  • admin/src/Components/ProductForm.jsx (1 hunks)
  • admin/src/Components/RequestPasswordReset.jsx (1 hunks)
  • admin/src/Components/SearchBox.jsx (1 hunks)
  • admin/src/Components/SelectInput.jsx (1 hunks)
  • admin/src/Components/TextInput.jsx (1 hunks)
  • admin/src/Components/Textarea.jsx (1 hunks)
  • admin/src/Membership/GroupBoxEditInfo.jsx (1 hunks)
  • admin/src/Membership/KeyEdit.jsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • admin/src/Components/SearchBox.jsx
  • admin/src/Membership/GroupBoxEditInfo.jsx
🧰 Additional context used
📓 Learnings (1)
admin/src/Membership/KeyEdit.jsx (1)
Learnt from: emanuelen5
PR: makerspace/makeradmin#581
File: admin/src/Membership/KeyEdit.jsx:2-2
Timestamp: 2024-12-09T22:05:34.237Z
Learning: In React components using React Router v6, if hooks like `useParams` are used, the `withRouter` higher-order component is unnecessary and should be removed to simplify the code.
🪛 Biome (1.9.4)
admin/src/Components/Textarea.jsx

[error] 52-52: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/TextInput.jsx

[error] 56-57: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/ProductForm.jsx

[error] 35-35: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

admin/src/Components/SelectInput.jsx

[error] 52-52: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (23)
admin/src/Components/RequestPasswordReset.jsx (2)

1-7: LGTM! Clean imports and modern component declaration.

The component has been properly converted to a functional component with appropriate imports.


8-12: LGTM! Clean state management implementation.

The component has been properly converted to use controlled input with useState.

admin/src/Components/Textarea.jsx (1)

52-52: 🛠️ Refactor suggestion

Refactor the onChange handler for better maintainability

The current implementation directly assigns to the model within an expression, which can be confusing and harder to maintain.

-            onChange={(e) => (model[name] = e.target.value)}
+            onChange={(e) => {
+                model[name] = e.target.value;
+                handleModelChange();
+            }}
🧰 Tools
🪛 Biome (1.9.4)

[error] 52-52: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/TextInput.jsx (1)

55-58: 🛠️ Refactor suggestion

Simplify the onChange handler and add model update trigger

The current implementation has a complex nested structure and lacks a model update trigger.

-            onChange={(event) =>
-                (model[name] =
-                    event.target.value === "" ? null : event.target.value)
-            }
+            onChange={(event) => {
+                const newValue = event.target.value === "" ? null : event.target.value;
+                model[name] = newValue;
+                handleModelChange();
+            }}
🧰 Tools
🪛 Biome (1.9.4)

[error] 56-57: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/MemberForm.js (3)

1-18: LGTM! Clean hooks implementation

The conversion to hooks is well-implemented with proper cleanup and dependency management.


24-27: 🛠️ Refactor suggestion

Modernize form submission handling

The current approach of returning false is an older pattern. Modern React code should explicitly prevent the default form action.

-                    onSave();
-                    return false;
+                    e.preventDefault();
+                    onSave();

Likely invalid or redundant comment.


10-13: Verify subscription behavior to prevent infinite renders

Given the reported regressions in the PR with infinite network requests, let's verify the member subscription implementation.

admin/src/Components/FileInput.jsx (2)

6-8: ⚠️ Potential issue

Implement file upload handlers

The component is missing essential functions for handling file selection and upload.

 const clearUpload = () => {
     setFilename("");
+    setUploadProgress(0);
+    setUploadStatus('idle');
+    setErrorMessage('');
 };

+const handleFileSelect = (event) => {
+    const file = event.target.files[0];
+    if (file) {
+        setFilename(file.name);
+        onFileSelect?.(file);
+    }
+};

Likely invalid or redundant comment.


1-40: Verify component usage patterns

Let's check how this component is used across the codebase to ensure our changes are compatible with existing implementations.

admin/src/Membership/KeyEdit.jsx (6)

1-9: LGTM! Imports correctly updated for functional component

The imports have been properly updated to include necessary hooks and the direct import of useParams without withRouter, aligning with React Router v6 best practices.


52-63: Consider renaming DateTimeInput to DateTimeDisplay

Based on previous feedback, DateTimeInput is actually a readonly component.

Consider creating a new issue to rename this component to DateTimeDisplay since it's readonly. This would make the component's purpose clearer.


105-105: LGTM! Clean export without HOC

The export statement has been correctly updated to remove the withRouter HOC, aligning with modern React Router practices.


24-26: 🛠️ Refactor suggestion

Add error handling to onSave

The onSave function should include error handling similar to onDelete.

 const onSave = useCallback(() => {
-    keyRef.current.save();
+    keyRef.current.save()
+        .then(() => {
+            // Consider showing a success message
+        })
+        .catch((error) => {
+            // Handle save errors
+            console.error('Failed to save key:', error);
+        });
 }, []);

15-22: Verify subscription cleanup implementation

The subscription setup and cleanup look correct. Let's verify the unsubscribe implementation in the Key model.

✅ Verification successful

Based on the search results, I can see a clear pattern of subscription cleanup implementation across the codebase. The subscribe method is defined in admin/src/Models/Base.js which returns an unsubscribe function that removes the subscriber from the subscribers list. This pattern is consistently used throughout the codebase, including in the file under review.

Let's verify the implementation in the Base model to ensure the cleanup is properly handled:


Let me try a different approach to get the Base model implementation:


Subscription cleanup implementation is correct

The subscription cleanup in the KeyEdit.jsx component is properly implemented. The Base model's subscribe method returns a cleanup function that removes the subscriber from the list, and the component correctly uses this cleanup function in the useEffect's return callback. This pattern is consistently used across the codebase and follows React's best practices for cleanup.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for subscribe/unsubscribe implementation
rg -A 5 "subscribe\(|unsubscribe\(" 

Length of output: 25249


Script:

#!/bin/bash
# Get the Base model implementation
ast-grep --pattern 'subscribe(callback) {
  $$$
}'

Length of output: 52


Script:

#!/bin/bash
# Get the Base model implementation with more context
rg -A 10 "subscribe\(callback\)" admin/src/Models/Base.js

Length of output: 452


10-13: Verify the Key instance lifecycle management

The Key instance is stored in a ref and initialized only once. Let's verify that this doesn't cause any issues with updates or re-renders.

✅ Verification successful

Let me gather more information about the Base model and any potential update mechanisms.


Let me try one more search with corrected patterns:


Based on the gathered information, I can now provide the final response:

The Key instance ref usage is implemented correctly

The implementation is safe because:

  • The Key instance is properly initialized using Key.get() which fetches fresh data via refresh()
  • The component subscribes to Key model changes in useEffect and properly cleans up the subscription
  • The Base model (parent of Key) has proper refresh and update mechanisms
  • The ref is used to maintain a stable reference to the Key instance across re-renders while allowing the instance itself to be mutable
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other components using Key.get to understand the pattern
ast-grep --pattern 'Key.get($_)'

# Search for Key model implementation
rg -A 10 "class Key"

Length of output: 2121


Script:

#!/bin/bash
# Search for Base model implementation
rg -A 10 "class Base extends|class Base {" 

# Look for any update or refresh methods in Key or Base
ast-grep --pattern 'update($$$)'

# Search for Key.get implementation
ast-grep --pattern 'get($_) {
  $$$
}'

# Look for useEffect usage with Key
rg -A 5 "useEffect.*Key"

Length of output: 397


Script:

#!/bin/bash
# Search for Base model implementation with fixed pattern
rg -A 10 "^class Base"

# Look for model update patterns in the specific files
rg -A 5 "update|refresh" admin/src/Models/

# Search for effects in KeyEdit
rg -A 5 "useEffect" admin/src/Membership/KeyEdit.jsx

# Look for Key.get static method
rg -A 5 "static get" admin/src/Models/Key.js

Length of output: 9481

admin/src/Components/DatePeriodInput.js (2)

6-16: LGTM! Clean implementation of state and effects.

The implementation correctly:

  • Initializes state with period values
  • Sets up subscription with proper cleanup
  • Uses appropriate dependency array

9-16: Verify subscription handling to prevent infinite requests.

Given the reported regressions in the PR comments about infinite network requests, we should verify that the subscription handling in this component isn't contributing to the issue.

admin/src/Components/SelectInput.jsx (3)

2-9: LGTM! Clean conversion to function component

The component setup follows React best practices with proper hooks usage and state initialization.


24-37: LGTM! Well-implemented subscription management

The subscription setup and cleanup are properly handled with appropriate dependencies.


52-52: ⚠️ Potential issue

Avoid assignment within an expression in the onChange handler

The assignment within the expression can lead to confusion and side effects.

Apply this improvement:

-onChange={(o) => (props.model[props.name] = props.getValue(o))}
+onChange={(o) => {
+    props.model[props.name] = props.getValue(o);
+}}
🧰 Tools
🪛 Biome (1.9.4)

[error] 52-52: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

admin/src/Components/ProductForm.jsx (3)

12-22: LGTM! Well-structured utility functions

The utility functions are well-implemented with:

  • Modern array methods replacing lodash dependencies
  • Proper null checks and fallback logic
  • Clear single responsibilities

53-63: LGTM! Well-implemented event handlers

The handlers are concise, focused, and include proper checks before mutations.


34-37: ⚠️ Potential issue

Fix variable usage before declaration

The variable selectedActionType is being used before it's declared.

Apply this fix:

-            const selectedActionType = filterSelectedActionType(
+            const newSelectedActionType = filterSelectedActionType(
                 selectedActionType,
                 newAvailableActionTypes,
             );
🧰 Tools
🪛 Biome (1.9.4)

[error] 35-35: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

model={message}
name="subject"
title="Ärende"
onChange={() => setBodyLength(message.body.length)} // Ensure the length is updated when body changes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consolidate duplicate onChange handlers

The same onChange handler is duplicated for both TextInput and Textarea components.

+    const handleBodyChange = useCallback(() => {
+        setBodyLength(message.body.length);
+    }, [message.body]);

     <TextInput
         model={message}
         name="subject"
         title="Ärende"
-        onChange={() => setBodyLength(message.body.length)}
+        onChange={handleBodyChange}
     />
     <Textarea
         model={message}
         name="body"
         title="Meddelande"
-        onChange={() => setBodyLength(message.body.length)}
+        onChange={handleBodyChange}
     />

Also applies to: 117-117

Comment on lines +130 to +135
<a
className="uk-button uk-button-danger uk-float-left"
onClick={onDelete}
>
<i className="uk-icon-save" />{" "}
{member.id ? "Spara" : "Skapa"}
</button>
</div>
</form>
</div>
);
}
}
<i className="uk-icon-trash" /> Ta bort medlem
</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve delete button accessibility and semantics

The delete action uses an anchor tag (<a>) with onClick, which:

  • Isn't semantic HTML (anchors are for navigation)
  • May cause accessibility issues
  • Doesn't prevent default action
-                        <a
-                            className="uk-button uk-button-danger uk-float-left"
-                            onClick={onDelete}
+                        <button
+                            type="button"
+                            className="uk-button uk-button-danger uk-float-left"
+                            onClick={(e) => {
+                                e.preventDefault();
+                                onDelete();
+                            }}
-                        </a>
+                        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a
className="uk-button uk-button-danger uk-float-left"
onClick={onDelete}
>
<i className="uk-icon-save" />{" "}
{member.id ? "Spara" : "Skapa"}
</button>
</div>
</form>
</div>
);
}
}
<i className="uk-icon-trash" /> Ta bort medlem
</a>
<button
type="button"
className="uk-button uk-button-danger uk-float-left"
onClick={(e) => {
e.preventDefault();
onDelete();
}}
>
<i className="uk-icon-trash" /> Ta bort medlem
</button>

Comment on lines +20 to +29
useEffect(() => {
const unsubscribe = collection.subscribe(({ page, items }) => {
setPage(page);
setItems(items);
setLoading(false);
});
return () => {
unsubscribe();
};
}, [collection]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add cleanup and optimize the collection subscription effect

The current implementation has a few potential improvements:

  1. Missing cleanup of state when collection changes
  2. Hook dependencies could be optimized

Consider this implementation:

 useEffect(() => {
+    // Reset state when collection changes
+    setPage({});
+    setItems(null);
+    setLoading(true);
+    
     const unsubscribe = collection.subscribe(({ page, items }) => {
         setPage(page);
         setItems(items);
         setLoading(false);
     });
     return () => {
         unsubscribe();
     };
 }, [collection]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const unsubscribe = collection.subscribe(({ page, items }) => {
setPage(page);
setItems(items);
setLoading(false);
});
return () => {
unsubscribe();
};
}, [collection]);
useEffect(() => {
// Reset state when collection changes
setPage({});
setItems(null);
setLoading(true);
const unsubscribe = collection.subscribe(({ page, items }) => {
setPage(page);
setItems(items);
setLoading(false);
});
return () => {
unsubscribe();
};
}, [collection]);

Comment on lines +128 to +135
rows = items.map((item, i) => (
<React.Fragment key={i}>
{rowComponent({
item,
deleteItem: () => deleteItem(collection, item),
})}
</React.Fragment>
));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid using array indices as React keys

Using array indices as keys can lead to performance issues and bugs with item reordering or deletion.

Consider using a unique identifier from the item:

-rows = items.map((item, i) => (
-    <React.Fragment key={i}>
+rows = items.map((item) => (
+    <React.Fragment key={item.id || item.uniqueIdentifier}>

Committable suggestion skipped: line range outside the PR's diff.

import React, { useState } from "react";

const FileInput = () => {
const [filename, setFilename] = useState("");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing state for upload handling

The component needs additional state variables to manage the upload process and provide feedback to users.

 const [filename, setFilename] = useState("");
+const [uploadProgress, setUploadProgress] = useState(0);
+const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, success, error
+const [errorMessage, setErrorMessage] = useState('');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [filename, setFilename] = useState("");
const [filename, setFilename] = useState("");
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadStatus, setUploadStatus] = useState('idle'); // idle, uploading, success, error
const [errorMessage, setErrorMessage] = useState('');

Comment on lines +25 to +30
<input
id="upload-select"
className="uk-hidden"
type="file"
/>
</a>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Connect file input and add upload feedback UI

The file input needs to be connected to handlers and the UI should show upload progress and errors.

 <input
     id="upload-select"
     className="uk-hidden"
     type="file"
+    onChange={handleFileSelect}
+    accept={acceptedFileTypes}
 />
+{uploadStatus === 'uploading' && (
+    <div className="uk-margin-small-top">
+        <progress className="uk-progress" value={uploadProgress} max="100" />
+    </div>
+)}
+{uploadStatus === 'error' && (
+    <div className="uk-alert uk-alert-danger">{errorMessage}</div>
+)}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1 to +3
import React, { useState } from "react";

const FileInput = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing props for component configuration

The component should accept props for configuring the upload behavior and handling file selection callbacks. Consider adding props like onFileSelect, acceptedFileTypes, etc.

-const FileInput = () => {
+const FileInput = ({ onFileSelect, acceptedFileTypes, maxFileSize }) => {

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +18 to +26
const handleDayChange = (date, type) => {
if (type === "start") {
period.start = date || null;
setStart(date || null);
} else if (type === "end") {
period.end = date || null;
setEnd(date || null);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding date validation and avoiding direct mutations.

  1. The direct mutation of the period object (period.start = date) could lead to unexpected behavior. Consider using a proper update method if provided by the period object.

  2. There's no validation to ensure the end date isn't before the start date.

 const handleDayChange = (date, type) => {
+    // Validate date range
+    if (type === "end" && date && start && date < start) {
+        return; // or show error message
+    }
+    if (type === "start" && date && end && date > end) {
+        return; // or show error message
+    }
+
     if (type === "start") {
-        period.start = date || null;
+        // Consider using period.updateStart(date) if available
+        period.start = date || null;
         setStart(date || null);
     } else if (type === "end") {
+        // Consider using period.updateEnd(date) if available
         period.end = date || null;
         setEnd(date || null);
     }
 };

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +11 to +22
useEffect(() => {
if (props.dataSource) {
let options = [];
let newOptions = [];
if (props.nullOption) {
options = [props.nullOption];
newOptions = [props.nullOption];
}
get({ url: props.dataSource, params: { page_size: 0 } }).then(
(data) => this.setState({ options: options.concat(data.data) }),
(data) => setOptions(newOptions.concat(data.data)),
() => null,
);
}
}
}, [props.dataSource, props.nullOption]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Network request in SelectInput lacks cleanup, unlike other components

The search results show that while most components in the codebase properly implement cleanup for subscriptions, the SelectInput component's useEffect with the network request doesn't handle cleanup for potential in-flight requests. This could lead to memory leaks or race conditions if the component unmounts while the request is pending.

  • admin/src/Components/SelectInput.jsx: Add cleanup to the network request useEffect:
useEffect(() => {
    if (props.dataSource) {
        let aborted = false;
        let newOptions = [];
        if (props.nullOption) {
            newOptions = [props.nullOption];
        }
        get({ url: props.dataSource, params: { page_size: 0 } }).then(
            (data) => !aborted && setOptions(newOptions.concat(data.data)),
            () => null,
        );
        return () => { aborted = true; };
    }
}, [props.dataSource, props.nullOption]);
🔗 Analysis chain

Verify cleanup of subscriptions and network requests

Given the reported issues with infinite network requests in other components, let's verify the cleanup implementation across similar components.

Also applies to: 24-37


Let me try a different search pattern to find potential cleanup issues in useEffect hooks.

The previous pattern might have been too specific. This new search will help us:

  1. Find all useEffect hooks that make network requests
  2. Find any cleanup functions implemented in useEffect hooks
    This will give us a better picture of the cleanup implementation across components.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for components with potential cleanup issues
echo "Searching for components with useEffect and network requests..."
ast-grep --pattern 'useEffect(() => {
  $$$
  get({
    $$$
  })
  $$$
})' admin/src/Components/

echo "Searching for components with useEffect and subscriptions..."
ast-grep --pattern 'useEffect(() => {
  $$$
  subscribe($$$)
  $$$
})' admin/src/Components/

Length of output: 461


Script:

#!/bin/bash
# Search for all useEffect hooks in Components directory
echo "Searching for useEffect hooks with network requests..."
rg -U "useEffect\(.+\{[\s\S]+?get\(.+?\)[\s\S]+?\}.*\)" admin/src/Components/ -A 5

echo "Searching for cleanup functions in useEffect..."
rg -U "useEffect\(.+\{[\s\S]+?return\s+\(*\s*\(*\)\s*=>\s*\{[\s\S]+?\}" admin/src/Components/

Length of output: 12610

Comment on lines +30 to +51
useEffect(() => {
const handleProductChange = () => {
const newActions = product.actions;
const newAvailableActionTypes = filterAvailableActions(newActions);
const selectedActionType = filterSelectedActionType(
selectedActionType,
newAvailableActionTypes,
);

setActions(newActions);
setAvailableActionTypes(newAvailableActionTypes);
setSelectedActionType(selectedActionType);
setSaveDisabled(!product.canSave());
};
}

componentDidMount() {
const { product } = this.props;
this.unsubscribe = product.subscribe(() =>
this.setState(productChanged),
);
}
const unsubscribe = product.subscribe(handleProductChange);
handleProductChange();

componentWillUnmount() {
this.unsubscribe();
}
return () => {
unsubscribe();
};
}, [product, selectedActionType]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential infinite loop in useEffect

The useEffect hook includes selectedActionType in its dependency array while also updating it inside the effect. This can cause an infinite loop of updates.

Apply this fix to remove the unnecessary dependency:

    useEffect(() => {
        const handleProductChange = () => {
            const newActions = product.actions;
            const newAvailableActionTypes = filterAvailableActions(newActions);
            const newSelectedActionType = filterSelectedActionType(
                selectedActionType,
                newAvailableActionTypes,
            );

            setActions(newActions);
            setAvailableActionTypes(newAvailableActionTypes);
            setSelectedActionType(newSelectedActionType);
            setSaveDisabled(!product.canSave());
        };

        const unsubscribe = product.subscribe(handleProductChange);
        handleProductChange();

        return () => {
            unsubscribe();
        };
-    }, [product, selectedActionType]);
+    }, [product]); // selectedActionType is not needed in dependencies
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const handleProductChange = () => {
const newActions = product.actions;
const newAvailableActionTypes = filterAvailableActions(newActions);
const selectedActionType = filterSelectedActionType(
selectedActionType,
newAvailableActionTypes,
);
setActions(newActions);
setAvailableActionTypes(newAvailableActionTypes);
setSelectedActionType(selectedActionType);
setSaveDisabled(!product.canSave());
};
}
componentDidMount() {
const { product } = this.props;
this.unsubscribe = product.subscribe(() =>
this.setState(productChanged),
);
}
const unsubscribe = product.subscribe(handleProductChange);
handleProductChange();
componentWillUnmount() {
this.unsubscribe();
}
return () => {
unsubscribe();
};
}, [product, selectedActionType]);
useEffect(() => {
const handleProductChange = () => {
const newActions = product.actions;
const newAvailableActionTypes = filterAvailableActions(newActions);
const selectedActionType = filterSelectedActionType(
selectedActionType,
newAvailableActionTypes,
);
setActions(newActions);
setAvailableActionTypes(newAvailableActionTypes);
setSelectedActionType(selectedActionType);
setSaveDisabled(!product.canSave());
};
const unsubscribe = product.subscribe(handleProductChange);
handleProductChange();
return () => {
unsubscribe();
};
}, [product]); // selectedActionType is not needed in dependencies
🧰 Tools
🪛 Biome (1.9.4)

[error] 35-35: This variable is used before its declaration.

The variable is declared here:

(lint/correctness/noInvalidUseBeforeDeclaration)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
legacy Old stuff that might need updating or refactoring
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Convert React components to functions
4 participants