Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add Input Mask Validation and Support in Form Requests and Components#776

Closed
chiragchhatrala wants to merge 18 commits intomainOpnForm/OpnForm:mainfrom
1fca6-text-input-maskOpnForm/OpnForm:1fca6-text-input-maskCopy head branch name to clipboard
Closed

Add Input Mask Validation and Support in Form Requests and Components#776
chiragchhatrala wants to merge 18 commits intomainOpnForm/OpnForm:mainfrom
1fca6-text-input-maskOpnForm/OpnForm:1fca6-text-input-maskCopy head branch name to clipboard

Conversation

@chiragchhatrala
Copy link
Copy Markdown
Collaborator

@chiragchhatrala chiragchhatrala commented Jun 6, 2025

  • Introduced a new InputMaskRule for validating input masks in form requests, enhancing data integrity for text fields.
  • Updated AnswerFormRequest and UserFormRequest to include input mask validation rules, allowing for flexible input formats.
  • Enhanced TextInput component to support input masks, improving user experience with formatted input fields.
  • Added useInputMask composable for managing input mask logic, ensuring consistent behavior across components.
  • Updated FieldOptions to allow configuration of input masks for text fields, providing users with more customization options.

These changes aim to improve form handling and user input validation, ensuring that data adheres to specified formats while enhancing the overall user experience in form interactions.

Summary by CodeRabbit

  • New Features

    • Added support for input masks on text fields, allowing users to enter data in specific formats (e.g., phone numbers, custom patterns).
    • Introduced a new masked input component for improved data entry and validation.
    • Added an input mask pattern editor in the form field options, including help and validation for mask syntax.
    • Implemented reactive input masking utilities for dynamic formatting and validation in forms.
  • Bug Fixes

    • Input mask validation now ensures user input matches the specified pattern, preventing invalid entries.
  • Tests

    • Added comprehensive tests to ensure input mask validation works correctly for various patterns.

- Introduced a new `InputMaskRule` for validating input masks in form requests, enhancing data integrity for text fields.
- Updated `AnswerFormRequest` and `UserFormRequest` to include input mask validation rules, allowing for flexible input formats.
- Enhanced `TextInput` component to support input masks, improving user experience with formatted input fields.
- Added `useInputMask` composable for managing input mask logic, ensuring consistent behavior across components.
- Updated `FieldOptions` to allow configuration of input masks for text fields, providing users with more customization options.

These changes aim to improve form handling and user input validation, ensuring that data adheres to specified formats while enhancing the overall user experience in form interactions.
@chiragchhatrala chiragchhatrala requested a review from JhumanJ June 6, 2025 15:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 6, 2025

Walkthrough

This update introduces input mask support for text fields across both backend and frontend. It adds a custom input mask validation rule, updates form requests to handle and validate input masks, implements a composable for mask logic, and integrates a new masked input component into the form system with UI support for specifying and previewing input masks.

Changes

Files/Paths Change Summary
api/app/Rules/InputMaskRule.php
api/tests/Unit/Rules/InputMaskRuleTest.php
Added custom InputMaskRule validation class and comprehensive unit tests for input mask patterns.
api/app/Http/Requests/AnswerFormRequest.php
api/app/Http/Requests/UserFormRequest.php
Integrated input mask validation into form request rules for text fields and validated the input_mask attribute in form definitions.
client/composables/useInputMask.js Introduced a Vue composable for parsing, formatting, and validating input values against mask patterns.
client/components/forms/heavy/MaskInput.vue Added a new masked input Vue component supporting dynamic mask formatting, validation, and integration with forms.
client/components/open/forms/OpenFormField.vue Updated to render MaskInput for text fields with an input mask, passing the mask pattern as a prop.
client/components/open/forms/fields/components/FieldOptions.vue Added UI for specifying input mask patterns in field options, including validation, help tooltip, and Crisp helpdesk integration.
client/composables/components/useComponentRegistry.js Registered the new MaskInput component in the form components registry for dynamic usage.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FormUI
    participant MaskInput
    participant useInputMask
    participant Backend

    User->>FormUI: Opens form with text field (with input_mask)
    FormUI->>MaskInput: Renders with mask prop
    MaskInput->>useInputMask: Parses mask, formats input
    User->>MaskInput: Types input
    MaskInput->>useInputMask: Formats and validates input
    MaskInput->>FormUI: Emits masked/unmasked value
    FormUI->>Backend: Submits value
    Backend->>InputMaskRule: Validates value against mask
    InputMaskRule-->>Backend: Pass/Fail
    Backend-->>FormUI: Returns validation result
    FormUI-->>User: Displays success or error
Loading

Suggested reviewers

  • JhumanJ

Poem

In fields where forms and inputs grow,
A rabbit hops with masks in tow.
From nine to star and letter a,
Your data’s shaped the proper way.
With MaskInput new and rules so neat,
Validation’s now a bunny treat!
🐰✨


📜 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 f18b914 and 516ca53.

📒 Files selected for processing (1)
  • client/components/open/forms/fields/components/FieldOptions.vue (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/components/open/forms/fields/components/FieldOptions.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build the Nuxt app

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ 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.
    • Explain this complex logic.
    • 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 explain this code block.
    • @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 explain its main purpose.
    • @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.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

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.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this 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
Copy Markdown
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: 6

🔭 Outside diff range comments (1)
client/components/forms/TextInput.vue (1)

64-67: ⚠️ Potential issue

Add missing imports for Vue composition API and useInputMask.

Several Vue composition API functions and the useInputMask composable are used but not imported.

 import {inputProps, useFormInput} from "./useFormInput.js"
 import InputWrapper from "./components/InputWrapper.vue"
+import { ref, computed, watch, nextTick } from 'vue'
+import { useInputMask } from '../../composables/useInputMask.js'
🧹 Nitpick comments (2)
api/app/Http/Requests/UserFormRequest.php (1)

162-162: Input mask validation rule looks correct.

The regex pattern appropriately restricts input mask characters to commonly used mask symbols. The sometimes|nullable rules correctly make this optional.

Consider adding a comment explaining the regex pattern for future maintainability:

+ // Input mask validation: 9=digit, a=letter, *=alphanumeric, ().-? and spaces allowed
  'properties.*.input_mask' => 'sometimes|nullable|string|regex:/^[9a*().\\-?\\s]*$/',
client/components/forms/TextInput.vue (1)

126-136: Reconsider the auto-clear behavior for better UX.

Automatically clearing the entire input when the user backspaces might be frustrating. Consider keeping the partial input to allow users to correct mistakes without starting over.

 // Auto-clear if incomplete and user is backspacing/clearing
 if (!isComplete(formatted) && inputValue.length > formatted.length) {
-  maskedValue.value = ''
-  compVal.value = ''
-  nextTick(() => {
-    if (inputRef.value) {
-      inputRef.value.value = ''
-    }
-  })
-  return
+  // Keep the formatted partial input instead of clearing
+  maskedValue.value = formatted
+  compVal.value = formatted
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 705bff4 and e36fa7b.

📒 Files selected for processing (8)
  • api/app/Http/Requests/AnswerFormRequest.php (2 hunks)
  • api/app/Http/Requests/UserFormRequest.php (1 hunks)
  • api/app/Rules/InputMaskRule.php (1 hunks)
  • api/tests/Unit/Rules/InputMaskRuleTest.php (1 hunks)
  • client/components/forms/TextInput.vue (4 hunks)
  • client/components/open/forms/OpenFormField.vue (1 hunks)
  • client/components/open/forms/fields/components/FieldOptions.vue (1 hunks)
  • client/composables/useInputMask.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
client/composables/useInputMask.js

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🪛 GitHub Check: Run client linters
client/components/forms/TextInput.vue

[failure] 87-87:
'inputWrapperProps' is assigned a value but never used

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
  • GitHub Check: Build the Nuxt app
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
🔇 Additional comments (8)
client/components/open/forms/OpenFormField.vue (1)

321-323: LGTM - Clean integration of input mask property.

The conditional logic correctly passes the input mask from field configuration to the input component properties. The implementation is straightforward and integrates well with the existing property handling pattern.

client/components/open/forms/fields/components/FieldOptions.vue (1)

225-233: Well-designed UI for input mask configuration.

The conditional rendering (!field.multi_lines) appropriately restricts input masks to single-line text fields. The placeholder and help text provide excellent user guidance with clear format explanation and practical examples.

api/app/Http/Requests/AnswerFormRequest.php (2)

7-7: Correct import of InputMaskRule.

The import statement properly adds the InputMaskRule class for use in validation.


195-199: Solid conditional validation implementation.

The logic correctly applies input mask validation only when an input mask is specified, with appropriate fallback to basic string validation. The defensive check for both existence and truthiness of the input_mask property is good practice.

api/tests/Unit/Rules/InputMaskRuleTest.php (3)

5-25: LGTM! Well-structured test for phone number mask validation.

The test properly validates both passing and failing cases for phone number format masks.


27-42: LGTM! Good coverage for numeric-only mask validation.

The test correctly validates that the mask accepts only numeric values and rejects alphanumeric inputs.


44-78: LGTM! Comprehensive test coverage for mixed character masks.

Excellent coverage of edge cases including missing characters, extra characters, wrong character types, and missing literals. The test thoroughly validates the mask parsing and validation logic.

api/app/Rules/InputMaskRule.php (1)

12-27: LGTM! Well-structured validation flow.

Good practice to validate the mask pattern before using it to validate the value. The error messages are clear and informative.

Comment on lines +82 to +109
private function parseMask(string $mask): array
{
$tokens = [];
$optional = false;
$patterns = [
'9' => '/[0-9]/',
'a' => '/[a-zA-Z]/',
'*' => '/[a-zA-Z0-9]/'
];

for ($i = 0; $i < strlen($mask); $i++) {
$char = $mask[$i];

if ($char === '?') {
$optional = true;
continue;
}

$tokens[] = [
'char' => $char,
'regex' => $patterns[$char] ?? null,
'literal' => !isset($patterns[$char]),
'optional' => $optional
];
}

return $tokens;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the optional flag handling in mask parsing.

The optional flag is never reset after being set, making all subsequent tokens optional after the first '?'. This would incorrectly parse masks like "999?-999" where only the third digit should be optional.

 private function parseMask(string $mask): array
 {
     $tokens = [];
-    $optional = false;
     $patterns = [
         '9' => '/[0-9]/',
         'a' => '/[a-zA-Z]/',
         '*' => '/[a-zA-Z0-9]/'
     ];

     for ($i = 0; $i < strlen($mask); $i++) {
         $char = $mask[$i];

         if ($char === '?') {
-            $optional = true;
+            // Mark the previous token as optional
+            if (count($tokens) > 0) {
+                $tokens[count($tokens) - 1]['optional'] = true;
+            }
             continue;
         }

         $tokens[] = [
             'char' => $char,
             'regex' => $patterns[$char] ?? null,
             'literal' => !isset($patterns[$char]),
-            'optional' => $optional
+            'optional' => false
         ];
     }

     return $tokens;
 }
📝 Committable suggestion

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

Suggested change
private function parseMask(string $mask): array
{
$tokens = [];
$optional = false;
$patterns = [
'9' => '/[0-9]/',
'a' => '/[a-zA-Z]/',
'*' => '/[a-zA-Z0-9]/'
];
for ($i = 0; $i < strlen($mask); $i++) {
$char = $mask[$i];
if ($char === '?') {
$optional = true;
continue;
}
$tokens[] = [
'char' => $char,
'regex' => $patterns[$char] ?? null,
'literal' => !isset($patterns[$char]),
'optional' => $optional
];
}
return $tokens;
}
private function parseMask(string $mask): array
{
$tokens = [];
$patterns = [
'9' => '/[0-9]/',
'a' => '/[a-zA-Z]/',
'*' => '/[a-zA-Z0-9]/'
];
for ($i = 0; $i < strlen($mask); $i++) {
$char = $mask[$i];
if ($char === '?') {
// Mark the previous token as optional
if (count($tokens) > 0) {
$tokens[count($tokens) - 1]['optional'] = true;
}
continue;
}
$tokens[] = [
'char' => $char,
'regex' => $patterns[$char] ?? null,
'literal' => !isset($patterns[$char]),
'optional' => false
];
}
return $tokens;
}
🤖 Prompt for AI Agents
In api/app/Rules/InputMaskRule.php between lines 82 and 109, the optional flag
is set to true when a '?' character is encountered but never reset, causing all
following tokens to be marked optional incorrectly. To fix this, modify the
logic so that the optional flag applies only to the token immediately following
the '?'. After processing that token, reset the optional flag back to false
before continuing to parse the rest of the mask.

Comment on lines +29 to +32
private function validateMaskPattern(string $mask): bool
{
return preg_match('/^[9a*().\\-?\\s]*$/', $mask);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove backslash from the mask pattern regex.

The regex allows backslashes for escaping, but the parseMask method doesn't handle escaped characters. This could lead to unexpected behavior.

 private function validateMaskPattern(string $mask): bool
 {
-    return preg_match('/^[9a*().\\-?\\s]*$/', $mask);
+    return preg_match('/^[9a*().\-?\s]*$/', $mask);
 }
📝 Committable suggestion

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

Suggested change
private function validateMaskPattern(string $mask): bool
{
return preg_match('/^[9a*().\\-?\\s]*$/', $mask);
}
private function validateMaskPattern(string $mask): bool
{
return preg_match('/^[9a*().\-?\s]*$/', $mask);
}
🤖 Prompt for AI Agents
In api/app/Rules/InputMaskRule.php around lines 29 to 32, the regex pattern in
validateMaskPattern currently includes a backslash character which allows
escaping, but since parseMask does not handle escaped characters, this can cause
unexpected behavior. Remove the backslash from the regex pattern so that
backslashes are not allowed in the mask string, ensuring consistency with
parseMask's handling.

Comment on lines +13 to +34
const parseMask = (maskValue) => {
if (!maskValue) return []

const tokens = []
let optional = false

for (let i = 0; i < maskValue.length; i++) {
const char = maskValue[i]
if (char === '?') {
optional = true
continue
}

tokens.push({
char,
regex: maskTokens[char] || null,
literal: !maskTokens[char],
optional
})
}
return tokens
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the optional flag handling to match the backend implementation.

The optional flag is never reset, making all tokens after a '?' optional. This should mark only the preceding token as optional.

 const parseMask = (maskValue) => {
   if (!maskValue) return []

   const tokens = []
-  let optional = false

   for (let i = 0; i < maskValue.length; i++) {
     const char = maskValue[i]
     if (char === '?') {
-      optional = true
+      // Mark the previous token as optional
+      if (tokens.length > 0) {
+        tokens[tokens.length - 1].optional = true
+      }
       continue
     }

     tokens.push({
       char,
       regex: maskTokens[char] || null,
       literal: !maskTokens[char],
-      optional
+      optional: false
     })
   }
   return tokens
 }
📝 Committable suggestion

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

Suggested change
const parseMask = (maskValue) => {
if (!maskValue) return []
const tokens = []
let optional = false
for (let i = 0; i < maskValue.length; i++) {
const char = maskValue[i]
if (char === '?') {
optional = true
continue
}
tokens.push({
char,
regex: maskTokens[char] || null,
literal: !maskTokens[char],
optional
})
}
return tokens
}
const parseMask = (maskValue) => {
if (!maskValue) return []
const tokens = []
for (let i = 0; i < maskValue.length; i++) {
const char = maskValue[i]
if (char === '?') {
// Mark the previous token as optional
if (tokens.length > 0) {
tokens[tokens.length - 1].optional = true
}
continue
}
tokens.push({
char,
regex: maskTokens[char] || null,
literal: !maskTokens[char],
optional: false
})
}
return tokens
}
🤖 Prompt for AI Agents
In client/composables/useInputMask.js between lines 13 and 34, the optional flag
is set to true when encountering a '?' but never reset, causing all subsequent
tokens to be marked optional incorrectly. Modify the logic so that the optional
flag applies only to the token immediately before the '?'. This means you should
mark the previous token as optional when you see '?', and not set a persistent
optional flag for following tokens.

Comment on lines +79 to +82
const getUnmaskedValue = (value) => {
if (!value) return value
return value.replace(/[^\w]/g, '')
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Update getUnmaskedValue to correctly extract input characters.

The function should extract only the actual input characters (digits and letters), not remove all non-word characters.

 const getUnmaskedValue = (value) => {
   if (!value) return value
-  return value.replace(/[^\w]/g, '')
+  // Extract only alphanumeric characters that represent actual input
+  return value.replace(/[^0-9a-zA-Z]/g, '')
 }
📝 Committable suggestion

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

Suggested change
const getUnmaskedValue = (value) => {
if (!value) return value
return value.replace(/[^\w]/g, '')
}
const getUnmaskedValue = (value) => {
if (!value) return value
// Extract only alphanumeric characters that represent actual input
return value.replace(/[^0-9a-zA-Z]/g, '')
}
🤖 Prompt for AI Agents
In client/composables/useInputMask.js around lines 79 to 82, the
getUnmaskedValue function currently removes all non-word characters but does not
correctly extract only the actual input characters (digits and letters). Update
the function to explicitly extract and return only digits and letters from the
input value, ensuring it filters out any other characters without simply
removing non-word characters.

Comment on lines +49 to +77
const formatValue = (value) => {
if (!mask.value || !value) return value

const tokens = parsedMask.value
const cleanValue = value.replace(/[^\w]/g, '')
let formatted = ''
let valueIndex = 0

for (const token of tokens) {
if (token.literal) {
formatted += token.char
continue
}

if (valueIndex >= cleanValue.length) {
if (token.optional) break
continue
}

if (token.regex && token.regex.test(cleanValue[valueIndex])) {
formatted += cleanValue[valueIndex]
valueIndex++
} else if (!token.optional) {
break
}
}

return formatted
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the input cleaning regex and apply optional chaining.

The current regex removes all non-word characters, including valid mask literals like parentheses and hyphens. Also, apply the static analysis suggestion for optional chaining.

 const formatValue = (value) => {
   if (!mask.value || !value) return value

   const tokens = parsedMask.value
-  const cleanValue = value.replace(/[^\w]/g, '')
+  // Only remove characters that aren't part of the mask pattern
+  let cleanValue = ''
+  for (const char of value) {
+    if (/[0-9a-zA-Z]/.test(char)) {
+      cleanValue += char
+    }
+  }
   let formatted = ''
   let valueIndex = 0
   
   for (const token of tokens) {
     if (token.literal) {
       formatted += token.char
       continue
     }
     
     if (valueIndex >= cleanValue.length) {
       if (token.optional) break
       continue
     }
     
-    if (token.regex && token.regex.test(cleanValue[valueIndex])) {
+    if (token.regex?.test(cleanValue[valueIndex])) {
       formatted += cleanValue[valueIndex]
       valueIndex++
     } else if (!token.optional) {
       break
     }
   }
   
   return formatted
 }
📝 Committable suggestion

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

Suggested change
const formatValue = (value) => {
if (!mask.value || !value) return value
const tokens = parsedMask.value
const cleanValue = value.replace(/[^\w]/g, '')
let formatted = ''
let valueIndex = 0
for (const token of tokens) {
if (token.literal) {
formatted += token.char
continue
}
if (valueIndex >= cleanValue.length) {
if (token.optional) break
continue
}
if (token.regex && token.regex.test(cleanValue[valueIndex])) {
formatted += cleanValue[valueIndex]
valueIndex++
} else if (!token.optional) {
break
}
}
return formatted
}
const formatValue = (value) => {
if (!mask.value || !value) return value
const tokens = parsedMask.value
// Only remove characters that aren't part of the mask pattern
let cleanValue = ''
for (const char of value) {
if (/[0-9a-zA-Z]/.test(char)) {
cleanValue += char
}
}
let formatted = ''
let valueIndex = 0
for (const token of tokens) {
if (token.literal) {
formatted += token.char
continue
}
if (valueIndex >= cleanValue.length) {
if (token.optional) break
continue
}
if (token.regex?.test(cleanValue[valueIndex])) {
formatted += cleanValue[valueIndex]
valueIndex++
} else if (!token.optional) {
break
}
}
return formatted
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🤖 Prompt for AI Agents
In client/composables/useInputMask.js between lines 49 and 77, the regex used to
clean the input value removes all non-word characters, which incorrectly strips
valid mask literals such as parentheses and hyphens. Update the regex to only
remove characters that are not allowed by the mask, preserving valid literals.
Additionally, apply optional chaining where appropriate to safely access nested
properties without errors.

Comment thread client/components/forms/TextInput.vue Outdated
Comment on lines +85 to +93
const { formatValue, isComplete, getMaskPlaceholder, isValidMask } = useInputMask(() => props.mask)

const { compVal, inputWrapperProps } = useFormInput(
props,
context,
{
formPrefixKey: props.nativeType === "file" ? "file-" : null
},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate useFormInput call and fix unused variable.

The useFormInput composable is called twice, causing duplicate initialization. The first call's inputWrapperProps is never used.

 setup(props, context) {
   const { formatValue, isComplete, getMaskPlaceholder, isValidMask } = useInputMask(() => props.mask)

-  const { compVal, inputWrapperProps } = useFormInput(
-    props,
-    context,
-    {
-      formPrefixKey: props.nativeType === "file" ? "file-" : null
-    },
-  )
+  const formInputResult = useFormInput(
+    props,
+    context,
+    {
+      formPrefixKey: props.nativeType === "file" ? "file-" : null
+    },
+  )
+  const compVal = formInputResult.compVal

   const maskedValue = ref('')
   const inputRef = ref(null)
   
   // ... rest of the setup code ...

   return {
-    ...useFormInput(
-      props,
-      context,
-      {
-        formPrefixKey: props.nativeType === "file" ? "file-" : null
-      },
-    ),
+    ...formInputResult,
     onEnterPress,
     onChange,
     handleInput,
     maskedValue,
     effectivePlaceholder,
     inputRef,
     isValidMask,
     displayValue
   }

Also applies to: 194-209

🧰 Tools
🪛 GitHub Check: Run client linters

[failure] 87-87:
'inputWrapperProps' is assigned a value but never used

🤖 Prompt for AI Agents
In client/components/forms/TextInput.vue around lines 85 to 93 and also 194 to
209, there are duplicate calls to the useFormInput composable causing redundant
initialization, and the inputWrapperProps from the first call is unused. Remove
the first useFormInput call entirely and keep only the second call where the
returned variables are actually used, ensuring no unused variables remain.

Comment thread client/components/forms/TextInput.vue Outdated
Copy link
Copy Markdown
Collaborator

@JhumanJ JhumanJ left a comment

Choose a reason for hiding this comment

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

Great but some improvements:

  • In the field options:
  1. only allow the letters a and number 9
  2. let's add support for https://primevue.org/inputmask/#slotchar
  3. let's create a basic help page and add link to it
  4. let's refactor in a popover for cleaner options and better explain (let's wait for the migration to nuxt ui 3 and tailwind 4 beforehands)
  • For textInput: i think we did too many changes which are making the component too complicated. Let's instead revert our changes there, and create a dedicated MaskInput. For the user it's still a text input option but for the code it'll be cleaner.

Also in terms of feature let's improve:

  • show with underscore remaing characters to help the user fill the form
    CleanShot 2025-06-12 at 22 11 28@2x

  • Whenever a character is wrong and not allowed, we should just prevent it, not clear the whole input like it's currently done - not usable this way

Copy link
Copy Markdown
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 comments (1)
client/components/forms/TextInput.vue (1)

64-66: Missing core Vue & composable imports – component will not compile

ref, computed, nextTick, watch, and useInputMask are all referenced but never imported, breaking the build at runtime.

-import {inputProps, useFormInput} from "./useFormInput.js"
-import InputWrapper from "./components/InputWrapper.vue"
+import InputWrapper from "./components/InputWrapper.vue"
+import { inputProps, useFormInput } from "./useFormInput.js"
+
+// 🛠  Required Vue reactivity utilities
+import { ref, computed, nextTick, watch } from "vue"
+
+// 🛠  Composable providing mask logic
+import { useInputMask } from "@/composables/useInputMask"   // adjust path if necessary
♻️ Duplicate comments (1)
client/components/forms/TextInput.vue (1)

85-94: useFormInput is still executed twice – remove the first call

The earlier review already pointed this out. Instantiating useFormInput twice duplicates watchers & emits, wasting resources and risking state divergence.

-  const { compVal } = useFormInput(
-    props,
-    context,
-    {
-      formPrefixKey: props.nativeType === "file" ? "file-" : null
-    },
-  )
+  const formInputResult = useFormInput(
+    props,
+    context,
+    { formPrefixKey: props.nativeType === "file" ? "file-" : null },
+  )
+  const { compVal } = formInputResult
-return {
-  ...useFormInput(
-    props,
-    context,
-    {
-      formPrefixKey: props.nativeType === "file" ? "file-" : null
-    },
-  ),
+return {
+  ...formInputResult,

Also applies to: 194-200

🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)

115-152: Cursor-maintenance logic is brittle and can be simplified

Manually resetting inputRef.value.value inside nextTick may cause flicker and breaks IME composition. Consider letting Vue control the DOM and rely on v-model plus a @input="handleInput" listener:

-      nextTick(() => {
-        if (inputRef.value && inputRef.value.value !== formatted) {
-          const cursorPos = inputRef.value.selectionStart
-          inputRef.value.value = formatted
-          if (inputRef.value.setSelectionRange) {
-            inputRef.value.setSelectionRange(cursorPos, cursorPos)
-          }
-        }
-      })
+      // Emit only the masked value; Vue will update the DOM via v-model.
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 62244f3 and 4d8c0c2.

📒 Files selected for processing (1)
  • client/components/forms/TextInput.vue (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Build the Nuxt app
  • GitHub Check: Run code linters
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
  • GitHub Check: Run client linters

Copy link
Copy Markdown
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 comments (1)
client/components/forms/TextInput.vue (1)

64-67: Import the required utilities and useInputMask to prevent runtime failures
ref, computed, watch, nextTick, and useInputMask are referenced but never imported, which will break both the linter and the runtime bundle.

-import {inputProps, useFormInput} from "./useFormInput.js"
-import InputWrapper from "./components/InputWrapper.vue"
+import { ref, computed, watch, nextTick } from "vue"
+import { inputProps, useFormInput } from "./useFormInput.js"
+import InputWrapper from "./components/InputWrapper.vue"
+import useInputMask from "../../composables/useInputMask.js"
🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)

98-113: Potential cursor-position glitches when using computed setter only
handleInput relies on inputRef.value.selectionStart, but when the setter is triggered via v-model, selectionStart may be null in some browsers (e.g. Safari) because the DOM update hasn’t flushed yet. Consider binding @input="handleInput" directly and passing the real event instead of fabricating one, to guarantee reliable cursor handling.

This is optional but will save edge-case bug reports.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4d8c0c2 and a251e7d.

📒 Files selected for processing (1)
  • client/components/forms/TextInput.vue (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build the Nuxt app
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
  • GitHub Check: Run client linters
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)

Comment thread client/components/forms/TextInput.vue Outdated
Copy link
Copy Markdown
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 comments (1)
client/components/forms/TextInput.vue (1)

64-66: Missing essential imports – component will crash at runtime
ref, computed, watch, nextTick and useInputMask are used but never imported.

-import {inputProps, useFormInput} from "./useFormInput.js"
-import InputWrapper from "./components/InputWrapper.vue"
+import { ref, computed, watch, nextTick } from "vue"
+import { useInputMask } from "@/composables/useInputMask.js"
+import { inputProps, useFormInput } from "./useFormInput.js"
+import InputWrapper from "./components/InputWrapper.vue"
♻️ Duplicate comments (4)
client/composables/useInputMask.js (3)

17-31: optional flag still leaks – breaks all masks after the first ?
Same issue flagged earlier: the optional boolean is set when encountering '?' but never reset, therefore every subsequent token is marked optional and the mask truncates at the first missing char.

-    let optional = false
-
     for (let i = 0; i < maskValue.length; i++) {
       const char = maskValue[i]
       if (char === '?') {
-        optional = true
+        // mark *previous* token optional, don’t affect the rest
+        if (tokens.length > 0) {
+          tokens[tokens.length - 1].optional = true
+        }
         continue
       }
 
       tokens.push({
         char,
         regex: maskTokens[char] || null,
         literal: !maskTokens[char],
-        optional
+        optional: false
       })
     }

52-55: Over-aggressive clean-up regex removes valid alpha-numeric input
/[^\w]/g strips underscores but also removes locale-specific letters and treats _ as word-char. Replace with an explicit alphanumeric whitelist as discussed previously.

-    const cleanValue = value.replace(/[^\w]/g, '')
+    const cleanValue = value.replace(/[^0-9a-zA-Z]/g, '')

79-82: getUnmaskedValue replicates the same regex problem

-  return value.replace(/[^\w]/g, '')
+  return value.replace(/[^0-9a-zA-Z]/g, '')
client/components/forms/TextInput.vue (1)

85-96: Duplicate useFormInput invocation – double watchers & wasted reactivity
The first call is stored in compVal, the second (spread in return) recreates the whole composable. Call it once and reuse the result.

-    const { compVal } = useFormInput(
+    const formInput = useFormInput(
       props,
       context,
       {
         formPrefixKey: props.nativeType === "file" ? "file-" : null
       },
     )
+    const { compVal } = formInput

…and later in return:

-      ...useFormInput(
-        props,
-        context,
-        {
-          formPrefixKey: props.nativeType === "file" ? "file-" : null
-        },
-      ),
+      ...formInput,
🧹 Nitpick comments (1)
client/components/forms/TextInput.vue (1)

135-143: nextTick, selectionStart handling relies on nullable inputRef
Before accessing selectionStart, guard against null and ensure the element is still mounted to prevent TypeErrors during rapid unmount / remount cycles.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a251e7d and 0affd68.

📒 Files selected for processing (2)
  • client/components/forms/TextInput.vue (4 hunks)
  • client/composables/useInputMask.js (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
client/composables/useInputMask.js

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build the Nuxt app

Copy link
Copy Markdown
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

🧹 Nitpick comments (3)
client/components/forms/MaskInput.vue (3)

68-70: Name / file mismatch may confuse consumers

The component is exported as TextInput, yet the file is named MaskInput.vue.
Either rename the file to TextInput.vue or update name: to MaskInput for clarity & easier debugging in devtools.


73-81: Prop typing edge-cases

  • autocomplete accepts Boolean | Object, but <input autocomplete> only expects a string ("on"/"off" or datalist tokens). Passing a boolean/object will render "true"/"[object Object]" in the DOM.
  • pattern & mask defaults are null; Vue will render the literal "null" attribute. Prefer undefined so the attribute is omitted.
-autocomplete: {type: [Boolean, String, Object], default: null},
+autocomplete: { type: String, default: undefined },
 ...
-pattern: { type: String, default: null },
-mask: { type: String, default: null }
+pattern: { type: String, default: undefined },
+mask: { type: String, default: undefined }

34-35: Redundant event prevention

@keydown.enter.prevent already stops the default form submission.
Calling event.preventDefault() again in onEnterPress is unnecessary and slightly misleading—consider removing the method entirely and the corresponding listener, or drop the .prevent modifier.

Also applies to: 202-205

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0affd68 and d79be3f.

📒 Files selected for processing (2)
  • client/components/forms/MaskInput.vue (1 hunks)
  • client/components/open/forms/OpenFormField.vue (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/components/open/forms/OpenFormField.vue
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - mysql)
  • GitHub Check: Run Feature & Unit tests (PHP 8.2 - pgsql)
  • GitHub Check: Build the Nuxt app
🔇 Additional comments (1)
client/components/forms/MaskInput.vue (1)

115-146: Underscore stripping may reject legitimate input

handleInput unconditionally strips _ before formatting. If the mask itself legitimately contains underscores (e.g., part numbers like ABC_DEF), the user can never enter them. Consider making the placeholder character configurable or skipping this replace when the mask includes literal underscores.

Comment on lines +64 to +67
<script>
import {inputProps, useFormInput} from "./useFormInput.js"
import InputWrapper from "./components/InputWrapper.vue"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing essential imports – component will fail to compile

ref, computed, watch, nextTick, and the very useInputMask composable used below are never imported.
Add them (and group with existing imports) or the script will throw at runtime.

-import {inputProps, useFormInput} from "./useFormInput.js"
+import { ref, computed, watch, nextTick } from "vue"
+import { inputProps, useFormInput } from "./useFormInput.js"
+import { useInputMask } from "../composables/useInputMask.js"
📝 Committable suggestion

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

Suggested change
<script>
import {inputProps, useFormInput} from "./useFormInput.js"
import InputWrapper from "./components/InputWrapper.vue"
<script>
import { ref, computed, watch, nextTick } from "vue"
import { inputProps, useFormInput } from "./useFormInput.js"
import { useInputMask } from "../composables/useInputMask.js"
import InputWrapper from "./components/InputWrapper.vue"
🤖 Prompt for AI Agents
In client/components/forms/MaskInput.vue around lines 64 to 67, the script is
missing imports for ref, computed, watch, nextTick, and the useInputMask
composable, which are used later in the code. Add these imports from their
respective sources and group them with the existing imports at the top of the
script to prevent runtime errors and ensure the component compiles correctly.

- Implemented a new method to validate input mask patterns, ensuring only allowed characters (a, 9, *, and common punctuation) are accepted.
- Updated the help text to clarify the format and provide examples for users, improving usability and guidance for input mask patterns.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
client/components/forms/heavy/MaskInput.vue (2)

201-204: Remove redundant return statement.

The return false is redundant after event.preventDefault().

     const onEnterPress = (event) => {
       event.preventDefault()
-      return false
     }

69-69: Remove empty components object.

The empty components object is unnecessary.

-  components: {},
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 703c97d and 5849875.

📒 Files selected for processing (3)
  • client/components/forms/heavy/MaskInput.vue (1 hunks)
  • client/components/open/forms/fields/components/FieldOptions.vue (3 hunks)
  • client/composables/components/useComponentRegistry.js (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • client/composables/components/useComponentRegistry.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/components/open/forms/fields/components/FieldOptions.vue
🧰 Additional context used
📓 Path-based instructions (4)
client/**

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/**/*.vue

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/components/**/*

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/**/*.{js,vue}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Use Laravel's validation features for form and request validation.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Implement proper request validation using Form Requests.
client/components/forms/heavy/MaskInput.vue (3)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Use Nuxt UI components (https://ui.nuxt.com/components) and Tailwind for components and styling.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build the Nuxt app
  • GitHub Check: Run client linters

<input
ref="inputRef"
:id="id ? id : name"
v-model="displayValue"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix input event handling for mask formatting.

The displayValue setter calls handleInput with a synthetic event object, but handleInput is designed to work with native input events. This could cause issues with cursor positioning and formatting.

Replace v-model with explicit value binding and input handler:

-      v-model="displayValue"
+      :value="displayValue"
+      @input="handleInput"

Then update the displayValue setter to handle non-masked inputs only:

       set(newValue) {
-        if (props.mask && isValidMask.value) {
-          handleInput({ target: { value: newValue } })
-        } else {
+        if (!props.mask || !isValidMask.value) {
           compVal.value = newValue
         }
       }

Also applies to: 107-111

🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at line 10 and also lines
107-111, the current use of v-model on displayValue triggers the setter with a
synthetic event, but handleInput expects a native input event, causing cursor
and formatting issues. Replace v-model with explicit :value binding to
displayValue and an @input event listener that calls handleInput directly with
the native event. Then modify the displayValue setter to only update the
internal value for non-masked inputs without calling handleInput, ensuring
proper event handling and cursor behavior.

Comment on lines +64 to +229
<script>
import {inputProps, useFormInput} from "../useFormInput.js"

export default {
name: "MaskInput",
components: {},

props: {
...inputProps,
nativeType: {type: String, default: "text"},
accept: {type: String, default: null},
min: {type: Number, required: false, default: null},
max: {type: Number, required: false, default: null},
autocomplete: {type: [Boolean, String, Object], default: null},
maxCharLimit: {type: Number, required: false, default: null},
pattern: { type: String, default: null },
mask: { type: String, default: null }
},

setup(props, context) {
const { formatValue, isValidMask, getDisplayValue } = useInputMask(() => props.mask)

const { compVal } = useFormInput(
props,
context,
{
formPrefixKey: props.nativeType === "file" ? "file-" : null
},
)

const maskedValue = ref('')
const inputRef = ref(null)

const displayValue = computed({
get() {
if (props.mask && isValidMask.value) {
return getDisplayValue(compVal.value)
} else {
return compVal.value
}
},
set(newValue) {
if (props.mask && isValidMask.value) {
handleInput({ target: { value: newValue } })
} else {
compVal.value = newValue
}
}
})

const handleInput = (event) => {
const inputValue = event.target.value

if (!props.mask || !isValidMask.value) {
// No mask or invalid mask - behave as normal input
compVal.value = inputValue
return
}

// Remove underscores from input value for processing
const cleanInputValue = inputValue.replace(/_/g, '')

// Get the previous formatted value to compare
const previousFormatted = maskedValue.value
const formatted = formatValue(cleanInputValue)

// If the formatted value is the same as before, it means the new character was invalid
// In this case, we should revert to the previous state
if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) {
// Invalid character entered - revert the input
nextTick(() => {
if (inputRef.value) {
const cursorPos = inputRef.value.selectionStart - 1
inputRef.value.value = getDisplayValue(previousFormatted)
// Set cursor position to where it was before the invalid character
if (inputRef.value.setSelectionRange && cursorPos >= 0) {
inputRef.value.setSelectionRange(cursorPos, cursorPos)
}
}
})
return
}

// Valid input - update the values
maskedValue.value = formatted
compVal.value = formatted

// Update input display value
nextTick(() => {
if (inputRef.value) {
const displayValue = getDisplayValue(formatted)
if (inputRef.value.value !== displayValue) {
const cursorPos = inputRef.value.selectionStart
inputRef.value.value = displayValue
// Maintain cursor position logic here
if (inputRef.value.setSelectionRange) {
inputRef.value.setSelectionRange(cursorPos, cursorPos)
}
}
}
})
}

const effectivePlaceholder = computed(() => {
if (props.placeholder) return props.placeholder
if (props.mask && isValidMask.value) return getDisplayValue('')
return null
})

// Watch for mask changes (form editor support)
watch(() => props.mask, (newMask) => {
if (!newMask) {
maskedValue.value = compVal.value || ''
} else if (compVal.value) {
// Reformat existing value with new mask
maskedValue.value = formatValue(compVal.value)
}
})

// Watch for compVal changes from parent
watch(compVal, (newVal) => {
if (props.mask && isValidMask.value && newVal) {
maskedValue.value = formatValue(newVal)
} else {
maskedValue.value = newVal || ''
}
}, { immediate: true })


const onChange = (event) => {
if (props.nativeType !== "file") return

const file = event.target.files[0]

props.form[props.name] = file
}

const onEnterPress = (event) => {
event.preventDefault()
return false
}

return {
...useFormInput(
props,
context,
{
formPrefixKey: props.nativeType === "file" ? "file-" : null
},
),
onEnterPress,
onChange,
handleInput,
maskedValue,
effectivePlaceholder,
inputRef,
isValidMask,
displayValue
}
},
computed: {
charCount() {
return this.compVal ? this.compVal.length : 0
},
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Convert component to use Vue Composition API script setup style.

The component uses Vue Options API, which violates the project's coding guidelines. All Vue components should use the Composition API script setup style.

Refactor the component to use <script setup>:

-<script>
-import {inputProps, useFormInput} from "../useFormInput.js"
-
-export default {
-  name: "MaskInput",
-  components: {},
-
-  props: {
-    ...inputProps,
-    nativeType: {type: String, default: "text"},
-    // ... other props
-  },
-
-  setup(props, context) {
-    // ... setup logic
-  },
-  
-  computed: {
-    charCount() {
-      return this.compVal ? this.compVal.length : 0
-    },
-  },
-}
-</script>
+<script setup>
+import { computed, ref, watch, nextTick } from 'vue'
+import { inputProps, useFormInput } from "../useFormInput.js"
+import { useInputMask } from '@/composables/useInputMask'
+
+const props = defineProps({
+  ...inputProps,
+  nativeType: { type: String, default: "text" },
+  accept: { type: String, default: null },
+  min: { type: Number, required: false, default: null },
+  max: { type: Number, required: false, default: null },
+  autocomplete: { type: [Boolean, String, Object], default: null },
+  maxCharLimit: { type: Number, required: false, default: null },
+  pattern: { type: String, default: null },
+  mask: { type: String, default: null }
+})
+
+// Component logic here...
+const charCount = computed(() => compVal.value ? compVal.value.length : 0)
+</script>

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

🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue from lines 64 to 229, the
component currently uses the Vue Options API, but the project requires using the
Composition API with the <script setup> syntax. Refactor the entire component by
removing the export default object and instead use the <script setup> block.
Move all props definitions to defineProps, replace data and computed properties
with refs and computed functions, and convert methods to regular functions
within the setup scope. Also, import and use composables directly inside the
setup block, and return only what is needed for the template. This will align
the component with the project's Composition API style guidelines.

Comment on lines +193 to +199
const onChange = (event) => {
if (props.nativeType !== "file") return

const file = event.target.files[0]

props.form[props.name] = file
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid direct mutation of props.

Directly mutating props.form[props.name] violates Vue's one-way data flow principle.

Use the form input composable's methods to update the value:

     const onChange = (event) => {
       if (props.nativeType !== "file") return

       const file = event.target.files[0]
-       
-      props.form[props.name] = file
+      compVal.value = file
     }
📝 Committable suggestion

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

Suggested change
const onChange = (event) => {
if (props.nativeType !== "file") return
const file = event.target.files[0]
props.form[props.name] = file
}
const onChange = (event) => {
if (props.nativeType !== "file") return
const file = event.target.files[0]
compVal.value = file
}
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 193 to 199, the code
directly mutates props.form[props.name], which violates Vue's one-way data flow
principle. Instead of assigning the file directly to props.form[props.name], use
the provided form input composable's method to update the value properly,
ensuring the component respects Vue's reactive data flow and avoids direct prop
mutation.

</template>

<script>
import {inputProps, useFormInput} from "../useFormInput.js"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing import for useInputMask composable.

The useInputMask composable is used but not imported.

Add the missing import:

 import {inputProps, useFormInput} from "../useFormInput.js"
+import { useInputMask } from '@/composables/useInputMask'

Also applies to: 84-84

🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at lines 65 and 84, the
useInputMask composable is used but not imported. Add an import statement for
useInputMask from its module at the top of the file alongside the existing
imports to ensure it is properly referenced.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (7)
client/composables/useInputMask.js (3)

13-34: Fix the optional flag handling to match the backend implementation.

The optional flag is never reset, making all tokens after a '?' optional. This should mark only the preceding token as optional.


49-77: Fix the input cleaning regex and apply optional chaining.

The current regex removes all non-word characters, including valid mask literals like parentheses and hyphens. Also, apply the static analysis suggestion for optional chaining.


79-82: Update getUnmaskedValue to correctly extract input characters.

The function should extract only the actual input characters (digits and letters), not remove all non-word characters.

client/components/forms/heavy/MaskInput.vue (4)

64-231: Convert component to use Vue Composition API script setup style.

The component uses Vue Options API, which violates the project's coding guidelines. All Vue components should use the Composition API script setup style.


65-65: Add missing import for useInputMask composable.

The useInputMask composable is used but not imported.


10-10: Fix input event handling for mask formatting.

The displayValue setter calls handleInput with a synthetic event object, but handleInput is designed to work with native input events. This could cause issues with cursor positioning and formatting.


194-200: Avoid direct mutation of props.

Directly mutating props.form[props.name] violates Vue's one-way data flow principle.

🧹 Nitpick comments (1)
client/composables/useInputMask.js (1)

126-126: Apply optional chaining for safer property access.

The static analysis tool suggests using optional chaining for safer access to the regex property.

Apply this diff to use optional chaining:

-      if (token.regex && token.regex.test(cleanValue[valueIndex])) {
+      if (token.regex?.test(cleanValue[valueIndex])) {
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5849875 and f18b914.

📒 Files selected for processing (4)
  • client/components/forms/heavy/MaskInput.vue (1 hunks)
  • client/components/open/forms/OpenFormField.vue (2 hunks)
  • client/components/open/forms/fields/components/FieldOptions.vue (3 hunks)
  • client/composables/useInputMask.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/components/open/forms/OpenFormField.vue
  • client/components/open/forms/fields/components/FieldOptions.vue
🧰 Additional context used
📓 Path-based instructions (5)
client/**

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/**/*.vue

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/components/**/*

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/**/*.{js,vue}

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
client/**/*.js

Instructions used from:

Sources:
📄 CodeRabbit Inference Engine

  • .cursor/rules/front-end.mdc
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Use Laravel's validation features for form and request validation.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/back-end.mdc:0-0
Timestamp: 2025-06-30T15:05:17.582Z
Learning: Applies to api/**/*.php : Implement proper request validation using Form Requests.
client/components/forms/heavy/MaskInput.vue (8)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Use Nuxt UI components (https://ui.nuxt.com/components) and Tailwind for components and styling.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.{js,vue} : Implement proper error boundaries or try-catch mechanisms to handle errors gracefully, especially in asynchronous operations.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Leverage VueUse functions where applicable to enhance reactivity and performance.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Always use the Vue Composition API script setup style.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap asynchronous components in Suspense with a fallback UI made with <USkeleton/> components.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Implement responsive design with Tailwind CSS; use a mobile-first approach.
client/composables/useInputMask.js (3)
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : For forms inputs, use the components in client/components/forms such as <TextInput> etc.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Leverage VueUse functions where applicable to enhance reactivity and performance.
Learnt from: CR
PR: JhumanJ/OpnForm#0
File: .cursor/rules/front-end.mdc:0-0
Timestamp: 2025-06-30T15:07:03.422Z
Learning: Applies to client/**/*.vue : Wrap form inputs in @VForm.vue.
🪛 Biome (1.9.4)
client/composables/useInputMask.js

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 126-126: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (1)
client/composables/useInputMask.js (1)

94-97: No changes needed: Frontend mask regex already matches backend pattern

The regex in client/composables/useInputMask.js (/^[9a*().?\s-]*$/) is functionally equivalent to the backend’s InputMaskRule::validateMaskPattern (/^[9a*().\-\?\s]*$/), allowing the same set of mask characters. You can safely ignore the update suggestion.

Likely an incorrect or invalid review comment.

Comment on lines +39 to +47
const getMaskPlaceholder = computed(() => {
if (!mask.value) return ''

return parsedMask.value.map(token => {
if (token.literal) return token.char
if (token.optional) return ''
return token.char
}).join('')
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the mask placeholder generation logic.

The placeholder function returns the mask characters themselves instead of showing the expected input format. For non-literal tokens, it should return the slot character or nothing based on whether they're optional.

Apply this diff to fix the placeholder logic:

   const getMaskPlaceholder = computed(() => {
     if (!mask.value) return ''

     return parsedMask.value.map(token => {
       if (token.literal) return token.char
       if (token.optional) return ''
-      return token.char
+      return slotChar
     }).join('')
   })
📝 Committable suggestion

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

Suggested change
const getMaskPlaceholder = computed(() => {
if (!mask.value) return ''
return parsedMask.value.map(token => {
if (token.literal) return token.char
if (token.optional) return ''
return token.char
}).join('')
})
const getMaskPlaceholder = computed(() => {
if (!mask.value) return ''
return parsedMask.value.map(token => {
if (token.literal) return token.char
if (token.optional) return ''
return slotChar
}).join('')
})
🤖 Prompt for AI Agents
In client/composables/useInputMask.js around lines 39 to 47, the
getMaskPlaceholder computed property incorrectly returns the mask characters for
non-literal tokens instead of the expected input format. To fix this, update the
logic so that for literal tokens it returns the character as is, for optional
tokens it returns an empty string, and for other tokens it returns the slot
character representing the expected input format rather than the mask character.

Comment on lines +225 to +229
computed: {
charCount() {
return this.compVal ? this.compVal.length : 0
},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove computed property that duplicates setup logic.

The charCount computed property in the Options API section duplicates functionality already available in the setup function and will cause conflicts.

Remove the computed section as it's redundant:

-  computed: {
-    charCount() {
-      return this.compVal ? this.compVal.length : 0
-    },
-  },

The character count should be computed in the setup function instead.

📝 Committable suggestion

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

Suggested change
computed: {
charCount() {
return this.compVal ? this.compVal.length : 0
},
},
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 225 to 229, remove
the computed property charCount from the Options API section because it
duplicates logic already implemented in the setup function. This redundancy can
cause conflicts, so delete the entire computed block containing charCount and
ensure that character count is only computed within the setup function.

Comment on lines +95 to +96
const maskedValue = ref('')
const inputRef = ref(null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing Vue imports for composition functions.

The component uses ref without importing it from Vue.

Add the missing import:

 import {inputProps, useFormInput} from "../useFormInput.js"
+import { ref, computed, watch, nextTick } from 'vue'
📝 Committable suggestion

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

Suggested change
const maskedValue = ref('')
const inputRef = ref(null)
// at the top of client/components/forms/heavy/MaskInput.vue
import {inputProps, useFormInput} from "../useFormInput.js"
+import { ref, computed, watch, nextTick } from 'vue'
// …rest of your imports and <script setup> code…
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue around lines 95 to 96, the ref
function is used without importing it from Vue. Add an import statement at the
top of the script section to import ref from 'vue' to ensure the composition API
function is available.


// If the formatted value is the same as before, it means the new character was invalid
// In this case, we should revert to the previous state
if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix the mask value comparison logic.

The comparison uses previousFormatted.length but should use the unmasked value length for accurate validation.

Apply this diff to fix the comparison:

-      if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) {
+      if (formatted === previousFormatted && cleanInputValue.length > maskedValue.value.replace(/[^0-9a-zA-Z]/g, '').length) {
📝 Committable suggestion

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

Suggested change
if (formatted === previousFormatted && cleanInputValue.length > previousFormatted.length) {
if (formatted === previousFormatted && cleanInputValue.length > maskedValue.value.replace(/[^0-9a-zA-Z]/g, '').length) {
🤖 Prompt for AI Agents
In client/components/forms/heavy/MaskInput.vue at line 133, the condition
incorrectly compares cleanInputValue.length with previousFormatted.length, which
mixes masked and unmasked lengths. Update the comparison to use the length of
the unmasked previous value instead of previousFormatted.length to ensure
accurate validation of input changes.

@chiragchhatrala
Copy link
Copy Markdown
Collaborator Author

Great but some improvements:

  • In the field options:
  1. only allow the letters a and number 9
  2. let's add support for https://primevue.org/inputmask/#slotchar
  3. let's create a basic help page and add link to it
  4. let's refactor in a popover for cleaner options and better explain (let's wait for the migration to nuxt ui 3 and tailwind 4 beforehands)
  • For textInput: i think we did too many changes which are making the component too complicated. Let's instead revert our changes there, and create a dedicated MaskInput. For the user it's still a text input option but for the code it'll be cleaner.

Also in terms of feature let's improve:

  • show with underscore remaing characters to help the user fill the form
    CleanShot 2025-06-12 at 22 11 28@2x
  • Whenever a character is wrong and not allowed, we should just prevent it, not clear the whole input like it's currently done - not usable this way

All Done ✔️

Copy link
Copy Markdown
Collaborator

@JhumanJ JhumanJ left a comment

Choose a reason for hiding this comment

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

Image - let's move this option to advanced settings in the field settings - Regex inconsistency between frontend and backend Backend (UserFormRequest.php): /^[9a*().\\-?\\s]*$/ Frontend (useInputMask.js): /^[9a*().?\s-]*$/ Backend validation (InputMaskRule.php): /^[9a*().\\-?\\s]*$/ The frontend allows ? but the backend regex doesn't match it correctly. Also, the frontend allows - anywhere while the backend escapes it. - Issue: ? sets a flag for the next token, but it should mark the previous token as optional. Current behavior: 9? makes the 9 optional, but ?9 incorrectly makes 9 optional. - Character count includes mask characters

I recommend you also try in cursor expert PR review of @Branch with composer-1 model

chiragchhatrala and others added 3 commits December 9, 2025 14:00
- Updated regex patterns in UserFormRequest and InputMaskRule to improve input mask validation.
- Enhanced MaskInput component to utilize getUnmaskedValue for accurate character counting.
- Refactored useInputMask composable to correctly mark tokens as optional and validate mask patterns.
- Improved FieldOptions component by reintroducing input mask pattern help text and examples.

These changes enhance the functionality and user experience of input masks across the application.
- Improved InputMaskRule to handle optional mask tokens more effectively, allowing for better validation of input values.
- Updated error messages to escape mask patterns for XSS prevention.
- Enhanced MaskInput component to support pasting formatted values and maintain cursor position during input changes.
- Refactored useInputMask composable to streamline mask validation and formatting processes.

These changes improve the robustness and user experience of input masking across the application.
@chiragchhatrala
Copy link
Copy Markdown
Collaborator Author

Done

@chiragchhatrala
Copy link
Copy Markdown
Collaborator Author

@JhumanJ can you please check this

  • Cursor is at then end instead of being at the right position

@JhumanJ JhumanJ added the v2 label Dec 22, 2025
@chiragchhatrala
Copy link
Copy Markdown
Collaborator Author

Created new PR #993

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.