Skip to content

Conversation

ItzNotABug
Copy link
Member

@ItzNotABug ItzNotABug commented Aug 28, 2025

What does this PR do?

Allows removing single item relations.

Test Plan

Manual.

Screenshot 2025-08-28 at 6 24 17 PM Screenshot 2025-08-28 at 6 24 28 PM

Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

Have you read the Contributing Guidelines on issues?

(Write your answer here.)

Summary by CodeRabbit

  • New Features

    • Expanded multi-item relationship editing with per-item selects, add/remove controls, and a dedicated input area to add new items.
    • Paginated display for existing items while editing multi-relationships.
  • Enhancements

    • Improved single-relationship flow: clearer clearing behavior, dismiss button to reset selection, and refined layout.
    • Option lists now avoid duplicates and show a Loading option while data is fetched.
    • Add-item controls available at both top and bottom of lists, with better empty-state handling.
  • Refactor

    • Centralized option generation for consistency across single and multi selections.

Copy link

appwrite bot commented Aug 28, 2025

Console

Project ID: 688b7bf400350cbd60e9

Sites (2)
Site Status Logs Preview QR
 console-qa
688b7cf6003b1842c9dc
Ready Ready View Logs Preview URL QR Code
 console-cloud
688b7c18002b9b871a8f
Ready Ready View Logs Preview URL QR Code

Note

Appwrite has a Discord community with over 16 000 members.

Copy link
Contributor

coderabbitai bot commented Aug 28, 2025

Walkthrough

Refactors relationship.svelte to centralize option generation, adjust imports/types, and expand relationship editing. Adds generateOptions, multiple helper functions, and new UI flows for one-to-one and many-to-many: selection clearing, add/remove items, per-item selects, pagination in editing, and layout wrappers.

Changes

Cohort / File(s) Summary
Relationship editor refactor
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte
Import/type tweaks (add ComponentType, type-only imports). Introduce internal SelectOption type and generateOptions(). Overhaul single-relationship handling (clear to null, layout, dismiss). Major multi-relationship enhancements: helpers (getAvailableOptions, updateRelatedList, removeItem, addNewItem, cancelAddItem), per-item selects, add/remove UI, deduping, pagination, loading option.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant R as RelationshipComponent
  participant D as Rows/Store
  participant O as generateOptions()

  U->>R: Open relationship editor
  R->>D: Fetch related rows (async)
  D-->>R: rows/loading
  R->>O: generateOptions(loading, rows, column, editing)
  O-->>R: SelectOption[]

  alt Single relationship
    U->>R: Select/clear option
    R->>R: Map id -> row or null
    R-->>D: Update bound value
    opt Dismiss clicked
      R->>R: Clear value and singleRel
    end
  else Many relationship
    loop Per item
      U->>R: Add / change / remove item
      R->>O: get available options (dedupe)
      R->>R: updateRelatedList(add/remove/replace)
    end
    R-->>D: Persist relatedList binding
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I twitch my whiskers at dropdown skies,
New options sprout where relation lies;
Add, remove—hop, hop—so clean and light,
A warren of rows arranged just right.
With gentle paws I prune and blend—
Many or one, the links all mend. 🥕🐇

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch relations-inline-fix

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.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit 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:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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.

export let id: string;
export let label: string;
export let limited: boolean = false;
Copy link
Member Author

Choose a reason for hiding this comment

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

limited means from inline editor, spreadsheet cell.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte (6)

21-27: Fix prop types: value must allow null; label should allow undefined.

Current usage assigns null to value and undefined to label, but their types don't allow it.

-export let label: string;
+export let label: string | undefined = undefined;

-export let value: object | string[];
+export let value: Models.Row | Models.DefaultRow | null | string[];

31-37: Type mismatches: singleRel/search initialized with null but typed as string.

Prevents TS errors and aligns with null-clearing logic.

-let singleRel: string;
-let search: string = null;
+let singleRel: string | null = null;
+let search: string | null = null;

83-92: Guard against null option values when filtering selected options.

option.value can be null (e.g., “Unlink”), which makes includes(option.value) a type/runtime hazard.

-    return !otherItems.includes(option.value);
+    return typeof option.value !== 'string' ? true : !otherItems.includes(option.value);

270-288: Creation mode binding bug: bind:value to loop var doesn’t update the source array.

bind:value={item} mutates the local item, not relatedList[i]. As a result, relatedList never receives the new value.

-                                bind:value={item}
+                                bind:value={relatedList[i]}
                                 on:change={updateRelatedList} />

94-107: Avoid sending empty-string IDs; use null/explicit removal instead.

Setting the last item to '' keeps hasItems true and risks propagating [''] as a payload. Prefer removing the item and relying on the empty-input UI, or store null and strip nulls before submit.

-    if (relatedList.length === 1) {
-        relatedList[index] = '';
-    } else {
-        relatedList.splice(index, 1);
-    }
+    relatedList.splice(index, 1);
     updateRelatedList();

If you need a placeholder in editing mode, render it conditionally instead of storing sentinel values.


66-69: Replace Query.orderDesc('') with a valid field for ordering
Query.orderDesc('') is invalid—Appwrite requires a column name or a supported system field (e.g. $createdAt, $updatedAt, $sequence). Ordering by $id is also unreliable across SDK/backend versions.
Location: src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte:66

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6277f4e and e8d4720.

📒 Files selected for processing (1)
  • src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte (4 hunks)
⏰ 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: e2e
  • GitHub Check: build
🔇 Additional comments (2)
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte (2)

174-201: Minor: reactive label reassignment to undefined requires type change.

label = undefined (Line 199) conflicts with export let label: string;. The earlier type fix will cover this; ensure the prop type includes undefined.


60-81: Wrap getRows in try/finally, update its type, and fix ordering

  • Use a try { … } finally { loadingRelationships = false } to ensure loadingRelationships always resets.
  • Change the signature to async function getRows(search: string | null = null).
  • Replace Query.orderDesc('') with the correct column key (e.g. '$id') and confirm which field should be used for descending order.

Comment on lines +12 to +19
type SelectOption = {
label: string;
value: string | null;
data?: string[];
disabled?: boolean;
badge?: string;
leadingIcon?: ComponentType;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

SelectOption.data typing vs assigned values can be non-strings.

generateOptions maps raw row fields (could be number/boolean/null) into data?: string[]. Coerce to string to satisfy the type.

 type SelectOption = {
   label: string;
   value: string | null;
-  data?: string[];
+  data?: string[];
   disabled?: boolean;
   badge?: string;
   leadingIcon?: ComponentType;
 };
+
+// Declare options variable (used reactively below)
+let options: SelectOption[] = [];

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

🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte
around lines 12-19, the SelectOption.data is typed as string[] but
generateOptions maps raw row fields that can be number/boolean/null; coerce
every mapped value to a string when building the data array (e.g. map using
String(value) or value === null ? 'null' : String(value)) so the assigned values
match the declared type, and ensure any undefined values are filtered out before
assigning to data.

Comment on lines +122 to +172
function generateOptions(
loading: boolean,
rows: Models.Row[] | undefined,
column: Models.ColumnRelationship,
editing: boolean
): SelectOption[] {
if (loading) {
return [{ label: 'Loading...', value: null, disabled: true }];
}
let baseOptions: SelectOption[] =
rows?.map((row) => {
const names = preferences
.getDisplayNames(column?.relatedTable)
.filter((name) => name !== '$id');
const values = names
.map((name) => row?.[name])
.filter((value) => value != null && typeof value === 'string' && value !== '');
const displayValues = !editing
? values
: values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
let label: string;
if (!values.length) {
label = row.$id;
} else {
label = `${row.$id} (${displayValues.join(' | ')})`;
}
return {
label,
value: row.$id,
data: names.map((name) => row?.[name])
};
}) ?? [];
if (!isRelationshipToMany(column) && baseOptions.length && value && limited) {
baseOptions = [
{
value: null,
badge: 'Unlink',
label: 'Remove relationship'
},
...baseOptions
];
}
return baseOptions;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden generateOptions: ensure data[] contains strings.

Without coercion this can break TS or downstream consumers.

-                return {
+                return {
                     label,
                     value: row.$id,
-                    data: names.map((name) => row?.[name])
+                    data: names.map((name) => {
+                        const v = row?.[name];
+                        return v == null ? '' : typeof v === 'string' ? v : String(v);
+                    })
                 };
📝 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
function generateOptions(
loading: boolean,
rows: Models.Row[] | undefined,
column: Models.ColumnRelationship,
editing: boolean
): SelectOption[] {
if (loading) {
return [{ label: 'Loading...', value: null, disabled: true }];
}
let baseOptions: SelectOption[] =
rows?.map((row) => {
const names = preferences
.getDisplayNames(column?.relatedTable)
.filter((name) => name !== '$id');
const values = names
.map((name) => row?.[name])
.filter((value) => value != null && typeof value === 'string' && value !== '');
const displayValues = !editing
? values
: values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
let label: string;
if (!values.length) {
label = row.$id;
} else {
label = `${row.$id} (${displayValues.join(' | ')})`;
}
return {
label,
value: row.$id,
data: names.map((name) => row?.[name])
};
}) ?? [];
if (!isRelationshipToMany(column) && baseOptions.length && value && limited) {
baseOptions = [
{
value: null,
badge: 'Unlink',
label: 'Remove relationship'
},
...baseOptions
];
}
return baseOptions;
}
function generateOptions(
loading: boolean,
rows: Models.Row[] | undefined,
column: Models.ColumnRelationship,
editing: boolean
): SelectOption[] {
if (loading) {
return [{ label: 'Loading...', value: null, disabled: true }];
}
let baseOptions: SelectOption[] =
rows?.map((row) => {
const names = preferences
.getDisplayNames(column?.relatedTable)
.filter((name) => name !== '$id');
const values = names
.map((name) => row?.[name])
.filter((value) => value != null && typeof value === 'string' && value !== '');
const displayValues = !editing
? values
: values.map((value) => (value.length > 5 ? value.slice(0, 5) + '...' : value));
let label: string;
if (!values.length) {
label = row.$id;
} else {
label = `${row.$id} (${displayValues.join(' | ')})`;
}
return {
label,
value: row.$id,
data: names.map((name) => {
const v = row?.[name];
return v == null ? '' : typeof v === 'string' ? v : String(v);
})
};
}) ?? [];
if (!isRelationshipToMany(column) && baseOptions.length && value && limited) {
baseOptions = [
{
value: null,
badge: 'Unlink',
label: 'Remove relationship'
},
...baseOptions
];
}
return baseOptions;
}
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte
around lines 122–172, the generated option objects set data: names.map((name) =>
row?.[name]) which can produce non-strings (or null/undefined) and break TS or
downstream consumers; change the mapping to coerce each value to a string (and
normalize null/undefined to an empty string) so data is always string[], e.g.
replace the current names.map(...) with a mapping that returns String(value) or
'' for missing values.

data: names.map((name) => row?.[name])
};
}) ?? []);
$: options = generateOptions(loadingRelationships, rowList?.rows, column, editing);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Undeclared reactive variable options.

