|
1 | 1 | <script lang="ts">
|
2 | 2 | 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'; |
5 | 4 | import { preferences } from '$lib/stores/preferences';
|
6 | 5 | import { sdk } from '$lib/stores/sdk';
|
7 | 6 | import { Query, type Models } from '@appwrite.io/console';
|
8 | 7 | import { onMount } from 'svelte';
|
9 | 8 | import { doc } from '../store';
|
10 | 9 | import { isRelationshipToMany } from './store';
|
| 10 | + import { IconPlus, IconX } from '@appwrite.io/pink-icons-svelte'; |
11 | 11 | 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'; |
14 | 12 |
|
15 | 13 | export let id: string;
|
16 | 14 | export let label: string;
|
17 | 15 |
|
| 16 | + export let editing = false; |
18 | 17 | export let value: string | string[];
|
19 | 18 | export let attribute: Models.AttributeRelationship;
|
20 | 19 | export let optionalText: string | undefined = undefined;
|
21 |
| - export let editing = false; |
22 | 20 |
|
23 | 21 | const databaseId = page.params.database;
|
24 | 22 |
|
|
28 | 26 | let relatedList: string[] = [];
|
29 | 27 | let singleRel: string;
|
30 | 28 | let showInput = false;
|
| 29 | + let newItemValue: string = ''; |
31 | 30 | let limit = 10;
|
32 | 31 | let offset = 0;
|
33 | 32 |
|
|
39 | 38 | singleRel = value as string;
|
40 | 39 | }
|
41 | 40 | }
|
| 41 | +
|
42 | 42 | documentList = await getDocuments();
|
43 | 43 |
|
44 | 44 | if (editing && $doc?.[attribute.key]) {
|
|
59 | 59 | });
|
60 | 60 |
|
61 | 61 | 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] = ''; |
70 | 86 | } 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; |
75 | 98 | }
|
76 | 99 | }
|
77 | 100 |
|
| 101 | + function cancelAddItem() { |
| 102 | + newItemValue = ''; |
| 103 | + showInput = false; |
| 104 | + } |
| 105 | +
|
78 | 106 | //Reactive statements
|
79 | 107 | $: getDocuments(search).then((res) => (documentList = res));
|
80 | 108 |
|
|
84 | 112 | .reverse()
|
85 | 113 | .slice(offset, offset + limit)
|
86 | 114 | : [];
|
87 |
| - $: total = relatedList?.length ?? 0; |
| 115 | +
|
| 116 | + $: totalCount = relatedList?.length ?? 0; |
| 117 | +
|
88 | 118 | $: options =
|
89 | 119 | documentList?.documents?.map((n) => {
|
90 | 120 | 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 }; |
96 | 122 | }) ?? [];
|
| 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; |
97 | 130 | </script>
|
98 | 131 |
|
99 | 132 | {#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 |
132 | 147 | </Button>
|
133 |
| - {/each} |
134 |
| - {/if} |
| 148 | + {/if} |
| 149 | + </Layout.Stack> |
135 | 150 |
|
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 |
164 | 244 | </Button>
|
165 |
| - {/each} |
| 245 | + </Layout.Stack> |
166 | 246 | {/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> |
187 | 249 | {:else}
|
188 | 250 | <InputSelect
|
189 | 251 | {id}
|
|
0 commit comments