Skip to content

Conversation

@softmarshmallow
Copy link
Member

@softmarshmallow softmarshmallow commented Jan 18, 2026

  • New Features

    • Added tag system for customers with color support and autocomplete suggestions
    • Introduced max invitations per referrer setting for campaigns with visual progress tracking
    • Added required field indicators for email challenges
  • Improvements

    • Enhanced customer contact editing dialog with improved UI
    • Improved campaign status visualization with color-coded badges
    • Optimized mobile scrolling experience in referral pages
    • Refined share text formatting to remove trailing whitespace
  • Updates

    • Updated to Next.js 16.1.3

@vercel
Copy link

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
backgrounds Ready Ready Preview, Comment Jan 18, 2026 4:04pm
blog Ready Ready Preview, Comment Jan 18, 2026 4:04pm
docs Ready Ready Preview, Comment Jan 18, 2026 4:04pm
grida Ready Ready Preview, Comment Jan 18, 2026 4:04pm
viewer Ready Ready Preview, Comment Jan 18, 2026 4:04pm
2 Skipped Deployments
Project Deployment Review Updated (UTC)
code Ignored Ignored Jan 18, 2026 4:04pm
legacy Ignored Ignored Jan 18, 2026 4:04pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 18, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Dependency Updates
apps/backgrounds/package.json, apps/viewer/package.json, editor/package.json, package.json
Next.js and related packages bumped from 16.1.2 to 16.1.3; removed hast type dependency; packageManager field relocated in viewer package.json
Database & Type Definitions
database/database.types.ts, editor/lib/platform/index.ts
Added Insert variant to customer_with_tags view with optional tags field; extended Platform.Customer schema with tags property
Email Challenge Branding
editor/app/(api)/(public)/v1/session/[session]/field/[field]/challenge/email/start/route.ts, editor/components/formfield/email-challenge.tsx, editor/grida-forms-hosted/e/formview.tsx
Implemented dynamic brand data resolution from www table (title, publisher, lang); added requiredAsterisk prop to email challenge components; brand support URLs passed to email payload
Tag Input Components & Styling
editor/components/tag/tag-input.tsx, editor/components/tag/tag.tsx, editor/components/tag/autocomplete.tsx
Extended Tag type with optional color property; added color badge rendering in autocomplete; implemented colored indicator dots and dynamic CSS variables in tag display
Tag Input UI Showcase
editor/app/(dev)/ui/components/tags/_page.tsx, editor/app/(dev)/ui/components/tags/page.tsx
Created TagsPage component with four demo sections (Basic, Autocomplete, Colors, Empty State); refactored page.tsx to wrapper with exported metadata
Predicate Query Infrastructure
editor/scaffolds/data-query/predicate-config.provider.tsx, editor/scaffolds/data-query/index.ts, editor/lib/data/index.ts, editor/scaffolds/grid-editor/components/query/predicate.tsx, editor/scaffolds/grid-editor/components/query/query-chips.tsx
Introduced PredicateConfig context provider for enum-based filtering; added array predicate editors with Postgres text array literal parsing/serialization; extended predicate menu with asChild rendering and enum/default predicate support; added "ov" (Overlaps) operator for array formats
Customer Management Refactoring
editor/scaffolds/platform/customer/customer-edit-dialog.tsx, editor/scaffolds/platform/customer/use-customer-feed.ts, editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/[uid]/page.tsx, editor/scaffolds/platform/customer/...
Split CustomerEditDialog into CustomerCreateDialog (with tags) and CustomerContactsEditDialog; updated insertCustomer to target customer_with_tags view with grida_ciam_public client; changed data parameter type to include Insert from customer_with_tags
Customer Listing with Tag Predicates
editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/page.tsx
Integrated tag-based predicate configuration; added useTags hook to fetch project tags; replaced CustomerEditDialog with CustomerCreateDialog; wired tag options to predicate config and dialog
Campaign Quests Table
editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/quests-table.tsx
Replaced hardcoded invitation limits with campaign.max_invitations_per_referrer; introduced BADGE_PRESETS for quest/claim statuses; added dynamic progress bars and Infinity icon for unlimited invitations; enhanced status calculation for null max values
Campaign Context
editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/store.tsx
Extended CampaignContext with optional max_invitations_per_referrer field
Workspace & Utilities
editor/scaffolds/workspace/workspace.tsx, editor/lib/supabase-postgrest/builder.ts
Added useOptionalTags hook for optional tag consumption; introduced XPostgrestQuery.Literal namespace with parsePostgresTextArrayLiteral and toPostgresTextArrayLiteral helpers for Postgres text array encoding/decoding
Form Field & Code Block Updates
editor/components/formfield/form-field.tsx, editor/components/ai-elements/code-block.tsx
Forwarded requiredAsterisk prop through EmailChallengePreview and ChallengeEmailField; changed code block container overflow from hidden to auto; removed explicit Element type import from hast
Referral Template Updates
editor/theme/templates/enterprise/west-referral/invitation/page.tsx, editor/theme/templates/enterprise/west-referral/referrer/page.tsx
Added ScrollArea for mobile scrolling support and early null exit in InvitationPageTemplate; normalized WebSharePayload by trimming title, text, and URL; restructured clipboard fallback to use normalized payload
Grid Editor & Exports
editor/scaffolds/grid-editor/index.tsx
Removed unused imports (useDataGridTextSearch, DataQueryPredicateChip, etc.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

daily

Poem

🐰 Tags now dance with colors bright,
Brands bloom from www's light,
Predicates paint customer views,
Dialogs split into create and news—
A hoppy refactor, all wrapped up tight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title "Daily" is vague and does not describe the actual changes in the changeset. Provide a descriptive title that summarizes the main changes, such as 'Update Next.js to 16.1.3 and add tag support for customers' or a title reflecting the primary feature addition.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch canary

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

Comment @coderabbitai help to get the list of available commands and usage tips.

…duce CustomerCreateDialog for creating customers with tags, and update CustomerContactsEditDialog for editing contact information. Adjust database types to include tags in customer insert operations.
…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.
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a 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".

Comment on lines +31 to +41
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);

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 the Infinity icon import to avoid shadowing the global constant.

Line 31 imports Infinity from lucide-react, which shadows the global constant and triggers Biome's linting rules. Rename the import to InfinityIcon and 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 from useFormSession to avoid infinite skeleton on request failure.

The hook returns an error field (via SWR) that should be checked alongside isLoading and data. Without it, a failed fetch leaves data undefined while error is set, causing the skeleton to persist indefinitely. Other usages in the codebase (e.g., row-edit-panel.tsx line 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 any error. While this simplifies the code, fetch failures will silently leave customers as 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, formDoc will 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 adding items metadata for array type consistency.

The tags property in properties uses type: "array" but lacks items specification (scalar type info), unlike the more detailed TABLE definition. For consistency with CSV validation logic that checks spec.items?.format, consider:

     tags: {
       type: "array",
       required: true,
+      items: { type: "string" },
     },

This aligns with the validate_row function that inspects spec.items for 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 between react-hook-form's field value type and PhoneInput's expected prop type. Consider typing the form field correctly or investigating if PhoneInput expects string | undefined vs string | null.

Comment on lines +76 to +81
<TagInput
tags={tags}
setTags={setTags}
activeTagIndex={null}
setActiveTagIndex={() => {}}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines 23 to +29
export type Tag = {
id: string;
text: string;
/**
* Optional tint color (e.g. hex string) for the tag.
*/
color?: string;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +21 to +48
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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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}")
EOF

Repository: 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"}')));
EOF

Repository: 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}')));
EOF

Repository: 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 tsx

Repository: gridaco/grida

Length of output: 84


🏁 Script executed:

# Search for usages of this function
rg "parsePostgresTextArrayLiteral" --type ts

Repository: 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.ts

Repository: 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 -100

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

Comment on lines +113 to +116
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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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

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

Comment on lines +306 to +338
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?.();
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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

Repository: 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.tsx

Repository: gridaco/grida

Length of output: 896


🏁 Script executed:

# Search for submitFormToDefaultEndpoint definition/import
rg "submitFormToDefaultEndpoint" --type ts --type tsx -B 2 -A 5

Repository: 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 5

Repository: gridaco/grida

Length of output: 84


🏁 Script executed:

# Search for submitFormToDefaultEndpoint definition across the repo
rg "submitFormToDefaultEndpoint" -t js -t ts --max-count 20

Repository: gridaco/grida

Length of output: 552


🏁 Script executed:

# Search for the client.claim method
rg "claim\(" --max-count 20 -A 3

Repository: 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.tsx

Repository: gridaco/grida

Length of output: 1672


🏁 Script executed:

# Read the submitFormToDefaultEndpoint definition
cat -n editor/grida-forms-hosted/internal-sdk/submit.ts | head -50

Repository: 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 15

Repository: 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.ts

Repository: 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 2

Repository: 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 10

Repository: 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 -30

Repository: 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 -40

Repository: 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 2

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

Suggested change
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).

@softmarshmallow softmarshmallow merged commit cae9021 into main Jan 18, 2026
10 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 19, 2026
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.

1 participant