options = generateOptions(...) assigns to an undeclared variable, causing a ReferenceError/TS error.

Add this declaration above the reactive block (e.g., after the SelectOption type):

+let options: SelectOption[] = [];
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte
around line 186, the reactive assignment "options = generateOptions(...)" uses
an undeclared variable; declare the variable before the reactive block (for
example add a line after the SelectOption type declaration: let options:
SelectOption[] = []), making sure the type matches generateOptions' return type
and initialize to an empty array so the reactive statement can assign to it
without causing a ReferenceError/TS error.

Comment on lines +326 to 357
<Layout.Stack direction="row" alignItems="center" gap="s">
<InputSelect
{id}
{options}
autofocus={limited}
bind:value={singleRel}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
on:change={() => {
if (singleRel === null) {
value = null;
} else {
value = rowList.rows.find((row) => row.$id === singleRel);
}
}} />

{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
</Layout.Stack>
{/if}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Single-item unlink UX works; add small safety for row lookup and align types.

Avoid potential crash if rowList isn’t ready and align with null-clearing flow.

-            on:change={() => {
-                if (singleRel === null) {
-                    value = null;
-                } else {
-                    value = rowList.rows.find((row) => row.$id === singleRel);
-                }
-            }} />
+            on:change={() => {
+                if (singleRel === null) {
+                    value = null;
+                } else {
+                    value = rowList?.rows?.find((row) => row.$id === singleRel) ?? 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
<Layout.Stack direction="row" alignItems="center" gap="s">
<InputSelect
{id}
{options}
autofocus={limited}
bind:value={singleRel}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
on:change={() => {
if (singleRel === null) {
value = null;
} else {
value = rowList.rows.find((row) => row.$id === singleRel);
}
}} />
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
</Layout.Stack>
{/if}
<Layout.Stack direction="row" alignItems="center" gap="s">
<InputSelect
{id}
{options}
autofocus={limited}
bind:value={singleRel}
required={column.required}
label={limited ? undefined : label}
placeholder={`Select ${column.key}`}
on:change={() => {
if (singleRel === null) {
value = null;
} else {
value = rowList?.rows?.find((row) => row.$id === singleRel) ?? null;
}
}} />
{#if !limited && singleRel}
<div style:padding-block-start="2.25rem">
<Button
icon
extraCompact
on:click={() => {
value = null;
singleRel = null;
}}>
<Icon icon={IconX} size="s" />
</Button>
</div>
{/if}
</Layout.Stack>
{/if}
🤖 Prompt for AI Agents
In
src/routes/(console)/project-[region]-[project]/databases/database-[database]/table-[table]/rows/columns/types/relationship.svelte
around lines 326–357, the on:change handler can crash if rowList or rowList.rows
is undefined and may miscompare ID types; update the handler to safely access
rows (e.g., guard with rowList && rowList.rows or use optional chaining) and set
value to null when no match is found, and normalize ID types when comparing
(e.g., compare String(row.$id) === String(singleRel)) so the lookup never throws
and aligns with the null-clearing flow.

@ItzNotABug ItzNotABug merged commit b2286a1 into main Aug 28, 2025
4 of 5 checks passed
@ItzNotABug ItzNotABug deleted the relations-inline-fix branch August 28, 2025 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants