-
Notifications
You must be signed in to change notification settings - Fork 119
Daily #500
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
Daily #500
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR updates Next.js dependencies to 16.1.3 across multiple workspace packages, introduces tag-based customer management with predicate configuration infrastructure, enhances email challenges with dynamic brand data from the www table, extends tag input components with color and autocomplete support, and refactors customer management dialogs to separate create and contact edit workflows. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
…duce CustomerCreateDialog for creating customers with tags, and update CustomerContactsEditDialog for editing contact information. Adjust database types to include tags in customer insert operations.
…handling for missing forms
…s table to utilize this value for invitation tracking
…agInput and Autocomplete components to support optional tint colors. Update UI to reflect changes in tag display and interaction.
…eConfigProvider and enhancing the DataQueryPredicatesMenu. Update customer schema to include tags and improve predicate handling for array types. Add utility functions for Postgres text array literals.
…iple packages, ensuring compatibility and incorporating the latest features and fixes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12aa781887
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const quotedMatches = Array.from( | ||
| inner.matchAll(/"((?:\\.|[^"\\])*)"/g) | ||
| ).map((m) => | ||
| m[1] | ||
| .replace(/\\(["\\])/g, "$1") | ||
| .replace(/\\\\/g, "\\") | ||
| .trim() | ||
| ); | ||
|
|
||
| if (quotedMatches.length > 0) { | ||
| return quotedMatches.filter(Boolean); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve unquoted elements in mixed array literals
The new parsePostgresTextArrayLiteral returns only the quoted matches whenever any quoted element exists, which drops valid unquoted elements in mixed literals (e.g. {"foo bar",baz} becomes ["foo bar"]). Postgres allows mixing quoted/unquoted elements, so a saved predicate value in that format will be truncated when the UI parses it and then rewritten without the missing items, changing the filter results. Consider parsing both quoted and unquoted tokens or only using the quoted-only fast path when all elements are quoted.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/quests-table.tsx (1)
24-32: Alias theInfinityicon import to avoid shadowing the global constant.Line 31 imports
Infinityfromlucide-react, which shadows the global constant and triggers Biome's linting rules. Rename the import toInfinityIconand update its usage at line 264.🔧 Suggested fix
-import { - ChevronDown, - ChevronRight, - MoreHorizontal, - Users, - CheckCircle2, - Clock, - AlertCircle, - Infinity, -} from "lucide-react"; +import { + ChevronDown, + ChevronRight, + MoreHorizontal, + Users, + CheckCircle2, + Clock, + AlertCircle, + Infinity as InfinityIcon, +} from "lucide-react";- <Infinity className="size-4 text-muted-foreground" /> + <InfinityIcon className="size-4 text-muted-foreground" />editor/theme/templates/enterprise/west-referral/invitation/page.tsx (1)
399-414: Destructure and handle the error state fromuseFormSessionto avoid infinite skeleton on request failure.The hook returns an
errorfield (via SWR) that should be checked alongsideisLoadinganddata. Without it, a failed fetch leavesdataundefined whileerroris set, causing the skeleton to persist indefinitely. Other usages in the codebase (e.g.,row-edit-panel.tsxline 191) destructure this field—apply the same pattern here to surface error UI or a retry mechanism.editor/scaffolds/platform/customer/use-customer-feed.ts (1)
97-101: Silent error handling may mask fetch failures.The callback now destructures only
{ data }, ignoring anyerror. While this simplifies the code, fetch failures will silently leavecustomersas an empty array with no user feedback. Consider at minimum logging the error:- fetchCustomers(client, project_id, query).then(({ data }) => { + fetchCustomers(client, project_id, query).then(({ data, error }) => { + if (error) console.error("Failed to fetch customers:", error); if (data) { setCustomers(data); } });Or propagate via an optional error callback if consistent with the hook's design philosophy.
🤖 Fix all issues with AI agents
In `@editor/app/`(dev)/ui/components/tags/_page.tsx:
- Around line 76-81: The demos pass a constant activeTagIndex and a no-op setter
to TagInput which disables keyboard navigation; replace those with local state
using useState<number | null> (e.g., const [activeTagIndex, setActiveTagIndex] =
useState<number | null>(null)) inside each demo component and pass both
activeTagIndex and setActiveTagIndex into TagInput (replace the hardcoded
activeTagIndex={null} and setActiveTagIndex={() => {}}). Apply the same change
for the other demo instances that render TagInput (the blocks around the other
mentioned ranges) so arrow keys and Delete/Backspace work as intended.
- Around line 69-73: Update the demo tip text inside the notes prop JSX in the
tags page component: replace the current sentence "Click on a tag to remove it."
with wording that describes the actual interaction (e.g., "Click the close (×)
button on a tag to remove it."), keeping the surrounding JSX structure (the
<strong>Tips:</strong> fragment) intact so only the copy changes in the notes
prop of the component in _page.tsx.
In
`@editor/app/`(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/quests-table.tsx:
- Line 90: The progress and status calculations use max_invitations_per_referrer
and can divide by zero or produce >100 progress; update all calculations in
quests-table.tsx that reference max_invitations_per_referrer and
invitation_count to first guard against zero (treat 0 as a cap of 0 and avoid
division), compute progress as a clamped value (e.g., progress = Math.min(100,
Math.max(0, (invitation_count / max_invitations_per_referrer) * 100)) or 0 when
cap is 0), and change status checks to use >= (e.g., if invitation_count >=
max_invitations_per_referrer) so status moves out of "active" when at or over
the cap; apply the same fixes to the other occurrences of these calculations in
this file.
In `@editor/components/tag/tag-input.tsx`:
- Around line 23-29: The truncate logic in tag-input.tsx rebuilds tags using
only { id, text } which drops optional Tag.color (and any other properties);
update the truncate implementation to spread the original tag (e.g., ...tag) and
only override text (e.g., { ...tag, text: truncatedText }) so color is
preserved; locate the truncate function/logic and any place where new tag
objects are created (references to Tag and truncate) and replace the rebuild
with a spread-based construction to retain color and other fields.
In `@editor/lib/supabase-postgrest/builder.ts`:
- Around line 21-48: The parsePostgresTextArrayLiteral function incorrectly
discards unquoted elements whenever any quoted values exist; replace the
regex-only approach with a simple state-machine parser inside
parsePostgresTextArrayLiteral that iterates the inner text (from v.slice(1,-1)),
accumulates characters into a current token, toggles an inQuotes flag on
encountering unescaped double-quotes, handles backslash escapes (e.g., \" and
\\) inside quoted tokens, treats commas outside quotes as element separators,
trims tokens and ignores empty ones, and returns the full list preserving both
quoted and unquoted items so calls from components like predicate.tsx get
correct mixed-element arrays.
In `@editor/theme/templates/enterprise/west-referral/invitation/page.tsx`:
- Around line 113-116: The early return "if (!visible) return null;" is placed
before hooks are invoked which violates the rules-of-hooks; move this visibility
check so that all React hooks in this component (any
useState/useEffect/useMemo/useRef calls used in page.tsx) are called
unconditionally first, then perform "if (!visible) return null;" (or
alternatively create a small wrapper component that returns null when visible is
false and renders the current component inside it), ensuring symbols like
visible, dictionary, locale, t, and data remain unchanged.
- Around line 306-338: The onSubmit handler currently calls
submitFormToDefaultEndpoint and client.claim without error handling; wrap the
body of onSubmit in a try-catch so any exceptions from
submitFormToDefaultEndpoint or client.claim are caught, call
toast.error(copy.event_signup_fail) (and optionally include error.message)
inside the catch to surface the failure, and return/exit so success handling
(toast.success and onClaimed) only runs on successful completion; ensure you
keep the existing checks for customer_id and client inside the try block and
re-use the same identifiers (onSubmit, submitFormToDefaultEndpoint,
client.claim, toast.error, toast.success, onClaimed).
🧹 Nitpick comments (3)
editor/app/(api)/(public)/v1/session/[session]/field/[field]/challenge/email/start/route.ts (1)
120-128: Consider handling the query error separately from missing data.The formDoc null check is a good addition. However, the query error is not destructured or checked—if the database fails,
formDocwill be null and return 404, masking a potential 500-level server error.Suggested improvement
- const { data: formDoc } = await service_role.forms + const { data: formDoc, error: formDocError } = await service_role.forms .from("form_document") .select("lang") .eq("form_id", ctx.form.id) .single(); + if (formDocError) { + console.error("[forms][challenge_email]/error loading form_document", formDocError); + return NextResponse.json({ error: "internal error" }, { status: 500 }); + } + if (!formDoc) { return NextResponse.json({ error: "form not found" }, { status: 404 }); }editor/lib/platform/index.ts (1)
519-523: Consider addingitemsmetadata for array type consistency.The
tagsproperty inpropertiesusestype: "array"but lacksitemsspecification (scalar type info), unlike the more detailed TABLE definition. For consistency with CSV validation logic that checksspec.items?.format, consider:tags: { type: "array", required: true, + items: { type: "string" }, },This aligns with the
validate_rowfunction that inspectsspec.itemsfor array validation (lines 130-134).editor/scaffolds/platform/customer/customer-edit-dialog.tsx (1)
143-144: Type assertion on PhoneInput value obscures type mismatch.The
value={field.value as any}cast indicates a type incompatibility betweenreact-hook-form's field value type andPhoneInput's expected prop type. Consider typing the form field correctly or investigating ifPhoneInputexpectsstring | undefinedvsstring | null.
| <TagInput | ||
| tags={tags} | ||
| setTags={setTags} | ||
| activeTagIndex={null} | ||
| setActiveTagIndex={() => {}} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wire activeTagIndex state so keyboard navigation works in the demos.
With activeTagIndex={null} and a no‑op setter, arrow navigation and Delete/Backspace removal won’t work, which undercuts the “keyboard support” demo. Consider local state per demo instance.
🛠️ Suggested wiring
-import React, { useState } from "react";
+import React, { useState } from "react";
export default function TagsPage() {
+ const [basicActiveIndex, setBasicActiveIndex] = useState<number | null>(null);
+ const [autoActiveIndex, setAutoActiveIndex] = useState<number | null>(null);
+ const [colorActiveIndex, setColorActiveIndex] = useState<number | null>(null);
+ const [emptyActiveIndex, setEmptyActiveIndex] = useState<number | null>(null);
const [tags, setTags] = useState<{ id: string; text: string }[]>([
{ id: "react", text: "react" },
{ id: "typescript", text: "typescript" },
]);
...
<TagInput
tags={tags}
setTags={setTags}
- activeTagIndex={null}
- setActiveTagIndex={() => {}}
+ activeTagIndex={basicActiveIndex}
+ setActiveTagIndex={setBasicActiveIndex}
/>
...
<TagInput
tags={tagsWithAutocomplete}
setTags={setTagsWithAutocomplete}
enableAutocomplete
autocompleteOptions={autocompleteOptions}
- activeTagIndex={null}
- setActiveTagIndex={() => {}}
+ activeTagIndex={autoActiveIndex}
+ setActiveTagIndex={setAutoActiveIndex}
/>
...
<TagInput
tags={coloredTags}
setTags={setColoredTags}
enableAutocomplete
autocompleteOptions={coloredAutocompleteOptions}
- activeTagIndex={null}
- setActiveTagIndex={() => {}}
+ activeTagIndex={colorActiveIndex}
+ setActiveTagIndex={setColorActiveIndex}
/>
...
<TagInput
tags={emptyTags}
setTags={setEmptyTags}
- activeTagIndex={null}
- setActiveTagIndex={() => {}}
+ activeTagIndex={emptyActiveIndex}
+ setActiveTagIndex={setEmptyActiveIndex}
/>Also applies to: 102-109, 130-137, 151-156
🤖 Prompt for AI Agents
In `@editor/app/`(dev)/ui/components/tags/_page.tsx around lines 76 - 81, The
demos pass a constant activeTagIndex and a no-op setter to TagInput which
disables keyboard navigation; replace those with local state using
useState<number | null> (e.g., const [activeTagIndex, setActiveTagIndex] =
useState<number | null>(null)) inside each demo component and pass both
activeTagIndex and setActiveTagIndex into TagInput (replace the hardcoded
activeTagIndex={null} and setActiveTagIndex={() => {}}). Apply the same change
for the other demo instances that render TagInput (the blocks around the other
mentioned ranges) so arrow keys and Delete/Backspace work as intended.
|
|
||
| // FIXME: | ||
| const max_invitations_per_referrer = 10; | ||
| const max_invitations_per_referrer = campaign.max_invitations_per_referrer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard progress/status calculations for zero or over-limit caps.
If max_invitations_per_referrer is 0, the division yields Infinity/NaN; and if invitation_count exceeds the cap, the progress can exceed 100 while status stays “active.” Consider clamping and using >=.
✅ Suggested fix
{max_invitations_per_referrer !== null ? (
<div className="flex flex-col gap-1">
<div className="text-xs text-muted-foreground">
- {Math.round(
- (quest.invitation_count /
- max_invitations_per_referrer) *
- 100
- )}
+ {max_invitations_per_referrer > 0
+ ? Math.min(
+ 100,
+ Math.round(
+ (quest.invitation_count /
+ max_invitations_per_referrer) *
+ 100
+ )
+ )
+ : 0}
%
</div>
<Progress
value={
- (quest.invitation_count /
- max_invitations_per_referrer) *
- 100
+ max_invitations_per_referrer > 0
+ ? Math.min(
+ 100,
+ (quest.invitation_count /
+ max_invitations_per_referrer) *
+ 100
+ )
+ : 0
}
className="h-2"
/>
</div>
) : (
@@
{getStatusBadge(
max_invitations_per_referrer !== null &&
- quest.invitation_count === max_invitations_per_referrer
+ quest.invitation_count >= max_invitations_per_referrer
? "completed"
: "active"
)}Also applies to: 231-249, 270-275
🤖 Prompt for AI Agents
In
`@editor/app/`(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/quests-table.tsx
at line 90, The progress and status calculations use
max_invitations_per_referrer and can divide by zero or produce >100 progress;
update all calculations in quests-table.tsx that reference
max_invitations_per_referrer and invitation_count to first guard against zero
(treat 0 as a cap of 0 and avoid division), compute progress as a clamped value
(e.g., progress = Math.min(100, Math.max(0, (invitation_count /
max_invitations_per_referrer) * 100)) or 0 when cap is 0), and change status
checks to use >= (e.g., if invitation_count >= max_invitations_per_referrer) so
status moves out of "active" when at or over the cap; apply the same fixes to
the other occurrences of these calculations in this file.
| export type Tag = { | ||
| id: string; | ||
| text: string; | ||
| /** | ||
| * Optional tint color (e.g. hex string) for the tag. | ||
| */ | ||
| color?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve tag colors when truncating tags.
truncate rebuilds tags with only { id, text }, so any color is lost when truncation is enabled. Consider spreading the original tag and only overriding text.
🩹 Proposed fix
- const truncatedTags = truncate
- ? tags.map((tag) => ({
- id: tag.id,
- text:
- tag.text?.length > truncate
- ? `${tag.text.substring(0, truncate)}...`
- : tag.text,
- }))
- : displayedTags;
+ const truncatedTags = truncate
+ ? tags.map((tag) => ({
+ ...tag,
+ text:
+ tag.text?.length > truncate
+ ? `${tag.text.substring(0, truncate)}...`
+ : tag.text,
+ }))
+ : displayedTags;Also applies to: 416-424
🤖 Prompt for AI Agents
In `@editor/components/tag/tag-input.tsx` around lines 23 - 29, The truncate logic
in tag-input.tsx rebuilds tags using only { id, text } which drops optional
Tag.color (and any other properties); update the truncate implementation to
spread the original tag (e.g., ...tag) and only override text (e.g., { ...tag,
text: truncatedText }) so color is preserved; locate the truncate function/logic
and any place where new tag objects are created (references to Tag and truncate)
and replace the rebuild with a spread-based construction to retain color and
other fields.
| export function parsePostgresTextArrayLiteral(value: unknown): string[] { | ||
| if (typeof value !== "string") return []; | ||
| const v = value.trim(); | ||
| if (!v.startsWith("{") || !v.endsWith("}")) { | ||
| return v ? [v] : []; | ||
| } | ||
|
|
||
| const inner = v.slice(1, -1).trim(); | ||
| if (!inner) return []; | ||
|
|
||
| const quotedMatches = Array.from( | ||
| inner.matchAll(/"((?:\\.|[^"\\])*)"/g) | ||
| ).map((m) => | ||
| m[1] | ||
| .replace(/\\(["\\])/g, "$1") | ||
| .replace(/\\\\/g, "\\") | ||
| .trim() | ||
| ); | ||
|
|
||
| if (quotedMatches.length > 0) { | ||
| return quotedMatches.filter(Boolean); | ||
| } | ||
|
|
||
| return inner | ||
| .split(",") | ||
| .map((s) => s.trim()) | ||
| .filter(Boolean); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and examine the function
fd -p "builder.ts" editor/lib/supabase-postgrest/Repository: gridaco/grida
Length of output: 97
🏁 Script executed:
# Read the file and examine the function around lines 21-48
cat -n editor/lib/supabase-postgrest/builder.ts | sed -n '1,60p'Repository: gridaco/grida
Length of output: 2199
🏁 Script executed:
# Let's trace through the current implementation with a test case
python3 << 'EOF'
import re
def parsePostgresTextArrayLiteral_current(value):
"""Current implementation from the code"""
if not isinstance(value, str):
return []
v = value.strip()
if not v.startswith("{") or not v.endswith("}"):
return [v] if v else []
inner = v[1:-1].strip()
if not inner:
return []
# Find quoted matches
quoted_matches = []
for m in re.finditer(r'"((?:\\.|[^"\\])*)"', inner):
s = m.group(1)
s = re.sub(r'\\(["\\])', r'\1', s)
s = re.sub(r'\\\\', r'\\', s)
s = s.strip()
quoted_matches.append(s)
if len(quoted_matches) > 0:
return [x for x in quoted_matches if x]
return [s.strip() for s in inner.split(",") if s.strip()]
# Test cases
test_cases = [
'{a,"b"}', # Mixed quoted/unquoted - expects ["a", "b"], actual?
'{"a","b"}', # All quoted
'{a,b,c}', # All unquoted
'{"quoted","value"}', # All quoted
'{unquoted}', # Single unquoted
]
print("Testing current implementation:")
for test in test_cases:
result = parsePostgresTextArrayLiteral_current(test)
print(f" {test:20} => {result}")
EOFRepository: gridaco/grida
Length of output: 143
🏁 Script executed:
# Test the bug manually with node
node << 'EOF'
function parsePostgresTextArrayLiteral_current(value) {
if (typeof value !== "string") return [];
const v = value.trim();
if (!v.startsWith("{") || !v.endsWith("}")) {
return v ? [v] : [];
}
const inner = v.slice(1, -1).trim();
if (!inner) return [];
const quotedMatches = Array.from(
inner.matchAll(/"((?:\\.|[^"\\])*)"/g)
).map((m) =>
m[1]
.replace(/\\(["\\])/g, "$1")
.replace(/\\\\/g, "\\")
.trim()
);
if (quotedMatches.length > 0) {
return quotedMatches.filter(Boolean);
}
return inner
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
// Test cases
console.log("Current implementation:");
console.log(' {a,"b"}:', JSON.stringify(parsePostgresTextArrayLiteral_current('{a,"b"}')));
console.log(' {"a","b"}:', JSON.stringify(parsePostgresTextArrayLiteral_current('{"a","b"}')));
console.log(' {a,b,c}:', JSON.stringify(parsePostgresTextArrayLiteral_current('{a,b,c}')));
console.log(' {"a\\"b"}:', JSON.stringify(parsePostgresTextArrayLiteral_current('{"a\\"b"}')));
EOFRepository: gridaco/grida
Length of output: 166
🏁 Script executed:
# Now test the proposed fix
node << 'EOF'
function parsePostgresTextArrayLiteral_proposed(value) {
if (Array.isArray(value)) return value.map((v) => String(v));
if (typeof value !== "string") return [];
const v = value.trim();
if (!v.startsWith("{") || !v.endsWith("}")) {
return v ? [v] : [];
}
const inner = v.slice(1, -1);
if (!inner) return [];
const result = [];
let buf = "";
let inQuotes = false;
let escape = false;
for (let i = 0; i < inner.length; i++) {
const ch = inner[i];
if (escape) {
buf += ch;
escape = false;
continue;
}
if (ch === "\\") {
escape = true;
continue;
}
if (ch === '"') {
inQuotes = !inQuotes;
continue;
}
if (ch === "," && !inQuotes) {
result.push(buf.trim());
buf = "";
continue;
}
buf += ch;
}
result.push(buf.trim());
return result;
}
// Test cases
console.log("Proposed fix:");
console.log(' {a,"b"}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{a,"b"}')));
console.log(' {"a","b"}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{"a","b"}')));
console.log(' {a,b,c}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{a,b,c}')));
console.log(' {"a\\"b"}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{"a\\"b"}')));
console.log(' {}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{}')));
console.log(' {a}:', JSON.stringify(parsePostgresTextArrayLiteral_proposed('{a}')));
EOFRepository: gridaco/grida
Length of output: 182
🏁 Script executed:
# Check if there are any tests for this function
fd -p "test|spec" editor/lib/supabase-postgrest/ -o grep -l "parsePostgresTextArrayLiteral"Repository: gridaco/grida
Length of output: 184
🏁 Script executed:
# Search for usages of this function to understand impact
rg "parsePostgresTextArrayLiteral" --type ts --type tsxRepository: gridaco/grida
Length of output: 84
🏁 Script executed:
# Search for usages of this function
rg "parsePostgresTextArrayLiteral" --type tsRepository: gridaco/grida
Length of output: 814
🏁 Script executed:
# Also search in tsx files
rg "parsePostgresTextArrayLiteral"Repository: gridaco/grida
Length of output: 814
🏁 Script executed:
# Check for test files
fd "\.test\.|\.spec\." editor/lib/supabase-postgrest/Repository: gridaco/grida
Length of output: 100
🏁 Script executed:
# Read the test file
cat -n editor/lib/supabase-postgrest/parse.test.tsRepository: gridaco/grida
Length of output: 2100
🏁 Script executed:
# Also examine the predicate.tsx file to understand the usage context
cat -n editor/scaffolds/grid-editor/components/query/predicate.tsx | head -100Repository: gridaco/grida
Length of output: 3555
Fix array literal parsing for mixed quoted/unquoted elements.
The parser has a critical bug: when any quoted values exist, it returns only quoted matches, dropping all unquoted elements. For example, {a,"b"} returns ["b"] and silently loses a. This corrupts predicate values in query filters. The function is actively used in editor/scaffolds/grid-editor/components/query/predicate.tsx for building query conditions.
Replace the regex-based approach with a state machine that properly handles mixed quoted/unquoted arrays:
🛠️ Proposed fix (state-machine parser that keeps mixed items)
- export function parsePostgresTextArrayLiteral(value: unknown): string[] {
- if (typeof value !== "string") return [];
- const v = value.trim();
- if (!v.startsWith("{") || !v.endsWith("}")) {
- return v ? [v] : [];
- }
-
- const inner = v.slice(1, -1).trim();
- if (!inner) return [];
-
- const quotedMatches = Array.from(
- inner.matchAll(/"((?:\\.|[^"\\])*)"/g)
- ).map((m) =>
- m[1]
- .replace(/\\(["\\])/g, "$1")
- .replace(/\\\\/g, "\\")
- .trim()
- );
-
- if (quotedMatches.length > 0) {
- return quotedMatches.filter(Boolean);
- }
-
- return inner
- .split(",")
- .map((s) => s.trim())
- .filter(Boolean);
- }
+ export function parsePostgresTextArrayLiteral(value: unknown): string[] {
+ if (Array.isArray(value)) return value.map((v) => String(v));
+ if (typeof value !== "string") return [];
+ const v = value.trim();
+ if (!v.startsWith("{") || !v.endsWith("}")) {
+ return v ? [v] : [];
+ }
+
+ const inner = v.slice(1, -1);
+ if (!inner) return [];
+
+ const result: string[] = [];
+ let buf = "";
+ let inQuotes = false;
+ let escape = false;
+
+ for (let i = 0; i < inner.length; i++) {
+ const ch = inner[i];
+ if (escape) {
+ buf += ch;
+ escape = false;
+ continue;
+ }
+ if (ch === "\\") {
+ escape = true;
+ continue;
+ }
+ if (ch === '"') {
+ inQuotes = !inQuotes;
+ continue;
+ }
+ if (ch === "," && !inQuotes) {
+ result.push(buf.trim());
+ buf = "";
+ continue;
+ }
+ buf += ch;
+ }
+ result.push(buf.trim());
+
+ return result;
+ }🤖 Prompt for AI Agents
In `@editor/lib/supabase-postgrest/builder.ts` around lines 21 - 48, The
parsePostgresTextArrayLiteral function incorrectly discards unquoted elements
whenever any quoted values exist; replace the regex-only approach with a simple
state-machine parser inside parsePostgresTextArrayLiteral that iterates the
inner text (from v.slice(1,-1)), accumulates characters into a current token,
toggles an inQuotes flag on encountering unescaped double-quotes, handles
backslash escapes (e.g., \" and \\) inside quoted tokens, treats commas outside
quotes as element separators, trims tokens and ignores empty ones, and returns
the full list preserving both quoted and unquoted items so calls from components
like predicate.tsx get correct mixed-element arrays.
| if (!visible) return null; | ||
|
|
||
| const t = dictionary[locale]; | ||
| const { code, campaign, referrer_name: _referrer_name, is_claimed } = data; | ||
| const { code, referrer_name: _referrer_name, is_claimed } = data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n editor/theme/templates/enterprise/west-referral/invitation/page.tsx | sed -n '100,140p'Repository: gridaco/grida
Length of output: 1401
Avoid returning before hooks (rules-of-hooks).
Returning null before hook calls makes hook order conditional when visible changes. Move the early return below all hooks or split into a wrapper component.
🐛 Proposed fix
export default function InvitationPageTemplate({
data,
visible = true,
design,
locale,
client,
}: {
visible?: boolean;
design: Props;
locale: keyof typeof dictionary;
data: Platform.WEST.Referral.InvitationPublicRead & {
signup_form_id: string;
};
client?: Platform.WEST.Referral.WestReferralClient;
}) {
- if (!visible) return null;
-
const t = dictionary[locale];
const { code, referrer_name: _referrer_name, is_claimed } = data;
const referrer_name = _referrer_name || t.an_anonymous;
const router = useRouter();
const [claimed, setClaimed] = useState(is_claimed);
+
+ if (!visible) 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.
| if (!visible) return null; | |
| const t = dictionary[locale]; | |
| const { code, campaign, referrer_name: _referrer_name, is_claimed } = data; | |
| const { code, referrer_name: _referrer_name, is_claimed } = data; | |
| const t = dictionary[locale]; | |
| const { code, referrer_name: _referrer_name, is_claimed } = data; | |
| const referrer_name = _referrer_name || t.an_anonymous; | |
| const router = useRouter(); | |
| const [claimed, setClaimed] = useState(is_claimed); | |
| if (!visible) return null; |
🤖 Prompt for AI Agents
In `@editor/theme/templates/enterprise/west-referral/invitation/page.tsx` around
lines 113 - 116, The early return "if (!visible) return null;" is placed before
hooks are invoked which violates the rules-of-hooks; move this visibility check
so that all React hooks in this component (any useState/useEffect/useMemo/useRef
calls used in page.tsx) are called unconditionally first, then perform "if
(!visible) return null;" (or alternatively create a small wrapper component that
returns null when visible is false and renders the current component inside it),
ensuring symbols like visible, dictionary, locale, t, and data remain unchanged.
| const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
| e.preventDefault(); | ||
|
|
||
| const formdata = new FormData(e.target as HTMLFormElement); | ||
| const submit_json = await submitFormToDefaultEndpoint<{ | ||
| customer_id?: string | null; | ||
| }>(form_id, formdata); | ||
|
|
||
| const customer_id = | ||
| submit_json?.data?.customer_id && | ||
| typeof submit_json.data.customer_id === "string" | ||
| ? submit_json.data.customer_id | ||
| : null; | ||
|
|
||
| if (!customer_id) { | ||
| toast.error(copy.event_signup_fail); | ||
| return; | ||
| } | ||
|
|
||
| if (!client) { | ||
| toast.error(copy.event_signup_fail); | ||
| return; | ||
| } | ||
|
|
||
| const ok = await client.claim(invitation_code, customer_id); | ||
| if (!ok) { | ||
| toast.error(copy.event_signup_fail); | ||
| return; | ||
| } | ||
|
|
||
| toast.success(copy.event_signup_success); | ||
| onClaimed?.(); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and read the specified lines
wc -l editor/theme/templates/enterprise/west-referral/invitation/page.tsxRepository: gridaco/grida
Length of output: 128
🏁 Script executed:
# Read the actual code at lines 306-338
sed -n '306,338p' editor/theme/templates/enterprise/west-referral/invitation/page.tsxRepository: gridaco/grida
Length of output: 896
🏁 Script executed:
# Search for submitFormToDefaultEndpoint definition/import
rg "submitFormToDefaultEndpoint" --type ts --type tsx -B 2 -A 5Repository: gridaco/grida
Length of output: 84
🏁 Script executed:
# Search for client.claim to understand its signature
rg "\.claim\(" --type ts --type tsx -B 2 -A 5Repository: gridaco/grida
Length of output: 84
🏁 Script executed:
# Search for submitFormToDefaultEndpoint definition across the repo
rg "submitFormToDefaultEndpoint" -t js -t ts --max-count 20Repository: gridaco/grida
Length of output: 552
🏁 Script executed:
# Search for the client.claim method
rg "claim\(" --max-count 20 -A 3Repository: gridaco/grida
Length of output: 2141
🏁 Script executed:
# Check imports in the page.tsx file to understand where submitFormToDefaultEndpoint comes from
head -50 editor/theme/templates/enterprise/west-referral/invitation/page.tsxRepository: gridaco/grida
Length of output: 1672
🏁 Script executed:
# Read the submitFormToDefaultEndpoint definition
cat -n editor/grida-forms-hosted/internal-sdk/submit.ts | head -50Repository: gridaco/grida
Length of output: 1734
🏁 Script executed:
# Read the client.claim implementation from platform/index.ts
rg "claim\(code:" editor/lib/platform/index.ts -A 15Repository: gridaco/grida
Length of output: 629
🏁 Script executed:
# Check the rest of submitFormToDefaultEndpoint to see if it has error handling
cat -n editor/grida-forms-hosted/internal-sdk/submit.tsRepository: gridaco/grida
Length of output: 3049
🏁 Script executed:
# Search for similar async/await patterns in tsx files to see if try-catch is the standard pattern
rg "await.*submitFormToDefaultEndpoint|await.*\.claim\(" --max-count 10 -A 5 -B 2Repository: gridaco/grida
Length of output: 2143
🏁 Script executed:
# Check for other form submission handlers in similar files
rg "const onSubmit.*FormEvent" --max-count 5 -A 10Repository: gridaco/grida
Length of output: 2308
🏁 Script executed:
# Check if there are any other similar patterns that wrap async calls in try-catch for comparison
rg "try\s*{" --max-count 5 -A 10 editor/theme/templates/enterprise/west-referral/Repository: gridaco/grida
Length of output: 134
🏁 Script executed:
# Check for try-catch patterns in similar files
rg "try {" -A 5 editor/theme/templates/enterprise/west-referral/ | head -30Repository: gridaco/grida
Length of output: 130
🏁 Script executed:
# Check for try-catch patterns using a simpler search
rg "try\|catch" -A 3 editor/theme/templates/enterprise/west-referral/ | head -40Repository: gridaco/grida
Length of output: 39
🏁 Script executed:
# Alternative: search for async handlers with error handling in similar contexts
rg "onSuccess|onError" editor/theme/templates/enterprise/west-referral/ -A 2 -B 2Repository: gridaco/grida
Length of output: 39
Wrap async operations in try-catch to handle promise rejections and prevent unhandled errors.
The submitFormToDefaultEndpoint call (which uses fetch() and res.json()) and client.claim (which uses fetch()) can throw exceptions from network errors or parsing failures. Without a try-catch wrapper, these exceptions will propagate unhandled and leave the UI stuck without user feedback. Catch and surface the error through the existing toast error flow.
Proposed fix
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
-
- const formdata = new FormData(e.target as HTMLFormElement);
- const submit_json = await submitFormToDefaultEndpoint<{
- customer_id?: string | null;
- }>(form_id, formdata);
-
- const customer_id =
- submit_json?.data?.customer_id &&
- typeof submit_json.data.customer_id === "string"
- ? submit_json.data.customer_id
- : null;
-
- if (!customer_id) {
- toast.error(copy.event_signup_fail);
- return;
- }
-
- if (!client) {
- toast.error(copy.event_signup_fail);
- return;
- }
-
- const ok = await client.claim(invitation_code, customer_id);
- if (!ok) {
- toast.error(copy.event_signup_fail);
- return;
- }
-
- toast.success(copy.event_signup_success);
- onClaimed?.();
+ try {
+ const formdata = new FormData(e.target as HTMLFormElement);
+ const submit_json = await submitFormToDefaultEndpoint<{
+ customer_id?: string | null;
+ }>(form_id, formdata);
+
+ const customer_id =
+ submit_json?.data?.customer_id &&
+ typeof submit_json.data.customer_id === "string"
+ ? submit_json.data.customer_id
+ : null;
+
+ if (!customer_id || !client) {
+ toast.error(copy.event_signup_fail);
+ return;
+ }
+
+ const ok = await client.claim(invitation_code, customer_id);
+ if (!ok) {
+ toast.error(copy.event_signup_fail);
+ return;
+ }
+
+ toast.success(copy.event_signup_success);
+ onClaimed?.();
+ } catch {
+ toast.error(copy.event_signup_fail);
+ }
};📝 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 onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | |
| e.preventDefault(); | |
| const formdata = new FormData(e.target as HTMLFormElement); | |
| const submit_json = await submitFormToDefaultEndpoint<{ | |
| customer_id?: string | null; | |
| }>(form_id, formdata); | |
| const customer_id = | |
| submit_json?.data?.customer_id && | |
| typeof submit_json.data.customer_id === "string" | |
| ? submit_json.data.customer_id | |
| : null; | |
| if (!customer_id) { | |
| toast.error(copy.event_signup_fail); | |
| return; | |
| } | |
| if (!client) { | |
| toast.error(copy.event_signup_fail); | |
| return; | |
| } | |
| const ok = await client.claim(invitation_code, customer_id); | |
| if (!ok) { | |
| toast.error(copy.event_signup_fail); | |
| return; | |
| } | |
| toast.success(copy.event_signup_success); | |
| onClaimed?.(); | |
| }; | |
| const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | |
| e.preventDefault(); | |
| try { | |
| const formdata = new FormData(e.target as HTMLFormElement); | |
| const submit_json = await submitFormToDefaultEndpoint<{ | |
| customer_id?: string | null; | |
| }>(form_id, formdata); | |
| const customer_id = | |
| submit_json?.data?.customer_id && | |
| typeof submit_json.data.customer_id === "string" | |
| ? submit_json.data.customer_id | |
| : null; | |
| if (!customer_id || !client) { | |
| toast.error(copy.event_signup_fail); | |
| return; | |
| } | |
| const ok = await client.claim(invitation_code, customer_id); | |
| if (!ok) { | |
| toast.error(copy.event_signup_fail); | |
| return; | |
| } | |
| toast.success(copy.event_signup_success); | |
| onClaimed?.(); | |
| } catch { | |
| toast.error(copy.event_signup_fail); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@editor/theme/templates/enterprise/west-referral/invitation/page.tsx` around
lines 306 - 338, The onSubmit handler currently calls
submitFormToDefaultEndpoint and client.claim without error handling; wrap the
body of onSubmit in a try-catch so any exceptions from
submitFormToDefaultEndpoint or client.claim are caught, call
toast.error(copy.event_signup_fail) (and optionally include error.message)
inside the catch to surface the failure, and return/exit so success handling
(toast.success and onClaimed) only runs on successful completion; ensure you
keep the existing checks for customer_id and client inside the try block and
re-use the same identifiers (onSubmit, submitFormToDefaultEndpoint,
client.claim, toast.error, toast.success, onClaimed).
New Features
Improvements
Updates