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

feat(apps/manage-frontend): auto-save of user inputs in element edit modal #4474

Merged
merged 10 commits into from
Jan 23, 2025

Conversation

sjschlapbach
Copy link
Member

@sjschlapbach sjschlapbach commented Jan 23, 2025

Summary by CodeRabbit

  • New Features

    • Introduced auto-save functionality for question creation and editing.
    • Added data recovery prompt for unsaved changes.
    • Implemented local storage-based temporary data saving.
  • Localization

    • Added German and English translations for data recovery messages.
  • Bug Fixes

    • Improved handling of unsaved form data.
  • Chores

    • Updated GitHub Actions workflow artifact upload version.
    • Updated Cypress test suite to support new auto-save features.

Copy link

aviator-app bot commented Jan 23, 2025

Current Aviator status

Aviator will automatically update this comment as the status of the PR changes.
Comment /aviator refresh to force Aviator to re-examine your PR (or learn about other /aviator commands).

This PR was merged manually (without Aviator). Merging manually can negatively impact the performance of the queue. Consider using Aviator next time.


See the real-time status of this PR on the Aviator webapp.
Use the Aviator Chrome Extension to see the status of your PR within GitHub.

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 23, 2025
Copy link

coderabbitai bot commented Jan 23, 2025

📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive auto-save functionality for question and element management in the frontend application. The changes span multiple components and files, implementing a new AutoSaveMonitor component, adding a RecoveryPrompt component, and updating internationalization (i18n) messages for data recovery. The implementation allows users to recover unsaved form data, provides visual feedback during saving, and enhances the overall user experience when creating or editing questions and elements.

Changes

File Change Summary
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts Added new AutoSaveMonitor component for managing auto-saving of form values.
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx Integrated auto-save functionality, added local storage management, and modified initial values handling.
apps/frontend-manage/src/pages/index.tsx Added showRecoveryPrompt state and logic for handling auto-save recovery.
apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx Created new component for displaying data recovery modal.
apps/frontend-manage/src/components/questions/Question.tsx Added auto-save recovery logic for question editing.
packages/i18n/messages/de.ts & packages/i18n/messages/en.ts Added new translation messages for data recovery and temporary storage.
github/workflows/cypress-testing.yml Updated Cypress workflow to use actions/upload-artifact@v4.
apps/frontend-manage/src/components/questions/manipulation/ElementContentInput.tsx Removed autoFocus prop from ContentInput component.
cypress/cypress/e2e/B-feature-access-workflow.cy.ts Changed selector for closing modal from close-question-modal to close-element-modal.
cypress/cypress/e2e/D-questions-workflow.cy.ts Renamed modal close buttons and added tests for auto-save functionality.
cypress/cypress/fixtures/D-questions.json Added new section for auto-saving functionality in test data.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • rschlaefli
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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 comments (1)
apps/frontend-manage/src/pages/index.tsx (1)

Line range hint 266-268: Add error handling for query and mutation errors.

The component doesn't handle query errors or display error messages to users when operations fail.

Add error handling:

  {!dataQuestions || loadingQuestions ? (
    <Loader />
+ ) : errorQuestions ? (
+   <div className="flex items-center justify-center p-4 text-red-600">
+     {t('manage.general.errorLoadingQuestions')}
+   </div>
  ) : (
    // ... existing content
  )}

Also, add error handling for mutations:

  onClick={async () => {
-   await toggleIsArchived({
-     variables: {
-       questionIds: Object.keys(selectedElementContent).map(Number),
-       isArchived: true,
-     },
-   })
-   setSelectedQuestions({})
+   try {
+     await toggleIsArchived({
+       variables: {
+         questionIds: Object.keys(selectedElementContent).map(Number),
+         isArchived: true,
+       },
+     })
+     setSelectedQuestions({})
+   } catch (error) {
+     toast.error(t('manage.general.errorArchivingQuestions'))
+   }
  }}
🧹 Nitpick comments (8)
apps/frontend-manage/src/pages/index.tsx (2)

56-56: Consider centralizing toast state management.

The success toast state management could be simplified using React Context or a toast library to avoid prop drilling and callback passing.

Consider using a toast library like react-hot-toast or react-toastify:

+ import { toast } from 'react-hot-toast'

- const [successToast, setSuccessToast] = useState(false)

// Replace toast trigger callbacks with direct toast calls
- triggerSuccessToast={() => setSuccessToast(true)}
+ triggerSuccessToast={() => toast.success(t('manage.questionPool.successMessage'))}

// Remove the ElementSuccessToast component
- <ElementSuccessToast
-   open={successToast}
-   onClose={() => setSuccessToast(false)}
- />

Line range hint 124-134: Optimize memoization dependencies.

The processedQuestions memo has multiple dependencies that could potentially trigger unnecessary recalculations. Consider breaking down the processing into smaller, more focused memos.

Consider this optimization:

+ const filteredQuestions = useMemo(
+   () => {
+     if (!dataQuestions?.userQuestions) return []
+     return dataQuestions.userQuestions.filter(
+       (question) => filters.archive === question.isArchived
+     )
+   },
+   [dataQuestions?.userQuestions, filters.archive]
+ )

  const processedQuestions = useMemo(() => {
-   if (dataQuestions?.userQuestions) {
+   if (filteredQuestions.length > 0) {
      const items = processItems(
-       dataQuestions?.userQuestions,
+       filteredQuestions,
        filters,
        sort,
        index
      )
      return items
    }
-  }, [dataQuestions?.userQuestions, filters, index, sort])
+  }, [filteredQuestions, filters, index, sort])
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (3)

4-12: Consider adding JSDoc documentation for the hook.

Add documentation to describe the purpose, parameters, and behavior of the hook.

+/**
+ * Custom hook for auto-saving form values with a delay.
+ * @param values - The form values to be saved
+ * @param setAutoSavedElement - Callback to update the auto-saved element
+ * @param setSaving - Callback to update the saving state
+ */
 function useAutoSave({
   values,
   setAutoSavedElement,
   setSaving,

13-28: Consider using debounce instead of setTimeout.

The current implementation uses a basic setTimeout for delaying saves. Consider using a debounce utility (e.g., from lodash) for more robust handling of rapid changes.

-  const savingTimeout = useRef<NodeJS.Timeout | null>(null)
-  const autoSaveContent = useCallback(
-    ({ values }: { values: ElementFormTypes }) => {
-      if (savingTimeout.current) {
-        clearTimeout(savingTimeout.current as NodeJS.Timeout)
-      }
-
-      savingTimeout.current = setTimeout(async () => {
-        setSaving(false)
-        setAutoSavedElement(values)
-      }, 2000)
-    },
-    [setAutoSavedElement, setSaving]
-  )
+  const autoSaveContent = useMemo(
+    () =>
+      debounce(
+        ({ values }: { values: ElementFormTypes }) => {
+          setSaving(false)
+          setAutoSavedElement(values)
+        },
+        2000
+      ),
+    [setAutoSavedElement, setSaving]
+  )

30-39: Consider adding error handling.

The auto-save operation might fail. Consider adding error handling to manage failures gracefully.

   useEffect(() => {
     setSaving(true)
-    autoSaveContent({ values })
+    try {
+      autoSaveContent({ values })
+    } catch (error) {
+      console.error('Auto-save failed:', error)
+      setSaving(false)
+    }

     return () => {
       if (savingTimeout.current) {
         clearTimeout(savingTimeout.current as NodeJS.Timeout)
       }
     }
   }, [values])
apps/frontend-manage/src/components/questions/manipulation/StudentElementPreview.tsx (1)

168-180: Consider extracting the saving indicator into a separate component.

The saving indicator logic could be reused in other components. Consider extracting it into a reusable component.

+interface SavingIndicatorProps {
+  saving: boolean
+}
+
+const SavingIndicator: React.FC<SavingIndicatorProps> = ({ saving }) => (
+  <div className="float-right mt-3 text-slate-400">
+    {saving ? (
+      <div className="flex flex-row items-center gap-2">
+        <FontAwesomeIcon icon={faSpinner} className="animate-spin" />
+        {t('manage.questionForms.savingTemporarily')}
+      </div>
+    ) : (
+      <div className="flex flex-row items-center gap-2">
+        <FontAwesomeIcon icon={faSave} />
+        {t('manage.questionForms.temporarilySaved')}
+      </div>
+    )}
+  </div>
+)

-      <div className="float-right mt-3 text-slate-400">
-        {saving ? (
-          <div className="flex flex-row items-center gap-2">
-            <FontAwesomeIcon icon={faSpinner} className="animate-spin" />
-            {t('manage.questionForms.savingTemporarily')}
-          </div>
-        ) : (
-          <div className="flex flex-row items-center gap-2">
-            <FontAwesomeIcon icon={faSave} />
-            {t('manage.questionForms.temporarilySaved')}
-          </div>
-        )}
-      </div>
+      <SavingIndicator saving={saving} />
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (2)

88-94: Consider adding a prefix to localStorage keys.

Add a prefix to localStorage keys to avoid potential conflicts with other applications using the same domain.

   const [autoSavedElement, setAutoSavedElement] =
     useLocalStorage<ElementFormTypes>(
       typeof elementId === 'undefined'
-        ? 'autosave-element-creation'
-        : `autosave-element-${elementId}`,
+        ? 'klicker-autosave-element-creation'
+        : `klicker-autosave-element-${elementId}`,
       undefined
     )

298-305: Consider using a utility function for localStorage cleanup.

Extract the localStorage cleanup logic into a utility function for better reusability and maintainability.

+const clearAutoSaveData = (elementId?: number) => {
+  const key = typeof elementId === 'undefined'
+    ? 'klicker-autosave-element-creation'
+    : `klicker-autosave-element-${elementId}`
+  localStorage.removeItem(key)
+}

-        if (autoSavedElement) {
-          localStorage.removeItem(
-            typeof elementId === 'undefined'
-              ? 'autosave-element-creation'
-              : `autosave-element-${elementId}`
-          )
-        }
+        if (autoSavedElement) {
+          clearAutoSaveData(elementId)
+        }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 49da9f6 and c5b637b.

📒 Files selected for processing (6)
  • apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (1 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (8 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/StudentElementPreview.tsx (3 hunks)
  • apps/frontend-manage/src/pages/index.tsx (1 hunks)
  • packages/i18n/messages/de.ts (1 hunks)
  • packages/i18n/messages/en.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/i18n/messages/de.ts
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: check
  • GitHub Check: SonarCloud
  • GitHub Check: build
  • GitHub Check: cypress-run
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (1)

364-368: LGTM! Well-integrated auto-save functionality.

The AutoSaveMonitor component is properly integrated with the form values and state management.

packages/i18n/messages/en.ts (1)

985-986: LGTM! Clear and concise translation messages.

The translation messages effectively communicate the auto-save status to users.

apps/frontend-manage/src/pages/index.tsx Show resolved Hide resolved
@dosubot dosubot bot added the feature label Jan 23, 2025
Copy link

cypress bot commented Jan 23, 2025

klicker-uzh    Run #4149

Run Properties:  status check passed Passed #4149  •  git commit fe74da38fd ℹ️: Merge 706376681b07a2bbbfa19cbe0a0b292f20cb2400 into 49da9f6874b2bd8ce2dc88f8e465...
Project klicker-uzh
Branch Review autosave-questions
Run status status check passed Passed #4149
Run duration 19m 08s
Commit git commit fe74da38fd ℹ️: Merge 706376681b07a2bbbfa19cbe0a0b292f20cb2400 into 49da9f6874b2bd8ce2dc88f8e465...
Committer Julius Schlapbach
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 311
View all changes introduced in this branch ↗︎

Copy link

@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: 0

🧹 Nitpick comments (5)
apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx (2)

6-16: Props interface could be extracted for better maintainability.

Consider extracting the props interface into a separate type definition for better code organization and reusability.

+interface RecoveryPromptProps {
+  open: boolean
+  onRecovery: () => void
+  onDiscard: () => void
+  editMode: boolean
+}

-function RecoveryPrompt({
+function RecoveryPrompt({
   open,
   onRecovery,
   onDiscard,
   editMode,
-}: {
-  open: boolean
-  onRecovery: () => void
-  onDiscard: () => void
-  editMode: boolean
-}) {
+}: RecoveryPromptProps) {

19-36: Consider adding aria-label for better accessibility.

The modal implementation looks good, but could benefit from additional accessibility attributes.

 <Modal
   hideCloseButton
   escapeDisabled
   open={open}
   onClose={() => null}
   title={t('manage.questionForms.recoverData')}
+  aria-label={t('manage.questionForms.recoverData')}
   className={{ content: 'gap-1' }}
 >
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (1)

138-140: Consider adding a comment explaining the initialValues logic.

The conditional logic for initialValues could benefit from a comment explaining the precedence.

+// For duplicated elements, always use the initial values
+// For editing, prefer auto-saved values if they exist, otherwise use initial values
 initialValues={
   isDuplication ? initialValues : (autoSavedElement ?? initialValues)
 }
apps/frontend-manage/src/components/questions/Question.tsx (2)

182-190: Add error handling for localStorage access

While the logic is sound, consider adding try-catch block to handle potential localStorage access errors, which can occur in various scenarios (e.g., private browsing mode, storage quota exceeded).

 onClick={() => {
-  const value = localStorage.getItem(`autosave-element-${id}`)
+  let value = null
+  try {
+    value = localStorage.getItem(`autosave-element-${id}`)
+  } catch (error) {
+    console.error('Failed to access localStorage:', error)
+  }

   if (value) {
     setShowRecoveryPrompt(true)
   } else {
     setIsModificationModalOpen(true)
   }
 }}

198-212: Consider extracting localStorage operations to a custom hook

The localStorage operations could be moved to a custom hook to improve reusability and separation of concerns.

// useAutoSaveStorage.ts
const useAutoSaveStorage = (elementId: number) => {
  const clearAutosave = useCallback(() => {
    try {
      localStorage.removeItem(`autosave-element-${elementId}`)
    } catch (error) {
      console.error('Failed to clear autosave:', error)
    }
  }, [elementId])

  return { clearAutosave }
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c5b637b and a6c611c.

📒 Files selected for processing (6)
  • apps/frontend-manage/src/components/questions/Question.tsx (4 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (8 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx (1 hunks)
  • apps/frontend-manage/src/pages/index.tsx (4 hunks)
  • packages/i18n/messages/de.ts (1 hunks)
  • packages/i18n/messages/en.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/frontend-manage/src/pages/index.tsx
  • packages/i18n/messages/en.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: check
  • GitHub Check: SonarCloud
  • GitHub Check: build
🔇 Additional comments (10)
apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx (2)

1-4: LGTM! Clean imports with clear dependencies.

The imports are well-organized and include only the necessary dependencies.


37-56: LGTM! Well-structured button layout with clear actions.

The button implementation is clean with appropriate icons and translations.

apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (3)

16-16: LGTM! Well-structured auto-save initialization.

The auto-save functionality is properly initialized with appropriate state variables and storage key generation.

Also applies to: 77-77, 88-94


301-308: LGTM! Proper cleanup of auto-saved data.

The code correctly removes auto-saved data after successful form submission.


367-371: LGTM! Clean integration of auto-save monitoring.

The AutoSaveMonitor component is properly integrated with necessary props.

packages/i18n/messages/de.ts (1)

984-992: LGTM! Comprehensive translations for auto-save functionality.

The added translations cover all necessary UI text for the auto-save feature, including status messages, recovery prompts, and action buttons.

apps/frontend-manage/src/components/questions/Question.tsx (4)

23-23: LGTM: Clean import addition

The RecoveryPrompt component import is properly organized with other component imports.


85-85: LGTM: Well-structured state management

The showRecoveryPrompt state is properly initialized and follows React state management best practices.


210-210: Verify the purpose of the editMode prop

The editMode prop is hardcoded to false without clear purpose. Please clarify its intended use or remove if unnecessary.


182-212: Verify integration with autosave mechanism

Please ensure that:

  1. The autosave key format matches the one used in the actual autosave implementation
  2. The recovery flow properly restores all necessary form state

Run this script to verify the autosave key format consistency:

✅ Verification successful

✓ Autosave implementation is correct and consistent

The autosave key format and recovery flow are properly implemented across all components:

  • Creation mode uses autosave-element-creation
  • Edit mode uses autosave-element-${id}
  • ElementEditModal handles both scenarios consistently
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all autosave-related localStorage operations
rg "localStorage\.(get|set|remove)Item.*autosave-element-" --type ts

Length of output: 437


Script:

#!/bin/bash
# Search for autosave-related code patterns
rg "autosave" --type ts -A 3

Length of output: 2466

Copy link

@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: 0

🧹 Nitpick comments (5)
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (2)

15-34: Consider optimizing the value comparison logic.

The current implementation stringifies the entire values object for comparison. For large forms, this could be inefficient.

Consider implementing a more efficient comparison:

-if (JSON.stringify(values) !== initialValuesString) {
+// Use a deep comparison utility
+import isEqual from 'lodash/isEqual'
+
+if (!isEqual(values, JSON.parse(initialValuesString))) {
  setAutoSavedElement(values)
}

36-48: Consider debouncing the saving indicator to prevent UI flicker.

The saving state is set immediately on every value change, which might cause UI flicker with rapid changes.

Consider debouncing the saving indicator:

useEffect(() => {
-  setSaving(true)
+  const savingTimeout = setTimeout(() => setSaving(true), 500)
  autoSaveContent({ values })

  return () => {
    if (savingTimeout.current) {
      clearTimeout(savingTimeout.current as NodeJS.Timeout)
    }
+   clearTimeout(savingTimeout)
  }
}, [values])
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (3)

Line range hint 77-94: Consider namespacing the local storage keys.

The current storage keys could potentially clash with other applications using the same domain.

Consider adding a namespace:

-      typeof elementId === 'undefined' || isDuplication
-        ? 'autosave-element-creation'
-        : `autosave-element-${elementId}`,
+      typeof elementId === 'undefined' || isDuplication
+        ? '@klicker-uzh/autosave-element-creation'
+        : `@klicker-uzh/autosave-element-${elementId}`,

130-138: Improve code documentation and ESLint configuration.

The implementation is correct, but there are two areas for improvement:

  1. The comment could be more descriptive
  2. The ESLint disable comment might be unnecessary

Consider these improvements:

-  // only update the form values on initial rendering in creation or edit mode (not in duplication mode)
-  // (otherwise, saving the question will directly trigger another save)
+  // In creation or edit mode, use auto-saved values if available
+  // In duplication mode, always use initial values to prevent recursive saves
   const formikInitialValues = useMemo(() => {
     if (!initialValues) {
       return undefined
     }
     return isDuplication ? initialValues : (autoSavedElement ?? initialValues)
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [isOpen, isDuplication, initialValues])
+  }, [isOpen, isDuplication, initialValues, autoSavedElement])

309-316: Extract the storage key generation logic to reduce duplication.

The storage key generation logic is duplicated between initialization and cleanup.

Consider extracting the logic:

+const getStorageKey = (elementId: number | undefined, isDuplication: boolean) => {
+  return typeof elementId === 'undefined' || isDuplication
+    ? '@klicker-uzh/autosave-element-creation'
+    : `@klicker-uzh/autosave-element-${elementId}`
+}

// Use in initialization
const [autoSavedElement, setAutoSavedElement] = useLocalStorage<ElementFormTypes>(
-  typeof elementId === 'undefined' || isDuplication
-    ? 'autosave-element-creation'
-    : `autosave-element-${elementId}`,
+  getStorageKey(elementId, isDuplication),
  undefined
)

// Use in cleanup
if (autoSavedElement) {
-  localStorage.removeItem(
-    typeof elementId === 'undefined' || isDuplication
-      ? 'autosave-element-creation'
-      : `autosave-element-${elementId}`
-  )
+  localStorage.removeItem(getStorageKey(elementId, isDuplication))
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a6c611c and 3caf55a.

📒 Files selected for processing (4)
  • .github/workflows/cypress-testing.yml (1 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (1 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (8 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/frontend-manage/src/components/questions/manipulation/RecoveryPrompt.tsx
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: test
  • GitHub Check: check
  • GitHub Check: SonarCloud
  • GitHub Check: build
  • GitHub Check: cypress-run
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (1)

1-14: LGTM! Well-structured hook parameters with proper TypeScript types.

The parameter structure follows React best practices with proper type safety.

.github/workflows/cypress-testing.yml (1)

142-142: LGTM! Appropriate upgrade of GitHub Actions dependency.

The upgrade to actions/upload-artifact@v4 follows GitHub Actions best practices.

@dosubot dosubot bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jan 23, 2025
@sjschlapbach sjschlapbach changed the title feat(apps/manage-frontend): auto-save element edit modal content continuously feat(apps/manage-frontend): auto-save of user inputs in element edit modal Jan 23, 2025
Copy link

@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: 0

🧹 Nitpick comments (3)
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (2)

129-137: Consider adding error handling for JSON parsing.

While the memoization of initial values is well implemented, there's no explicit error handling for potential JSON parsing errors from local storage.

Consider wrapping the auto-saved element access in a try-catch:

 const formikInitialValues = useMemo(() => {
   if (!initialValues) {
     return undefined
   }
+  try {
     return isDuplication ? initialValues : (autoSavedElement ?? initialValues)
+  } catch (error) {
+    console.error('Failed to parse auto-saved element:', error)
+    return initialValues
+  }
 }, [isOpen, isDuplication, initialValues])

308-315: Consider using a cleanup utility function.

The local storage cleanup logic is duplicated from the key generation logic.

Consider extracting the key generation to a utility function:

+const getAutoSaveKey = (elementId?: number, isDuplication?: boolean) => 
+  typeof elementId === 'undefined' || isDuplication
+    ? 'autosave-element-creation'
+    : `autosave-element-${elementId}`

 // In the component:
-localStorage.removeItem(
-  typeof elementId === 'undefined' || isDuplication
-    ? 'autosave-element-creation'
-    : `autosave-element-${elementId}`
-)
+localStorage.removeItem(getAutoSaveKey(elementId, isDuplication))
apps/frontend-manage/src/pages/index.tsx (1)

395-405: Consider extracting auto-save check logic.

The create question button click handler contains inline local storage checks.

Consider extracting this logic to a separate function:

+const hasAutoSavedContent = () => {
+  const value = localStorage.getItem('autosave-element-creation')
+  return !!value
+}

 // In the onClick handler:
-const value = localStorage.getItem('autosave-element-creation')
-if (value) {
+if (hasAutoSavedContent()) {
   setShowRecoveryPrompt(true)
 } else {
   setIsQuestionCreationModalOpen(true)
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3caf55a and 7fc0138.

📒 Files selected for processing (9)
  • apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (1 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/ElementContentInput.tsx (0 hunks)
  • apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (6 hunks)
  • apps/frontend-manage/src/pages/index.tsx (4 hunks)
  • cypress/cypress/e2e/B-feature-access-workflow.cy.ts (1 hunks)
  • cypress/cypress/e2e/D-questions-workflow.cy.ts (23 hunks)
  • cypress/cypress/fixtures/D-questions.json (1 hunks)
  • packages/i18n/messages/de.ts (1 hunks)
  • packages/i18n/messages/en.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • apps/frontend-manage/src/components/questions/manipulation/ElementContentInput.tsx
✅ Files skipped from review due to trivial changes (1)
  • cypress/cypress/e2e/B-feature-access-workflow.cy.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts
  • packages/i18n/messages/en.ts
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: SonarCloud
  • GitHub Check: format
  • GitHub Check: cypress-run
  • GitHub Check: check
  • GitHub Check: Analyze (javascript)
  • GitHub Check: build
🔇 Additional comments (8)
cypress/cypress/e2e/D-questions-workflow.cy.ts (2)

62-62: Consistent renaming of modal close buttons.

The modal close button data-cy attribute has been consistently renamed from close-question-modal to close-element-modal across all test cases. This change aligns with the broader scope of the modal being used for both questions and other elements.

Also applies to: 104-104, 165-165, 214-214, 321-321, 496-496, 631-631, 797-797, 979-979, 1050-1050, 1232-1232, 1366-1366, 1411-1411, 1473-1473, 1525-1525, 1643-1643, 1722-1722, 1914-1915


1916-2183: Well-structured auto-save test suite with comprehensive coverage.

The auto-save test suite thoroughly covers key scenarios:

  • Empty questions not being stored
  • Non-empty questions being stored and loaded correctly
  • Recovery and discard functionality
  • Local storage cleanup after saving
  • Edit mode behavior
  • Duplication mode behavior

The test structure follows good practices:

  • Clear test descriptions
  • Proper waiting for auto-save triggers
  • Verification of both UI state and local storage
  • Helper function to reduce code duplication
cypress/cypress/fixtures/D-questions.json (1)

104-116: Well-structured test data for auto-save functionality.

The new autoSave section provides comprehensive test data including:

  • Base question data (title, content)
  • Multiple choices with correct answer marking
  • Modified versions for testing edits (titleEdited, contentEdited)
  • Duplication data (titleEditedDuplicated)
apps/frontend-manage/src/components/questions/manipulation/ElementEditModal.tsx (2)

87-93: Well-implemented auto-save storage with proper key management.

The useLocalStorage hook implementation correctly handles different storage keys:

  • Uses 'autosave-element-creation' for new elements
  • Uses autosave-element-${elementId} for existing elements
  • Properly handles duplication mode

374-378: Well-integrated auto-save monitoring.

The AutoSaveMonitor component is properly integrated with:

  • Access to current form values
  • Initial values for comparison
  • Setter for auto-save state
apps/frontend-manage/src/pages/index.tsx (2)

459-471: Well-implemented recovery prompt with clear user actions.

The RecoveryPrompt implementation:

  • Properly handles both recovery and discard actions
  • Cleans up local storage on discard
  • Correctly manages modal states

56-56: Remove redundant sortBy state.

The sortBy state is redundant as the sorting logic is already managed by the useSortingAndFiltering hook.

packages/i18n/messages/de.ts (1)

984-990: Well-structured translations for auto-save functionality!

The added German translations for the auto-save feature are clear, consistent, and maintain a professional tone. The messages effectively communicate the data recovery functionality to users.

Copy link

@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: 0

🧹 Nitpick comments (3)
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (3)

4-12: Consider using consistent types for comparison.

The initialValuesString prop is typed as string while values is of type ElementFormTypes. This might lead to inconsistent comparisons when checking for changes. Consider using the same type or adding type conversion utilities.

-  initialValuesString: string
+  initialValues: ElementFormTypes

15-30: Optimize the auto-save implementation.

Several improvements could enhance the code:

  1. The async keyword on line 22 is unnecessary as there are no await operations.
  2. Type casting could be handled more elegantly with proper TypeScript configuration.
  3. The string comparison could be optimized.
-      savingTimeout.current = setTimeout(async () => {
+      savingTimeout.current = setTimeout(() => {
         // only update the stored content if it has changed
-        if (JSON.stringify(values) !== initialValuesString) {
+        const currentValuesString = JSON.stringify(values)
+        if (currentValuesString !== initialValuesString) {
           setAutoSavedElement(values)
         }
       }, 2000)

1-45: Consider enhancing user feedback and configuration.

A few architectural improvements could enhance this implementation:

  1. Consider adding a callback to notify parent components about save status for visual feedback.
  2. The auto-save delay could be configurable through props.
  3. Consider implementing a save queue to handle rapid changes more efficiently.

Example implementation:

interface AutoSaveMonitorProps {
  values: ElementFormTypes;
  initialValues: ElementFormTypes;
  setAutoSavedElement: Dispatch<SetStateAction<ElementFormTypes>>;
  onSaveStatusChange?: (status: 'saving' | 'saved' | 'error') => void;
  saveDelay?: number;
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 7fc0138 and 7063766.

📒 Files selected for processing (1)
  • apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: format
  • GitHub Check: build
  • GitHub Check: cypress-run
  • GitHub Check: test
  • GitHub Check: SonarCloud
  • GitHub Check: check
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
apps/frontend-manage/src/components/questions/manipulation/AutoSaveMonitor.ts (3)

1-2: LGTM! Imports are well-organized.

All necessary React hooks and types are properly imported.


32-40: LGTM! Effect cleanup is properly implemented.

The cleanup function correctly handles the timeout clearing, and the dependency array is properly specified.


42-45: LGTM! Component export is correct.

The component appropriately returns null as it's a utility component with no UI elements.

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
6.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@sjschlapbach sjschlapbach merged commit 367e5c2 into v3 Jan 23, 2025
12 of 14 checks passed
@sjschlapbach sjschlapbach deleted the autosave-questions branch January 23, 2025 17:48
Copy link

cypress bot commented Jan 23, 2025

klicker-uzh    Run #4152

Run Properties:  status check passed Passed #4152  •  git commit 367e5c2541: feat(apps/manage-frontend): auto-save of user inputs in element edit modal (#447...
Project klicker-uzh
Branch Review v3
Run status status check passed Passed #4152
Run duration 18m 58s
Commit git commit 367e5c2541: feat(apps/manage-frontend): auto-save of user inputs in element edit modal (#447...
Committer Julius Schlapbach
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 311
View all changes introduced in this branch ↗︎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature size:XL This PR changes 500-999 lines, ignoring generated files.
Development

Successfully merging this pull request may close these issues.

1 participant