Skip to content

Commit 83aadb9

Browse files
authored
Merge pull request #2020 from appwrite/fix-relationship-selects
Fix: relationship selection issues
2 parents 5db25a3 + 523785c commit 83aadb9

File tree

2 files changed

+169
-108
lines changed
  • src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/document-[document]

2 files changed

+169
-108
lines changed

src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/document-[document]/+page.svelte

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
const databaseId = page.params.database;
2121
const collectionId = page.params.collection;
2222
const documentId = page.params.document;
23-
const editing = true;
2423
2524
function initWork() {
2625
const prohibitedKeys = [
@@ -124,7 +123,7 @@
124123
<svelte:fragment slot="title">{label}</svelte:fragment>
125124

126125
<svelte:fragment slot="aside">
127-
<AttributeItem {attribute} bind:formValues={$work} {label} {editing} />
126+
<AttributeItem {attribute} bind:formValues={$work} {label} editing />
128127
</svelte:fragment>
129128

130129
<svelte:fragment slot="actions">

src/routes/(console)/project-[region]-[project]/databases/database-[database]/collection-[collection]/document-[document]/attributes/relationship.svelte

Lines changed: 168 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
<script lang="ts">
22
import { page } from '$app/state';
3-
import { PaginationInline } from '$lib/components';
4-
import { Button } from '$lib/elements/forms';
3+
import { Button, InputSelect } from '$lib/elements/forms';
54
import { preferences } from '$lib/stores/preferences';
65
import { sdk } from '$lib/stores/sdk';
76
import { Query, type Models } from '@appwrite.io/console';
87
import { onMount } from 'svelte';
98
import { doc } from '../store';
109
import { isRelationshipToMany } from './store';
10+
import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte';
1111
import { Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
12-
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
13-
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
1412
1513
export let id: string;
1614
export let label: string;
1715
16+
export let editing = false;
1817
export let value: string | string[];
1918
export let attribute: Models.AttributeRelationship;
2019
export let optionalText: string | undefined = undefined;
21-
export let editing = false;
2220
2321
const databaseId = page.params.database;
2422
@@ -28,6 +26,7 @@
2826
let relatedList: string[] = [];
2927
let singleRel: string;
3028
let showInput = false;
29+
let newItemValue: string = '';
3130
let limit = 10;
3231
let offset = 0;
3332
@@ -39,6 +38,7 @@
3938
singleRel = value as string;
4039
}
4140
}
41+
4242
documentList = await getDocuments();
4343
4444
if (editing && $doc?.[attribute.key]) {
@@ -59,22 +59,50 @@
5959
});
6060
6161
async function getDocuments(search: string = null) {
62-
if (search) {
63-
const documents = await sdk
64-
.forProject(page.params.region, page.params.project)
65-
.databases.listDocuments(databaseId, attribute.relatedCollection, [
66-
Query.startsWith('$id', search),
67-
Query.orderDesc('')
68-
]);
69-
return documents;
62+
const queries = search ? [Query.startsWith('$id', search), Query.orderDesc('')] : [];
63+
return await sdk
64+
.forProject(page.params.region, page.params.project)
65+
.databases.listDocuments(databaseId, attribute.relatedCollection, queries);
66+
}
67+
68+
function getAvailableOptions(excludeIndex?: number) {
69+
return options?.filter((option) => {
70+
const otherItems =
71+
excludeIndex !== undefined
72+
? relatedList.filter((_, idx) => idx !== excludeIndex)
73+
: relatedList;
74+
return !otherItems.includes(option.value);
75+
});
76+
}
77+
78+
function updateRelatedList() {
79+
relatedList = relatedList;
80+
value = relatedList;
81+
}
82+
83+
function removeItem(index: number) {
84+
if (relatedList.length === 1) {
85+
relatedList[index] = '';
7086
} else {
71-
const documents = await sdk
72-
.forProject(page.params.region, page.params.project)
73-
.databases.listDocuments(databaseId, attribute.relatedCollection);
74-
return documents;
87+
relatedList.splice(index, 1);
88+
}
89+
updateRelatedList();
90+
}
91+
92+
function addNewItem() {
93+
if (newItemValue) {
94+
relatedList = [...relatedList, newItemValue];
95+
value = relatedList;
96+
newItemValue = '';
97+
showInput = false;
7598
}
7699
}
77100
101+
function cancelAddItem() {
102+
newItemValue = '';
103+
showInput = false;
104+
}
105+
78106
//Reactive statements
79107
$: getDocuments(search).then((res) => (documentList = res));
80108
@@ -84,106 +112,140 @@
84112
.reverse()
85113
.slice(offset, offset + limit)
86114
: [];
87-
$: total = relatedList?.length ?? 0;
115+
116+
$: totalCount = relatedList?.length ?? 0;
117+
88118
$: options =
89119
documentList?.documents?.map((n) => {
90120
const data = displayNames.filter((name) => name !== '$id').map((name) => n?.[name]);
91-
return {
92-
value: n.$id,
93-
label: n.$id,
94-
data
95-
};
121+
return { value: n.$id, label: n.$id, data };
96122
}) ?? [];
123+
124+
$: hasItems = totalCount > 0;
125+
$: showTopAddButton = !editing && totalCount === 0 && !showInput;
126+
$: showBottomAddButton =
127+
(!editing && hasItems && !showInput) ||
128+
(editing && hasItems && relatedList.every((item) => item) && !showInput);
129+
$: showEmptyInput = editing && totalCount === 0 && !showInput;
97130
</script>
98131

99132
{#if isRelationshipToMany(attribute)}
100-
<div class="u-width-full-line">
101-
<Layout.Stack direction="row" alignContent="space-between">
102-
<Layout.Stack gap="xxs" direction="row" alignItems="center">
103-
<Typography.Text variant="m-500">{label}</Typography.Text>
104-
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
105-
{optionalText}
106-
</Typography.Text>
107-
</Layout.Stack>
108-
{#if editing || total === 0}
109-
<Button
110-
secondary
111-
on:click={() => {
112-
showInput = true;
113-
}}>
114-
<Icon icon={IconPlus} slot="start" size="s" />
115-
Add item
116-
</Button>
117-
{/if}
118-
</Layout.Stack>
119-
<ul class="u-flex-vertical u-gap-4 u-margin-block-start-4">
120-
{#if !editing && relatedList?.length}
121-
{#each relatedList as item, i}
122-
<InputSelect {id} required bind:value={item} {options} />
123-
<Button
124-
extraCompact
125-
ariaLabel={`Delete item ${i}`}
126-
on:click={() => {
127-
relatedList.splice(i, 1);
128-
relatedList = relatedList;
129-
value = relatedList;
130-
}}>
131-
<span class="icon-x" aria-hidden="true"></span>
133+
<Layout.Stack gap="xxl">
134+
<Layout.Stack gap="m">
135+
<Layout.Stack direction="row" alignContent="space-between">
136+
<Layout.Stack gap="xxs" direction="row" alignItems="center">
137+
<Typography.Text variant="m-500">{label}</Typography.Text>
138+
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary">
139+
{optionalText}
140+
</Typography.Text>
141+
</Layout.Stack>
142+
143+
{#if showTopAddButton}
144+
<Button secondary on:click={() => (showInput = true)}>
145+
<Icon icon={IconPlus} slot="start" size="s" />
146+
Add item
132147
</Button>
133-
{/each}
134-
{/if}
148+
{/if}
149+
</Layout.Stack>
135150

136-
{#if showInput}
137-
<InputSelect
138-
{id}
139-
label="Rel"
140-
required
141-
placeholder={`Select ${attribute.key}`}
142-
bind:value={relatedList[total]}
143-
options={options?.filter((n) => !relatedList.includes(n.value))}
144-
on:change={() => {
145-
value = relatedList;
146-
showInput = false;
147-
}} />
148-
<Button extraCompact ariaLabel={`Hide input`} on:click={() => (showInput = false)}>
149-
<span class="icon-x" aria-hidden="true"></span>
150-
</Button>
151-
{/if}
152-
{#if paginatedItems && editing}
153-
{#each paginatedItems as item, i}
154-
<InputSelect {id} label="Rel" required bind:value={item} {options} />
155-
<Button
156-
extraCompact
157-
ariaLabel={`Delete item ${i}`}
158-
on:click={() => {
159-
relatedList.splice(i, 1);
160-
relatedList = relatedList;
161-
value = relatedList;
162-
}}>
163-
<span class="icon-x" aria-hidden="true"></span>
151+
<Layout.Stack gap="m">
152+
<!-- Empty input for editing mode when no items exist -->
153+
{#if showEmptyInput}
154+
<Layout.Stack direction="row">
155+
<InputSelect
156+
{id}
157+
required
158+
{options}
159+
bind:value={relatedList[0]}
160+
placeholder={`Select ${attribute.key}`}
161+
on:change={() => {
162+
if (!relatedList[0]) relatedList = [''];
163+
updateRelatedList();
164+
}} />
165+
</Layout.Stack>
166+
{/if}
167+
168+
<!-- Existing items in editing mode -->
169+
{#if editing && hasItems}
170+
{#each paginatedItems as _, paginatedIndex}
171+
{@const actualIndex = offset + paginatedIndex}
172+
<Layout.Stack direction="row">
173+
<InputSelect
174+
{id}
175+
required
176+
options={getAvailableOptions(actualIndex)}
177+
bind:value={relatedList[actualIndex]}
178+
placeholder={`Select ${attribute.key}`}
179+
on:change={updateRelatedList} />
180+
{#if relatedList[actualIndex]}
181+
<div style:padding-block-start="0.5rem">
182+
<Button
183+
icon
184+
extraCompact
185+
on:click={() => removeItem(actualIndex)}>
186+
<Icon icon={IconX} size="s" />
187+
</Button>
188+
</div>
189+
{/if}
190+
</Layout.Stack>
191+
{/each}
192+
{/if}
193+
194+
<!-- Existing items in creation mode -->
195+
{#if !editing && hasItems}
196+
{#each relatedList as item, i}
197+
<Layout.Stack direction="row">
198+
<InputSelect
199+
{id}
200+
required
201+
options={getAvailableOptions(i)}
202+
bind:value={item}
203+
on:change={updateRelatedList} />
204+
<div style:padding-block-start="0.5rem">
205+
<Button
206+
icon
207+
extraCompact
208+
ariaLabel={`Delete item ${i}`}
209+
on:click={() => {
210+
relatedList.splice(i, 1);
211+
updateRelatedList();
212+
}}>
213+
<Icon icon={IconX} size="s" />
214+
</Button>
215+
</div>
216+
</Layout.Stack>
217+
{/each}
218+
{/if}
219+
220+
<!-- Input for adding new items -->
221+
{#if showInput}
222+
<Layout.Stack direction="row">
223+
<InputSelect
224+
{id}
225+
required
226+
placeholder={`Select ${attribute.key}`}
227+
bind:value={newItemValue}
228+
options={getAvailableOptions()}
229+
on:change={addNewItem} />
230+
<div style:padding-block-start="0.5rem">
231+
<Button icon extraCompact on:click={cancelAddItem}>
232+
<Icon icon={IconX} size="s" />
233+
</Button>
234+
</div>
235+
</Layout.Stack>
236+
{/if}
237+
</Layout.Stack>
238+
239+
{#if showBottomAddButton}
240+
<Layout.Stack direction="row" alignContent="flex-start">
241+
<Button extraCompact on:click={() => (showInput = true)}>
242+
<Icon icon={IconPlus} slot="start" size="s" />
243+
Add item
164244
</Button>
165-
{/each}
245+
</Layout.Stack>
166246
{/if}
167-
</ul>
168-
{#if editing}
169-
<div class="u-flex u-margin-block-start-32 u-main-space-between">
170-
<p class="text">Total results: {total}</p>
171-
<PaginationInline {limit} bind:offset {total} hidePages />
172-
</div>
173-
{/if}
174-
175-
{#if total > 0 && !editing}
176-
<Button
177-
extraCompact
178-
disabled={showInput}
179-
on:click={() => {
180-
showInput = true;
181-
}}>
182-
<Icon icon={IconPlus} slot="start" size="s" />
183-
Add item
184-
</Button>
185-
{/if}
186-
</div>
247+
</Layout.Stack>
248+
</Layout.Stack>
187249
{:else}
188250
<InputSelect
189251
{id}

0 commit comments

Comments
 (0)