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

Creation modal quality of life changes #467

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

Conversation

Blackshadow8910
Copy link
Contributor

@Blackshadow8910 Blackshadow8910 commented Jan 13, 2025

What type of PR is this?

  • feature

What this PR does / why we need it:

This PR does the following:

  • Autofills the parent location field in the create location modal
  • Focuses on the name fields when opening any create modal (Though looking at the code, I think it was already supposed to do that)
  • Rearranged the create modals to have the label and location inputs first
  • Changed the color of the multiselect border to match other components
  • Fixed the incomplete create modal shortcuts I made in Added keyboard accessible shortcut menu for create modals #457 (oops)

I think this layout for the modals is a little more consistent and efficient, since having the labels on top makes it possible to tab to the create button. Any other opinions on this would be appreciated though.

Which issue(s) this PR fixes:

Fixes #411

Summary by CodeRabbit

Release Notes

  • New Features

    • Added option to close modal by clicking outside.
    • Introduced a new multiselect component for label selection in item creation.
  • Improvements

    • Enhanced modal focus management for better user experience.
    • Refined label and location selection in create modals.
    • Updated keyboard shortcuts for creating items, locations, and labels.
  • Style Updates

    • Updated styling for multiselect component border colors.
  • User Experience

    • More intuitive modal interactions.
    • Simplified shortcut key mappings.

Copy link
Contributor

coderabbitai bot commented Jan 13, 2025

Walkthrough

This pull request introduces several enhancements to frontend components, focusing on modal interactions, form management, and keyboard shortcut handling. The changes primarily involve updating component props, modifying CSS classes, and refining modal focus and interaction behaviors. Key modifications include adding a clickOutsideToClose prop to modals, updating location and label creation workflows, and revising keyboard shortcut implementations in the default layout.

Changes

File Change Summary
frontend/components/Base/Modal.vue Added clickOutsideToClose prop to conditionally close modal when clicking outside
frontend/components/Form/Multiselect.vue Updated border color classes for styling elements
frontend/components/Item/CreateModal.vue Replaced whenever with watch for modal focus management
frontend/components/Label/CreateModal.vue Replaced whenever with watch for modal state management
frontend/components/Location/CreateModal.vue Added location-related computed properties, updated modal focus logic
frontend/components/global/QuickMenu/Modal.vue Added :click-outside-to-close="true" prop
frontend/layouts/default.vue Updated keyboard shortcuts, added activeElement handling
frontend/components/global/QuickMenu/Input.vue Modified watch function behavior regarding inputBoxFocused state

Assessment against linked issues

Objective Addressed Explanation
Fill parent location field when creating location

Possibly related PRs

  • show add photo button on mobile #229: The changes in CreateModal.vue involve modifications to the modal's layout and styling, which may relate to the overall user interaction improvements introduced in the main PR's Modal.vue changes.
  • Added keyboard accessible shortcut menu for create modals #457: This PR modifies the BaseModal.vue to enhance modal functionality, including handling clicks outside the modal, which directly relates to the clickOutsideToClose property introduced in the main PR.

Suggested Reviewers

  • tankerkiller125
  • tonyaellie

Security Recommendations

  1. Input Validation: Ensure that the new clickOutsideToClose prop is properly sanitized to prevent potential XSS vulnerabilities.
  2. Keyboard Shortcut Security: Validate that the new keyboard shortcut implementation doesn't introduce unintended access to sensitive actions.
  3. Modal Interaction: Implement additional checks to prevent unauthorized modal interactions, especially with the new click-outside functionality.
  4. Route Parameter Handling: Validate and sanitize locationId and labelId to prevent potential injection or manipulation.
  5. Timeout Function Security: Ensure that timeout functions used for focus management cannot be exploited for malicious purposes.

📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 026fbae and 224b669.

📒 Files selected for processing (7)
  • frontend/components/Base/Modal.vue (2 hunks)
  • frontend/components/Form/Multiselect.vue (2 hunks)
  • frontend/components/Item/CreateModal.vue (1 hunks)
  • frontend/components/Label/CreateModal.vue (1 hunks)
  • frontend/components/Location/CreateModal.vue (3 hunks)
  • frontend/components/global/QuickMenu/Input.vue (1 hunks)
  • frontend/layouts/default.vue (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • frontend/components/Form/Multiselect.vue
  • frontend/components/global/QuickMenu/Input.vue
  • frontend/components/Item/CreateModal.vue
  • frontend/components/Base/Modal.vue
  • frontend/components/Label/CreateModal.vue
  • frontend/layouts/default.vue
⏰ Context from checks skipped due to timeout of 90000ms (6)
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (1)
frontend/components/Location/CreateModal.vue (1)

Line range hint 5-13: Consider adding input sanitization for security.

While v-model provides basic XSS protection, consider adding explicit input sanitization for the name and description fields before submission to protect against injection attacks.

Run this check to verify other form implementations:


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
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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/components/Base/Modal.vue (1)

58-62: Add rate limiting to click outside handler

The click outside handler could be triggered rapidly, potentially causing race conditions.

Consider debouncing the handler:

  if (props.clickOutsideToClose) {
+   const debouncedClose = useDebounceFn(() => {
+     close();
+   }, 300);
    onClickOutside(modalBox, () => {
-     close();
+     debouncedClose();
    });
  }
frontend/layouts/default.vue (1)

203-208: Sanitize shortcut keys before use

The current implementation directly uses shortcut keys without sanitization. While the risk is low since shortcuts are defined internally, it's good practice to sanitize all user inputs.

Apply this diff to add sanitization:

  if (option?.shortcut) {
-   const shortcutKeycode = option.shortcut.replace(/([0-9])/, "digit$&");
+   // Sanitize shortcut key to only allow specific characters
+   const sanitizedShortcut = option.shortcut.replace(/[^0-9!@#]/, "");
+   if (sanitizedShortcut !== option.shortcut) {
+     console.error('Invalid shortcut key');
+     return;
+   }
+   const shortcutKeycode = sanitizedShortcut.replace(/([0-9])/, "digit$&");
    whenever(keys[shortcutKeycode], () => {
      if (activeElement.value?.tagName !== "INPUT") {
        option.action();
      }
    });
  }
🛑 Comments failed to post (3)
frontend/components/Base/Modal.vue (1)

44-47: 💡 Codebase verification

⚠️ Potential issue

🔒 Add event listener cleanup to prevent security vulnerabilities

The click outside handler needs proper cleanup to prevent:

  • Memory leaks affecting application performance
  • Potential information disclosure if modal content remains accessible
  • Resource exhaustion from accumulated event listeners
+ const stopClickOutside = ref();

  if (props.clickOutsideToClose) {
-    onClickOutside(modalBox, () => {
+    stopClickOutside.value = onClickOutside(modalBox, () => {
      close();
    });
  }

+ onUnmounted(() => {
+   if (stopClickOutside.value) {
+     stopClickOutside.value();
+   }
+ });
🔗 Analysis chain

Ensure proper cleanup of click outside handler

The click outside handler is added conditionally but there's no cleanup when the component is unmounted.

Add cleanup to prevent memory leaks and potential security issues:

+ const stopClickOutside = ref();

  if (props.clickOutsideToClose) {
-    onClickOutside(modalBox, () => {
+    stopClickOutside.value = onClickOutside(modalBox, () => {
      close();
    });
  }

+ onUnmounted(() => {
+   if (stopClickOutside.value) {
+     stopClickOutside.value();
+   }
+ });
🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Find onClickOutside implementation and usage
ast-grep --pattern 'const onClickOutside = $_'

# Search for similar modal cleanup patterns
rg "onClickOutside.*modal" -A 5 -B 5

# Find unmounted cleanup patterns
ast-grep --pattern 'onUnmounted(() => {
  $$$
})'

Length of output: 4153


Script:

#!/bin/bash
# Search for onClickOutside implementation in composables
fd "use.*\.ts$" --exec cat {} \; 

# Look for vueuse imports as it might be from there
rg "@vueuse/.*onClickOutside" -A 2 -B 2

Length of output: 26563

frontend/components/Location/CreateModal.vue (1)

98-103: ⚠️ Potential issue

Validate route parameters to prevent path traversal

The location ID from route parameters needs validation to prevent potential security issues.

Add validation:

  const locationId = computed(() => {
    if (route.fullPath.includes("/location/")) {
-     return route.params.id;
+     const id = route.params.id;
+     // Ensure id is a valid UUID or your expected format
+     const validIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+     return validIdPattern.test(id) ? id : null;
    }
    return null;
  });
📝 Committable suggestion

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

  const locationId = computed(() => {
    if (route.fullPath.includes("/location/")) {
      const id = route.params.id;
      // Ensure id is a valid UUID or your expected format
      const validIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
      return validIdPattern.test(id) ? id : null;
    }
    return null;
  });
frontend/components/Item/CreateModal.vue (1)

144-163: ⚠️ Potential issue

Validate route parameters before use

The current implementation directly uses route parameters without validation. This could potentially lead to XSS attacks if the route parameters contain malicious content.

Apply this diff to add validation:

  if (locationId.value) {
+   // Validate locationId is a valid UUID
+   if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(locationId.value)) {
+     console.error('Invalid location ID format');
+     return;
+   }
    const found = locations.value.find(l => l.id === locationId.value);
    if (found) {
      form.location = found;
    }
  }

  if (labelId.value) {
+   // Validate labelId is a valid UUID
+   if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(labelId.value)) {
+     console.error('Invalid label ID format');
+     return;
+   }
    form.labels = labels.value.filter(l => l.id === labelId.value);
  }
📝 Committable suggestion

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

  watch(
    () => modal.value,
    (open) => {
      if (open) {
        useTimeoutFn(() => { focused.value = true }, 50);
  
        if (locationId.value) {
          // Validate locationId is a valid UUID
          if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(locationId.value)) {
            console.error('Invalid location ID format');
            return;
          }
          const found = locations.value.find(l => l.id === locationId.value);
          if (found) {
            form.location = found;
          }
        }
  
        if (labelId.value) {
          // Validate labelId is a valid UUID
          if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(labelId.value)) {
            console.error('Invalid label ID format');
            return;
          }
          form.labels = labels.value.filter(l => l.id === labelId.value);
        }
      } else {
        focused.value = false;
      }
    }

@tonyaellie
Copy link
Collaborator

tonyaellie commented Jan 13, 2025

Linting needs to be run, the labels dropdown seems to get hidden on small screens
image
The shortcut for creating an item seems to work but the other two dont for me.
Using the quick select when one of the options are selected and you click outside of the model acts as if you selected option.
image

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

🧹 Nitpick comments (1)
frontend/components/global/QuickMenu/Input.vue (1)

Line range hint 1-116: Consider additional security measures for the quick menu component.

While reviewing the component, I noticed some potential security considerations:

  1. Input validation: Consider sanitizing user input before using it in the filtering logic to prevent potential XSS attacks through action text.
  2. Action execution safeguards: The quick select feature (single-character shortcuts) could benefit from an additional confirmation step for destructive actions.
  3. Rate limiting: Consider adding debounce to prevent rapid-fire action execution.

Here's a suggested implementation for input sanitization:

  const filteredActions = computed(() => {
-   const searchTerm = inputValue.value.toLowerCase();
+   const searchTerm = sanitizeInput(inputValue.value.toLowerCase());
    return (props.actions || []).filter(action => {
      return action.text.toLowerCase().includes(searchTerm) || action.shortcut?.includes(searchTerm);
    });
  });

+ function sanitizeInput(input: string): string {
+   // Remove potentially dangerous characters while preserving legitimate search terms
+   return input.replace(/[<>'"]/g, '');
+ }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9f511e3 and 026fbae.

📒 Files selected for processing (2)
  • frontend/components/Item/CreateModal.vue (2 hunks)
  • frontend/components/global/QuickMenu/Input.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/components/Item/CreateModal.vue
⏰ Context from checks skipped due to timeout of 90000ms (7)
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: build (linux/arm/v7)
  • GitHub Check: Frontend and End-to-End Tests / Integration Tests
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/arm64)
  • GitHub Check: build (linux/amd64)
  • GitHub Check: build (linux/amd64)
🔇 Additional comments (1)
frontend/components/global/QuickMenu/Input.vue (1)

84-85: LGTM! The focus handling change improves security.

The simplified focus handling logic that only clears input on blur is better than automatically revealing actions, as it reduces the risk of unintended action execution.

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
frontend/components/Location/CreateModal.vue (1)

Line range hint 108-143: Enhance security and error handling in create function.

The current implementation has several security and reliability concerns:

  1. No rate limiting on form submission
  2. Basic error handling
  3. Unconstrained navigation

Consider implementing:

  1. Rate limiting for form submissions
  2. More detailed error handling
  3. Navigation validation
+ const SUBMIT_COOLDOWN = 1000; // 1 second
+ let lastSubmitTime = 0;
+
  async function create(close = true) {
    if (loading.value) {
      toast.error("Already creating a location");
      return;
    }
+   
+   // Rate limiting
+   const now = Date.now();
+   if (now - lastSubmitTime < SUBMIT_COOLDOWN) {
+     toast.error("Please wait before submitting again");
+     return;
+   }
+   lastSubmitTime = now;
+
    loading.value = true;

    if (shift.value) {
      close = false;
    }

    const { data, error } = await api.locations.create({
      name: form.name,
      description: form.description,
      parentId: form.parent ? form.parent.id : null,
    });

    if (error) {
      loading.value = false;
-     toast.error("Couldn't create location");
+     // More detailed error handling
+     toast.error(`Failed to create location: ${error.message || 'Unknown error'}`);
+     return;
    }

    if (data) {
+     // Validate data.id before navigation
+     if (!data.id || typeof data.id !== 'string') {
+       toast.error("Invalid response from server");
+       loading.value = false;
+       return;
+     }
      toast.success("Location created");
    }
    reset();

    if (close) {
      modal.value = false;
-     navigateTo(`/location/${data.id}`);
+     // Safe navigation with validated id
+     if (data?.id) {
+       navigateTo(`/location/${encodeURIComponent(data.id)}`);
+     }
    }
  }
🛑 Comments failed to post (2)
frontend/components/Location/CreateModal.vue (2)

100-105: ⚠️ Potential issue

Enhance route parameter validation for security.

The current implementation directly uses route parameters without proper validation, which could lead to security vulnerabilities.

Consider this safer implementation:

  const locationId = computed(() => {
    if (route.fullPath.includes("/location/")) {
-     return route.params.id;
+     const id = route.params.id;
+     // Validate id format (assuming UUID format)
+     return typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)
+       ? id
+       : null;
    }
    return null;
  });
📝 Committable suggestion

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

const locationId = computed(() => {
    if (route.fullPath.includes("/location/")) {
      const id = route.params.id;
      // Validate id format (assuming UUID format)
      return typeof id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)
        ? id
        : null;
    }
    return null;
  });

62-79: 🛠️ Refactor suggestion

Improve timing and cleanup in watch handler.

The current implementation has several potential issues:

  1. The 50ms timeout is arbitrary and might not be reliable across different devices
  2. Missing cleanup for the timeout on component unmount
  3. Potential race condition between timeout and location finding

Consider this improved implementation:

+ const timeoutRef = ref<number | null>(null);
+ 
  watch(
    () => modal.value,
    open => {
      if (open) {
-       useTimeoutFn(() => {
-         focused.value = true;
-       }, 50);
+       // Clear any existing timeout
+       if (timeoutRef.value) clearTimeout(timeoutRef.value);
+       // Set new timeout and store reference
+       timeoutRef.value = setTimeout(() => {
+         focused.value = true;
+         timeoutRef.value = null;
+       }, 50);

        if (locationId.value) {
          const found = locations.value.find(l => l.id === locationId.value);
          if (found) {
            form.parent = found;
          }
        }
      } else {
        focused.value = false;
+       // Clear timeout on modal close
+       if (timeoutRef.value) {
+         clearTimeout(timeoutRef.value);
+         timeoutRef.value = null;
+       }
      }
    }
  );
+
+ // Cleanup on component unmount
+ onBeforeUnmount(() => {
+   if (timeoutRef.value) {
+     clearTimeout(timeoutRef.value);
+     timeoutRef.value = null;
+   }
+ });
📝 Committable suggestion

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

  const timeoutRef = ref<number | null>(null);
  
  watch(
    () => modal.value,
    open => {
      if (open) {
        // Clear any existing timeout
        if (timeoutRef.value) clearTimeout(timeoutRef.value);
        // Set new timeout and store reference
        timeoutRef.value = setTimeout(() => {
          focused.value = true;
          timeoutRef.value = null;
        }, 50);

        if (locationId.value) {
          const found = locations.value.find(l => l.id === locationId.value);
          if (found) {
            form.parent = found;
          }
        }
      } else {
        focused.value = false;
        // Clear timeout on modal close
        if (timeoutRef.value) {
          clearTimeout(timeoutRef.value);
          timeoutRef.value = null;
        }
      }
    }
  );

  // Cleanup on component unmount
  onBeforeUnmount(() => {
    if (timeoutRef.value) {
      clearTimeout(timeoutRef.value);
      timeoutRef.value = null;
    }
  });

@tankerkiller125
Copy link
Contributor

Sorry for the broken Docker builds, we're working on sorting that out. However, the linting issues should be correct.

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

Successfully merging this pull request may close these issues.

Fill the parent location field when creating a location being inside another location
3 participants