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

Duplicate Budget #3847

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

Conversation

tlesicka
Copy link
Contributor

Splitting #3689 into two parts. This part is the Duplicate part.

@actual-github-bot actual-github-bot bot changed the title Duplicate Budget [WIP] Duplicate Budget Nov 17, 2024
Copy link

netlify bot commented Nov 17, 2024

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 73066ce
🔍 Latest deploy log https://app.netlify.com/sites/actualbudget/deploys/674137b9043fa300083ca8fe
😎 Deploy Preview https://deploy-preview-3847.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

github-actions bot commented Nov 17, 2024

Bundle Stats — desktop-client

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
10 5.45 MB → 5.46 MB (+10.07 kB) +0.18%
Changeset
File Δ Size
src/components/modals/manager/DuplicateFileModal.tsx 🆕 +8.1 kB 0 B → 8.1 kB
home/runner/work/actual/actual/packages/loot-core/src/client/actions/budgets.ts 📈 +1 kB (+18.37%) 5.45 kB → 6.46 kB
src/components/manager/BudgetList.tsx 📈 +708 B (+6.11%) 11.32 kB → 12.01 kB
src/components/Modals.tsx 📈 +282 B (+1.78%) 15.43 kB → 15.71 kB
View detailed bundle breakdown

Added

No assets were added

Removed

No assets were removed

Bigger

Asset File Size % Changed
static/js/index.js 3.44 MB → 3.45 MB (+10.07 kB) +0.29%

Smaller

No assets were smaller

Unchanged

Asset File Size % Changed
static/js/useAccountPreviewTransactions.js 1.68 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.5 kB 0%
static/js/AppliedFilters.js 21.32 kB 0%
static/js/workbox-window.prod.es5.js 5.69 kB 0%
static/js/resize-observer.js 18.37 kB 0%
static/js/BackgroundImage.js 122.29 kB 0%
static/js/narrow.js 82.93 kB 0%
static/js/wide.js 241.19 kB 0%
static/js/ReportRouter.js 1.52 MB 0%

Copy link
Contributor

github-actions bot commented Nov 17, 2024

Bundle Stats — loot-core

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
1 1.32 MB → 1.32 MB (+2.07 kB) +0.15%
Changeset
File Δ Size
packages/loot-core/src/server/util/budget-name.ts 📈 +611 B (+55.19%) 1.08 kB → 1.68 kB
packages/loot-core/src/platform/server/fs/index.web.ts 📈 +1.59 kB (+16.48%) 9.67 kB → 11.26 kB
packages/loot-core/src/server/main.ts 📈 +2.66 kB (+4.24%) 62.7 kB → 65.35 kB
View detailed bundle breakdown

Added

No assets were added

Removed

No assets were removed

Bigger

Asset File Size % Changed
kcab.worker.js 1.32 MB → 1.32 MB (+2.07 kB) +0.15%

Smaller

No assets were smaller

Unchanged

No assets were unchanged

@tlesicka tlesicka changed the title [WIP] Duplicate Budget Duplicate Budget Nov 17, 2024
Copy link
Contributor

coderabbitai bot commented Nov 17, 2024

Walkthrough

The pull request introduces a new modal component, DuplicateFileModal, into the existing Modals component within Modals.tsx. This modal is triggered by the 'duplicate-budget' case in a switch statement, allowing users to duplicate budget files. The modal accepts several props, including file, managePage, loadBudget, and onComplete. Additionally, the BudgetList.tsx component is modified to incorporate an optional onDuplicate callback across various components, enabling users to initiate file duplication from the UI. The duplicateBudget function is introduced in budgets.ts, which handles the duplication process asynchronously, managing state and error handling. Furthermore, the FinanceModals type is updated to include the new modal type, and several function signatures across the codebase are enhanced for type safety. Overall, these changes enhance the application's file management capabilities, particularly regarding budget duplication.

Possibly related PRs

Suggested labels

:sparkles: Merged

Suggested reviewers

  • youngcw
  • MikesGlitch

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 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
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: 3

🧹 Outside diff range and nitpick comments (15)
packages/loot-core/src/server/util/budget-name.ts (1)

7-10: LGTM! Function signature improvements

The updated signature with proper type annotations and default parameter enhances type safety and usability.

Consider using a Set or Map for existingFiles if dealing with larger datasets, as it would improve lookup performance from O(n) to O(1):

export async function uniqueFileName(
-  existingFiles: Budget[],
+  existingFiles: Set<string> | Map<string, Budget>,
  initialName: string = 'My Finances',
): Promise<string> {
  let idx = 1;
  let newName = initialName;
-  while (existingFiles.find(file => file.name === newName)) {
+  while (existingFiles instanceof Set ? existingFiles.has(newName) : existingFiles.has(newName)) {
    newName = `${initialName} ${idx}`;
    idx++;
  }
  return newName;
}
packages/loot-core/src/client/actions/budgets.ts (1)

176-179: Consider using template literals for better readability.

The i18n text could be more readable using template literals instead of concatenation with variables.

-         loadingText: t('Duplicating:  {{oldName}}  --  to:  {{newName}}', {
-           oldName,
-           newName,
-         }),
+         loadingText: t(`Duplicating: ${oldName} -- to: ${newName}`),
packages/loot-core/src/client/state-types/modals.d.ts (1)

81-110: LGTM! Consider enhancing the documentation.

The new 'duplicate-budget' modal type is well-structured with proper TypeScript types and comprehensive documentation. The implementation follows the existing patterns and integrates well with the codebase.

Consider adding an example usage in the JSDoc comment to demonstrate the expected behavior of the onComplete callback:

/**
 * Example usage:
 * onComplete: (event) => {
 *   if (event.status === 'success') {
 *     console.log('Budget duplicated successfully');
 *   } else if (event.status === 'failed') {
 *     console.error('Duplication failed:', event.error);
 *   } else {
 *     console.log('Duplication canceled by user');
 *   }
 * }
 */
packages/loot-core/src/types/server-handlers.d.ts (2)

329-344: LGTM! Consider improving the documentation.

The type definition is well-structured and consistent with other handlers. However, the JSDoc comments could be enhanced.

Consider updating the JSDoc to document all parameters and possible error cases:

 /**
  * Duplicates a budget file.
  * @param {Object} arg - The arguments for duplicating a budget.
  * @param {string} [arg.id] - The ID of the local budget to duplicate.
  * @param {string} [arg.cloudId] - The ID of the cloud-synced budget to duplicate.
  * @param {string} arg.newName - The name for the duplicated budget.
  * @param {boolean} [arg.cloudSync] - Whether to sync the duplicated budget to the cloud.
+ * @param {'none' | 'original' | 'copy'} arg.open - What to open after duplication: none, original budget, or the new copy.
  * @returns {Promise<string>} The ID of the newly created budget.
+ * @throws {Error} When neither id nor cloudId is provided
+ * @throws {Error} When both id and cloudId are provided
  */

338-344: Consider enhancing error handling types.

The return type could be more specific to handle potential error cases, similar to other handlers in this interface.

Consider updating the return type to include error cases:

-  }) => Promise<string>;
+  }) => Promise<string | { error: string }>;
packages/loot-core/src/platform/server/fs/index.web.ts (3)

22-22: Consider completing type annotations for all parameters

While the string parameter types are well defined, some parameters like contents in _writeFile and writeFile functions lack type annotations. Consider adding appropriate types for complete type safety.

Example for writeFile:

-export const writeFile = async function (filepath: string, contents) {
+export const writeFile = async function (filepath: string, contents: string | ArrayBuffer | Uint8Array) {

Also applies to: 26-26, 50-50, 70-70, 104-104, 113-113, 195-195, 319-322, 343-343, 347-347, 351-351


152-193: Well-implemented SQL file copy with proper error handling

The implementation is robust with:

  • Proper file handle management using try/finally
  • Error handling with cleanup
  • Efficient block-based copying

Consider adding validation for the block size to ensure it's within reasonable bounds.

 const blockSize = fromfile.meta.blockSize;
+const MAX_BLOCK_SIZE = 1024 * 1024; // 1MB
+if (!blockSize || blockSize <= 0 || blockSize > MAX_BLOCK_SIZE) {
+  throw new Error(`Invalid block size: ${blockSize}`);
+}

319-340: Consider improving error handling and simplifying the code structure

The implementation handles both regular and SQLite files well, but could be improved:

  1. Add more context to the initial error
  2. Simplify the success path by removing the unnecessary result variable
 export const copyFile = async function (
   frompath: string,
   topath: string,
 ): Promise<boolean> {
-  let result = false;
   try {
     const contents = await _readFile(frompath);
-    result = await _writeFile(topath, contents);
+    return await _writeFile(topath, contents);
   } catch (error) {
+    console.error(`Failed to copy file using standard method: ${error.message}`);
     if (frompath.endsWith('.sqlite') || topath.endsWith('.sqlite')) {
       try {
-        result = await _copySqlFile(frompath, topath);
+        return await _copySqlFile(frompath, topath);
       } catch (secondError) {
         throw new Error(
           `Failed to copy SQL file from ${frompath} to ${topath}: ${secondError.message}`,
         );
       }
     } else {
       throw error;
     }
   }
-  return result;
 };
packages/desktop-client/src/components/manager/BudgetList.tsx (2)

89-92: Consider extracting menu items configuration

The menu items array could be more maintainable by extracting it to a constant or configuration object.

+ const MENU_ITEMS = {
+   duplicate: { name: 'duplicate', text: t('Duplicate') },
+   delete: { name: 'delete', text: t('Delete') }
+ };
+
- const items = [
-   ...(onDuplicate ? [{ name: 'duplicate', text: t('Duplicate') }] : []),
-   { name: 'delete', text: t('Delete') },
- ];
+ const items = [
+   ...(onDuplicate ? [MENU_ITEMS.duplicate] : []),
+   MENU_ITEMS.delete
+ ];

262-265: Extract file type check to a separate function

The inline type guard makes the JSX less readable. Consider extracting it to a separate function.

+ const isDuplicatableFile = (file: File): boolean => {
+   return 'id' in file;
+ };

- <FileMenuButton
-   onDelete={() => onDelete(file)}
-   onDuplicate={'id' in file ? () => onDuplicate(file) : undefined}
- />
+ <FileMenuButton
+   onDelete={() => onDelete(file)}
+   onDuplicate={isDuplicatableFile(file) ? () => onDuplicate(file) : undefined}
+ />
packages/desktop-client/src/components/Modals.tsx (1)

590-599: Consider making the file prop required

While the implementation follows the existing pattern, the file prop appears to be required for budget duplication functionality. Consider making it non-optional to improve type safety.

Apply this diff to the DuplicateFileModal component's props interface in its implementation file:

interface DuplicateFileModalProps {
-  file?: File;
+  file: File;
  managePage?: boolean;
  loadBudget?: (id: string) => void;
  onComplete?: () => void;
}
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (3)

226-232: Refactor nested ternary operators for improved readability

Using nested ternary operators can make the code harder to read and maintain. Consider refactoring the variant assignment to enhance clarity.

Apply this diff to refactor the code:

- variant={
-   loadingState !== null
-     ? 'bare'
-     : isCloudFile
-       ? 'normal'
-       : 'primary'
- }
+ const variant =
+   loadingState !== null
+     ? 'bare'
+     : isCloudFile
+     ? 'normal'
+     : 'primary';
+
...
+ <ButtonWithLoading
+   variant={variant}

57-59: Consider supporting international characters in budget names

The current validation regex restricts budget names to ASCII characters. Allowing Unicode letters and numbers would enable users to use international characters in their budget names.

Apply this diff to update the regex:

- if (!/^[a-zA-Z0-9 .\-_()]+$/.test(trimmedName)) {
+ if (!/^[\p{L}\p{N} .\-_()]+$/u.test(trimmedName)) {

Note: This change uses Unicode property escapes and the u flag. Ensure that your JavaScript environment supports these features.


104-105: Include original error details in the onComplete callback

Currently, a generic error is passed to the onComplete callback on failure. Including the original error e can provide more context for error handling and debugging.

Apply this diff to include the original error:

 const newError = new Error(t('Failed to duplicate budget'));
- if (onComplete) onComplete({ status: 'failed', error: newError });
+ if (onComplete) onComplete({ status: 'failed', error: newError, originalError: e });

Note: Be cautious about exposing sensitive information when passing the original error.

packages/loot-core/src/server/main.ts (1)

1886-1891: Consider refactoring the deletion logic to avoid loading the budget.

Loading and then closing the budget to delete it (lines 1889-1890) may introduce unnecessary overhead or potential side effects if the budget cannot be loaded properly. Instead, consider deleting the budget files directly if the budget is not currently loaded. This approach can simplify the deletion process and reduce the chance of errors.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between dad702e and 02cc76c.

⛔ Files ignored due to path filters (1)
  • upcoming-release-notes/3847.md is excluded by !**/*.md
📒 Files selected for processing (9)
  • packages/desktop-client/src/components/Modals.tsx (2 hunks)
  • packages/desktop-client/src/components/manager/BudgetList.tsx (8 hunks)
  • packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (1 hunks)
  • packages/loot-core/src/client/actions/budgets.ts (1 hunks)
  • packages/loot-core/src/client/state-types/modals.d.ts (1 hunks)
  • packages/loot-core/src/platform/server/fs/index.web.ts (8 hunks)
  • packages/loot-core/src/server/main.ts (4 hunks)
  • packages/loot-core/src/server/util/budget-name.ts (1 hunks)
  • packages/loot-core/src/types/server-handlers.d.ts (1 hunks)
🧰 Additional context used
📓 Learnings (2)
packages/desktop-client/src/components/Modals.tsx (2)
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:144-156
Timestamp: 2024-11-10T16:45:31.225Z
Learning: In `packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx`, styles for buttons may differ for each button in the future, so avoid suggesting extraction of common styles in this file.
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:181-196
Timestamp: 2024-11-10T16:45:25.627Z
Learning: After refactoring `nameError` in `DuplicateFileModal.tsx`, it's not necessary to use `setNameError` in the `onPress` handlers when validation fails.
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (2)
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:144-156
Timestamp: 2024-11-10T16:45:31.225Z
Learning: In `packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx`, styles for buttons may differ for each button in the future, so avoid suggesting extraction of common styles in this file.
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:181-196
Timestamp: 2024-11-10T16:45:25.627Z
Learning: After refactoring `nameError` in `DuplicateFileModal.tsx`, it's not necessary to use `setNameError` in the `onPress` handlers when validation fails.
🔇 Additional comments (10)
packages/loot-core/src/server/util/budget-name.ts (1)

5-5: LGTM! Type import enhances type safety

The addition of the Budget type import improves type safety and code maintainability.

packages/loot-core/src/client/actions/budgets.ts (3)

151-171: Well-structured function signature with comprehensive TypeScript types.

The function signature is well-designed with:

  • Clear parameter naming
  • Proper TypeScript types
  • Good documentation for cloudSync parameter
  • Sensible default for loadBudget

172-195: Clean and consistent implementation of the duplicate budget flow.

The implementation follows the established patterns in the file:

  • Proper async/await usage
  • Consistent Redux state management
  • Modal handling matches other actions
  • Conditional file reloading based on managePage

183-189: Verify the 'duplicate-budget' handler implementation.

The send('duplicate-budget', ...) call passes parameters that should be handled correctly on the server side. Let's verify the handler implementation.

packages/loot-core/src/platform/server/fs/index.web.ts (1)

152-193: Verify cleanup of file resources across all code paths

Let's ensure proper cleanup of file resources across all code paths, especially during budget duplication operations.

Also applies to: 319-340

✅ Verification successful

Based on the shell script results, I can now provide a final response:

File operations are properly cleaned up across all code paths

The code review concern has been verified and the implementation is correct:

  • The _copySqlFile function has proper cleanup in both error and success paths:
    • Resources are cleaned up in the finally block: tofile.close() and fromfile.close()
    • Error path additionally cleans up the partially created file with _removeFile(toDbPath)
  • The error handling is comprehensive with proper error propagation
  • The implementation follows the same pattern as other file operations in the codebase
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential resource leaks in file operations

# Look for file operations without corresponding cleanup
echo "Checking for balanced file operations..."
rg -A 5 "open\(\)" | rg -B 5 "close\(\)"

# Check for error handling patterns
echo "Checking error handling patterns..."
rg "catch.*\{" -A 5

# Look for cleanup in finally blocks
echo "Checking cleanup patterns..."
rg "finally.*\{" -A 5

Length of output: 77016

packages/desktop-client/src/components/manager/BudgetList.tsx (3)

Line range hint 97-129: LGTM! Clean implementation of FileMenuButton

The component properly handles the optional duplication functionality and follows React patterns.


278-284: LGTM! Clean prop handling in BudgetFiles

The component properly passes the duplication callback to child components.


501-501: Verify duplicate modal and manage page interaction

The managePage: true flag is passed to the duplicate modal. Let's verify this interaction.

✅ Verification successful

Let me gather more information about the duplicate modal implementation to verify this interaction properly.


Let me check the DuplicateFileModal component implementation to see how the managePage prop is used.


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

The managePage flag is properly implemented and used correctly

The managePage flag is:

  • Properly typed in the DuplicateFileModal component props
  • Correctly passed through from BudgetList to the modal
  • Properly forwarded to the duplicateBudget action
  • Well-documented in the modal types with a clear purpose: "Indicates whether the duplication is initiated from the budget management page"
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for the duplicate modal implementation and its handling of managePage prop
ast-grep --pattern 'const DuplicateFileModal = ({ managePage, $$$'

# Search for other uses of managePage in modal contexts
rg "managePage.*true" -g "!{*.test.*,*.spec.*}"

Length of output: 261


Script:

#!/bin/bash
# Search for the duplicate budget modal component
ast-grep --pattern 'const DuplicateFileModal = {
  $$$
}'

# Search for any references to managePage in modal components
rg "managePage" -A 5 -B 5 --type typescript --type tsx

# Look for modal registration or configuration
rg "duplicate-budget" -A 5 -B 5

Length of output: 7588


Script:

#!/bin/bash
# Get the DuplicateFileModal component implementation
cat packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx

# Look for any conditional logic based on managePage
rg "managePage" -A 3 -B 3 packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx

Length of output: 8886

packages/desktop-client/src/components/Modals.tsx (1)

48-48: LGTM!

The import statement follows the existing pattern and maintains proper alphabetical ordering.

packages/loot-core/src/server/main.ts (1)

2168-2168: Verify the condition for starting the backup service.

At line 2168, the condition excludes web and mobile platforms and checks the environment variable:

if (
  !Platform.isWeb &&
  !Platform.isMobile &&
  process.env.NODE_ENV !== 'test'
) {
  await startBackupService(id);
}

Ensure that this condition accurately reflects all scenarios where the backup service should be started. For instance, if new platforms are introduced or environment variables change, the backup service might not start as intended.

To verify the platforms where the backup service starts, you can run the following script:

✅ Verification successful

Let me gather more information about the Platform checks and environment conditions.


Let me check the Platform module definition to understand the platform detection implementation.


Let me check the actual Platform module implementation to understand how platform detection works.


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

The backup service conditions are correctly implemented

The conditions for starting the backup service are appropriate and well-defined:

  • The check !Platform.isWeb && !Platform.isMobile ensures backups only run on desktop platforms, which is correct as web/mobile platforms have different storage constraints and backup mechanisms
  • The process.env.NODE_ENV !== 'test' condition is necessary to prevent backup operations during testing
  • The platform detection is properly implemented with environment-specific modules that define isWeb, isMobile, and isDesktop flags

The backup service also has a fail-safe implementation in backups.ts that ensures the initial backup is created and the service is properly restarted when needed.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: List all platform checks related to starting the backup service.

# Expected: Ensure that backup service starts only on desktop platforms outside of test environments.

# Search for startBackupService calls and their conditions
rg -A 5 "startBackupService"

Length of output: 1563


Script:

#!/bin/bash
# Search for Platform.isWeb and Platform.isMobile usage
rg -B 2 -A 2 "Platform\.(isWeb|isMobile)"

# Search for the Platform module definition
fd -e ts -e js Platform

Length of output: 1841


Script:

#!/bin/bash
# Look for other potential backup service related conditions
rg -B 2 -A 2 "process\.env\.NODE_ENV.*test"

Length of output: 3455


Script:

#!/bin/bash
# Check if there are any other conditions that might affect backup service
ast-grep --pattern 'if ($$$) {
  $$$
  startBackupService($$$)
  $$$
}'

Length of output: 1633


Script:

#!/bin/bash
# Check the content of the Platform module
cat packages/desktop-client/src/hooks/useSendPlatformRequest.ts

# Search for other Platform-related files
rg -l "Platform" --type ts

Length of output: 1641


Script:

#!/bin/bash
# Search for Platform implementation in core modules
rg -p "export (const|class|interface) Platform" --type ts

# Look for platform-specific implementations
fd -e ts "platform" --type f --exec head -n 20 {}

Length of output: 2229

packages/loot-core/src/client/actions/budgets.ts Outdated Show resolved Hide resolved
packages/loot-core/src/server/main.ts Outdated Show resolved Hide resolved
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: 0

🧹 Outside diff range and nitpick comments (1)
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (1)

157-158: Consider validating input on change for immediate feedback

Currently, validation occurs on input blur. Validating on each change can provide real-time feedback to the user, enhancing the user experience.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 02cc76c and 882c14d.

📒 Files selected for processing (2)
  • packages/desktop-client/src/components/manager/BudgetList.tsx (8 hunks)
  • packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (2)
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:144-156
Timestamp: 2024-11-10T16:45:31.225Z
Learning: In `packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx`, styles for buttons may differ for each button in the future, so avoid suggesting extraction of common styles in this file.
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:181-196
Timestamp: 2024-11-10T16:45:25.627Z
Learning: After refactoring `nameError` in `DuplicateFileModal.tsx`, it's not necessary to use `setNameError` in the `onPress` handlers when validation fails.
🔇 Additional comments (3)
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (2)

51-62: Validation logic is comprehensive and well-implemented

The validateNewName function effectively checks for empty names, excessive length, and invalid characters, ensuring cross-filesystem compatibility.


57-59: ⚠️ Potential issue

Correct the regular expression to include the dash '-' character properly

In the regular expression /^[a-zA-Z0-9 .\-_()]+$/, the dash - inside a character class defines a range unless it's at the start or end, or escaped. Currently, it may unintentionally create a range and not include the - character as intended. To include the - character, you should escape it or place it at the start or end of the character class.

Apply this diff to fix the regular expression:

-if (!/^[a-zA-Z0-9 .\-_()]+$/.test(trimmedName)) {
+if (!/^[a-zA-Z0-9 ._\-()]+$/.test(trimmedName)) {

Alternatively, place the dash at the end:

-if (!/^[a-zA-Z0-9 .\-_()]+$/.test(trimmedName)) {
+if (!/^[a-zA-Z0-9 ._()\-]+$/.test(trimmedName)) {

Likely invalid or redundant comment.

packages/desktop-client/src/components/manager/BudgetList.tsx (1)

Line range hint 67-92: LGTM on the implementation of the duplication functionality

The code changes effectively introduce the duplication feature by appropriately adding the onDuplicate prop to the relevant components (FileMenu, FileMenuButton, FileItem, BudgetFiles, and BudgetList). The conditional rendering and handling ensure that the duplication option is available when applicable.

Also applies to: 97-129, 190-196, 262-265, 278-284, 320-320, 496-508

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: 3

🧹 Outside diff range and nitpick comments (14)
packages/loot-core/src/server/util/budget-name.ts (3)

6-21: Consider caching budget list and adding error handling

The function makes an API call to get budgets on each invocation, which could be inefficient if called multiple times in succession. Additionally, error handling for the API call is missing.

Consider these improvements:

+let cachedBudgets: Array<{ name: string }> | null = null;
+
 export async function uniqueBudgetName(
   initialName: string = 'My Finances',
 ): Promise<string> {
-  const budgets = await handlers['get-budgets']();
+  try {
+    // Use cached budgets if available or fetch new ones
+    cachedBudgets = cachedBudgets || await handlers['get-budgets']();
+    const budgets = cachedBudgets;
+
+    let idx = 1;
+    let newName = initialName;
+    
+    // Add a maximum iteration limit as a safety measure
+    const MAX_ATTEMPTS = 1000;
+    while (idx < MAX_ATTEMPTS && budgets.find(file => file.name === newName)) {
+      newName = `${initialName} ${idx}`;
+      idx++;
+    }
+    
+    if (idx === MAX_ATTEMPTS) {
+      throw new Error('Unable to generate unique budget name after maximum attempts');
+    }
+
+    return newName;
+  } catch (error) {
+    throw new Error(`Failed to generate unique budget name: ${error.message}`);
+  } finally {
+    // Clear cache to ensure fresh data on next call
+    cachedBudgets = null;
+  }
 }

23-39: Optimize validation order and improve error messages

The validation logic could be optimized by checking simpler conditions first, and the error messages could be more specific.

Consider this improved implementation:

 export async function validateBudgetName(
   name: string,
 ): Promise<{ valid: boolean; message?: string }> {
   const trimmedName = name.trim();
-  const uniqueName = await uniqueBudgetName(trimmedName);
-  let message: string | null = null;
+  
+  // Check basic validations first before making API calls
+  if (trimmedName === '') {
+    return { valid: false, message: 'Budget name cannot be blank' };
+  }
+  
+  if (trimmedName.length > 100) {
+    return { valid: false, message: 'Budget name is too long (max length 100)' };
+  }
 
-  if (trimmedName === '') message = 'Budget name cannot be blank';
-  if (trimmedName.length > 100) {
-    message = 'Budget name is too long (max length 100)';
-  }
-  if (uniqueName !== trimmedName) {
-    message = `"${name}" already exists, try "${uniqueName}" instead`;
+  // Only check uniqueness if other validations pass
+  const uniqueName = await uniqueBudgetName(trimmedName);
+  if (uniqueName !== trimmedName) {
+    return {
+      valid: false,
+      message: `A budget named "${trimmedName}" already exists. Suggested name: "${uniqueName}"`
+    };
   }
 
-  return message ? { valid: false, message } : { valid: true };
+  return { valid: true };
 }

Line range hint 1-64: Consider splitting name generation and validation concerns

The file currently handles both name generation and validation logic. Consider splitting these into separate modules:

  • budget-name-generator.ts: For uniqueBudgetName and idFromBudgetName
  • budget-name-validator.ts: For validation logic

This would improve maintainability and make the code more modular.

packages/loot-core/src/client/actions/budgets.ts (3)

162-182: Enhance cloudSync parameter documentation.

The JSDoc comment for cloudSync could be more descriptive about the implications of enabling/disabling cloud sync.

Consider expanding the documentation:

  /**
-  * cloudSync is used to determine if the duplicate budget
-  * should be synced to the server
+  * Determines if the duplicated budget should be synced to the cloud server.
+  * When true, the budget will be uploaded to the cloud storage.
+  * When false, the budget remains local only.
+  * @default false
   */
  cloudSync?: boolean;

183-216: Consider adding success feedback and specific loading states.

The implementation looks solid with proper error handling and state management. Consider these enhancements:

 return async (dispatch: Dispatch) => {
   try {
     dispatch(
       setAppState({
-        loadingText: t('Duplicating:  {{oldName}}  --  to:  {{newName}}', {
+        loadingText: t('Step 1/2: Duplicating {{oldName}} to {{newName}}', {
           oldName,
           newName,
         }),
       }),
     );

     await send('duplicate-budget', {
       id,
       cloudId,
       newName,
       cloudSync,
       open: loadBudget,
     });

+    if (cloudSync) {
+      dispatch(
+        setAppState({
+          loadingText: t('Step 2/2: Uploading to cloud storage'),
+        }),
+      );
+    }

     dispatch(closeModal());

     if (managePage) {
       await dispatch(loadAllFiles());
     }
+
+    // Show success message
+    dispatch(
+      setAppState({
+        message: t('Successfully duplicated budget'),
+        messageType: 'success',
+      }),
+    );
   } catch (error) {
     console.error('Error duplicating budget:', error);
     throw error instanceof Error
       ? error
       : new Error('Error duplicating budget: ' + String(error));
   } finally {
     dispatch(setAppState({ loadingText: null }));
   }
 };

151-216: Consider implementing a common error handling pattern.

The newly added functions would benefit from a consistent error handling approach across all budget-related actions.

Consider creating a utility function to standardize error handling:

function handleBudgetActionError(
  error: unknown,
  action: string,
  dispatch: Dispatch,
): never {
  console.error(`Error ${action}:`, error);
  dispatch(setAppState({
    message: t(`Failed to ${action}`),
    messageType: 'error',
  }));
  throw error instanceof Error
    ? error
    : new Error(`Error ${action}: ` + String(error));
}

This would promote consistency and reduce duplication across budget actions.

packages/loot-core/src/types/server-handlers.d.ts (1)

337-352: Consider adding error handling types.

The type definition and JSDoc documentation for duplicate-budget are comprehensive. However, consider enhancing the return type to include potential error cases, similar to other budget-related handlers in this interface (e.g., delete-budget, download-budget).

-  }) => Promise<string>;
+  }) => Promise<string | { error?: string }>;
packages/loot-core/src/server/main.ts (4)

1717-1723: Add type annotations to handler parameters.

While the handlers are functionally correct, they could benefit from explicit TypeScript type annotations for better type safety and documentation.

-handlers['validate-budget-name'] = async function ({ name }) {
+handlers['validate-budget-name'] = async function ({ name }: { name: string }) {
   return validateBudgetName(name);
 };

-handlers['unique-budget-name'] = async function ({ name }) {
+handlers['unique-budget-name'] = async function ({ name }: { name: string }) {
   return uniqueBudgetName(name);
 };

1920-1925: Improve type safety in function parameters.

The function parameters should have explicit TypeScript interfaces for better type safety and documentation.

+interface DuplicateBudgetParams {
+  id: string;
+  newName: string;
+  cloudSync?: boolean;
+  open?: 'original' | 'copy' | undefined;
+}

-handlers['duplicate-budget'] = async function ({
-  id,
-  newName,
-  cloudSync,
-  open,
-}): Promise<string> {
+handlers['duplicate-budget'] = async function (params: DuplicateBudgetParams): Promise<string> {
+  const { id, newName, cloudSync, open } = params;

1969-1978: Extract cleanup logic into a separate function.

The error handling cleanup logic should be extracted into a reusable function for better maintainability.

+async function cleanupFailedDuplicate(newId: string): Promise<void> {
+  try {
+    const newBudgetDir = fs.getBudgetDir(newId);
+    if (await fs.exists(newBudgetDir)) {
+      await fs.removeDirRecursively(newBudgetDir);
+    }
+  } catch {
+    // Ignore cleanup errors
+  }
+}

 } catch (error) {
-  try {
-    const newBudgetDir = fs.getBudgetDir(newId);
-    if (await fs.exists(newBudgetDir)) {
-      await fs.removeDirRecursively(newBudgetDir);
-    }
-  } catch {} // Ignore cleanup errors
+  await cleanupFailedDuplicate(newId);
   throw new Error(`Failed to duplicate budget: ${error.message}`);
 }

2095-2096: Enhance return type safety for loadBudget function.

While adding the string type to the id parameter is good, the function's return type should also be explicitly defined for better type safety.

-async function loadBudget(id: string) {
+interface LoadBudgetResult {
+  error?: 'budget-not-found' | 'opening-budget' | 'loading-budget' | 'out-of-sync-migrations' | 'out-of-sync-data';
+}
+
+async function loadBudget(id: string): Promise<LoadBudgetResult> {
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (3)

69-70: Clarify the Comment Regarding Error Handling

Consider rephrasing the comment to enhance clarity and maintain code readability. A possible rewording:

- // The "Unknown error" should never happen, but this satisfies type checking
+ // This case should not occur; the fallback ensures type safety.

153-161: Enhance Accessibility with aria-describedby Attribute

To improve accessibility for users relying on assistive technologies, consider adding the aria-describedby attribute to the Input component. This will associate the input field with the error message, allowing screen readers to relay the error context to the user.

You can assign an id to the <FormError> component and reference it in the Input component:

<Input
  name="name"
  value={newName}
  aria-label={t('New Budget Name')}
  aria-invalid={nameError ? 'true' : 'false'}
+ aria-describedby={nameError ? 'name-error' : undefined}
  onChange={event => setNewName(event.target.value)}
  onBlur={event => validateAndSetName(event.target.value)}
  style={{ flex: 1 }}
/>

<FormError
  style={{ marginLeft: 150, color: theme.warningText }}
+ id="name-error"
>
  {nameError}
</FormError>

74-120: Ensure Consistent User Feedback on Validation Errors

When validation fails in handleDuplicate, consider setting nameError to provide immediate feedback within the modal. This ensures the user is aware of the specific validation issue without relying solely on the onComplete callback.

However, based on previous learnings, it's noted that after refactoring nameError in DuplicateFileModal.tsx, it's not necessary to use setNameError in the onPress handlers when validation fails. If this aligns with the intended design, you may disregard this suggestion.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 882c14d and d5bba7a.

📒 Files selected for processing (5)
  • packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (1 hunks)
  • packages/loot-core/src/client/actions/budgets.ts (1 hunks)
  • packages/loot-core/src/server/main.ts (7 hunks)
  • packages/loot-core/src/server/util/budget-name.ts (1 hunks)
  • packages/loot-core/src/types/server-handlers.d.ts (2 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (2)
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:144-156
Timestamp: 2024-11-10T16:45:31.225Z
Learning: In `packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx`, styles for buttons may differ for each button in the future, so avoid suggesting extraction of common styles in this file.
Learnt from: tlesicka
PR: actualbudget/actual#3689
File: packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx:181-196
Timestamp: 2024-11-10T16:45:25.627Z
Learning: After refactoring `nameError` in `DuplicateFileModal.tsx`, it's not necessary to use `setNameError` in the `onPress` handlers when validation fails.
🔇 Additional comments (6)
packages/loot-core/src/server/util/budget-name.ts (1)

Line range hint 41-64: Enhance ID generation safety and uniqueness

The current implementation has potential issues with uniqueness and safety.

Consider these improvements:

 export async function idFromBudgetName(name: string): Promise<string> {
+  // Stricter regex for safer file names
+  const safeNameRegex = /[^a-z0-9-]/gi;
-  let id = name.replace(/( |[^A-Za-z0-9])/g, '-') + '-' + uuidv4().slice(0, 7);
+  let id = name.toLowerCase().replace(safeNameRegex, '-') + '-' + uuidv4();

   // Make sure the id is unique. There's a chance one could already
   // exist (although very unlikely now that we append unique
   // characters onto the id)
-  let index = 0;
+  const MAX_ATTEMPTS = 100;
+  let attempts = 0;

   let budgetDir = fs.getBudgetDir(id);
-  while (await fs.exists(budgetDir)) {
-    index++;
-    budgetDir = fs.getBudgetDir(id + index.toString());
+  while (attempts < MAX_ATTEMPTS && await fs.exists(budgetDir)) {
+    attempts++;
+    id = name.toLowerCase().replace(safeNameRegex, '-') + '-' + uuidv4();
+    budgetDir = fs.getBudgetDir(id);
   }

-  // If a suffix was added, update the id
-  if (index > 0) {
-    id = id + index.toString();
+  if (attempts === MAX_ATTEMPTS) {
+    throw new Error('Failed to generate unique budget ID after maximum attempts');
   }

   return id;
 }

The improvements include:

  1. Using full UUID for better uniqueness
  2. Stricter regex pattern for safer file names
  3. Maximum attempts limit to prevent infinite loops
  4. Better error handling
  5. Case normalization for consistency

Let's verify the current budget IDs format:

packages/loot-core/src/types/server-handlers.d.ts (2)

306-308: LGTM! Well-defined validation handler type.

The type definition for validate-budget-name is clear and follows the existing pattern of validation handlers in the codebase.


310-310: LGTM! Clear and focused handler type.

The type definition for unique-budget-name is straightforward and maintains consistency with other naming-related handlers.

packages/loot-core/src/server/main.ts (2)

76-80: LGTM: Clean utility function imports.

The new imports for budget name validation utilities are well-organized and properly typed.


1907-1912: Consider improving the budget deletion workaround.

The current implementation has potential issues:

  1. Loading a budget just to delete it is inefficient
  2. No error handling for failed budget loads
  3. The comment acknowledges this as a temporary solution

Consider implementing a more robust solution that:

  • Directly handles file deletion without loading the budget
  • Includes proper error handling
  • Ensures cleanup of any related resources
packages/desktop-client/src/components/modals/manager/DuplicateFileModal.tsx (1)

102-104: Verify Error Handling in the Duplication Process

Ensure that the error handling within the duplication process provides clear and helpful feedback to the user. When an error occurs during duplication, verify that the user receives appropriate notifications and guidance to address the issue.

As a follow-up, you can run the following script to check for any unhandled promise rejections or console errors related to duplication:

This script helps identify instances where errors are logged to the console, allowing you to ensure that all errors are properly communicated to the user through the UI.

✅ Verification successful

These scripts will help us:

  1. Find all console.error statements and error creation patterns
  2. Examine error handling blocks in catch statements
  3. Identify error notification mechanisms used in the codebase

Based on the search results, I can now provide a final response as we have sufficient information about the error handling in the DuplicateFileModal component and the codebase's error notification system.

Error Handling is Properly Implemented

The error handling in the duplication process is well-implemented with appropriate user feedback:

  1. The code properly catches errors and creates a user-friendly error message using the translation system (t('Failed to duplicate budget')).
  2. It provides feedback through multiple channels:
    • Calls the onComplete callback with error status and details
    • Logs the error to console for debugging
    • Dispatches an error notification that appears in the UI through the app's notification system

The implementation follows the application's standard error handling patterns and integrates with the global notification system, ensuring users are properly informed when errors occur.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for console.error statements in the duplication logic.

# Find all occurrences of console.error in the codebase.
fd -e js -e ts 'console\.error' --exec cat {} +

Length of output: 51


Script:

#!/bin/bash
# Let's use ripgrep to search for console.error statements and error handling patterns
rg -U "console\.error|new Error.*Failed to duplicate" -g "*.{ts,tsx,js,jsx}" -A 3 -B 3

Length of output: 15103


Script:

#!/bin/bash
# Search for error handling patterns in the DuplicateFileModal component
ast-grep --pattern 'catch (e) {
  $$$
}'

Length of output: 42


Script:

#!/bin/bash
# Look for error notifications or toasts in the codebase
rg "toast|notification|alert.*error" -g "*.{ts,tsx,js,jsx}" -A 2

Length of output: 12940

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)
packages/loot-core/src/types/server-handlers.d.ts (1)

307-311: Add JSDoc documentation for name validation methods.

The purpose and behavior of these methods, especially unique-budget-name, could be clearer. Consider adding JSDoc documentation to explain:

  • What validation rules are applied
  • What string is returned by unique-budget-name (the validated name?)
  • The relationship between these methods

Example documentation:

/**
 * Validates a budget name against naming rules.
 * @param {Object} arg - The arguments for validation.
 * @param {string} arg.name - The budget name to validate.
 * @returns {Promise<{valid: boolean, message?: string}>} Validation result and optional error message.
 */
'validate-budget-name': ...

/**
 * Ensures a budget name is unique by appending a number if necessary.
 * @param {Object} arg - The arguments for generating unique name.
 * @param {string} arg.name - The proposed budget name.
 * @returns {Promise<string>} A unique budget name.
 */
'unique-budget-name': ...
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d5bba7a and 5aed264.

📒 Files selected for processing (1)
  • packages/loot-core/src/types/server-handlers.d.ts (2 hunks)
🔇 Additional comments (1)
packages/loot-core/src/types/server-handlers.d.ts (1)

Line range hint 307-354: LGTM! Well-integrated with existing budget operations.

The new methods:

  • Follow consistent patterns with existing budget-related methods
  • Provide complementary functionality without overlap
  • Maintain type safety throughout the interface

Comment on lines +347 to +353
'duplicate-budget': (arg: {
id?: string;
cloudId?: string;
newName: string;
cloudSync?: boolean;
open: 'none' | 'original' | 'copy';
}) => Promise<string>;
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 improving the duplicate-budget method signature.

Two suggestions for the method signature:

  1. The open parameter should be optional as it's a post-duplication preference
  2. The return type should include potential error cases for better error handling

Consider this improved type definition:

'duplicate-budget': (arg: {
  id?: string;
  cloudId?: string;
  newName: string;
  cloudSync?: boolean;
  open?: 'none' | 'original' | 'copy';
}) => Promise<{ id: string } | { error: string }>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants