Skip to content

Commit 94af62c

Browse files
hahn-kevmyieye
andauthored
bring back a button to create new words from an empty search (#2348)
* bring back a button to create new words from an empty search * Redesign Add searched word and make vlist more stable --------- Co-authored-by: Tim Haasdyk <tim_haasdyk@sil.org>
1 parent 248ea03 commit 94af62c

12 files changed

Lines changed: 129 additions & 23 deletions

File tree

frontend/viewer/src/locales/en.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ msgstr "Add part of"
168168
msgid "Add Sense"
169169
msgstr "Add Sense"
170170

171+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
172+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
173+
#: src/project/browse/EntriesList.svelte
174+
msgid "Add to dictionary"
175+
msgstr "Add to dictionary"
176+
171177
#. Dialog title
172178
#: src/lib/entry-editor/NewEntryDialog.svelte
173179
msgid "Add Word"
@@ -1322,6 +1328,7 @@ msgstr "No subject, unable to create a new {0}"
13221328
#. Relevant view: Lite
13231329
#. Classic view equivalent: "No entries found"
13241330
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1331+
#: src/project/browse/EntriesList.svelte
13251332
msgid "No words found"
13261333
msgstr "No words found"
13271334

frontend/viewer/src/locales/es.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Añadir parte de"
173173
msgid "Add Sense"
174174
msgstr "Añadir acepción"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "No hay tema, no se puede crear un nuevo {0}"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "No se han encontrado palabras"
13321339

frontend/viewer/src/locales/fr.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Ajouter une partie de"
173173
msgid "Add Sense"
174174
msgstr "Ajouter du sens"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "Aucun sujet, impossible de créer un nouveau {0}"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "Aucun mot trouvé"
13321339

frontend/viewer/src/locales/id.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Tambahkan bagian dari"
173173
msgid "Add Sense"
174174
msgstr "Tambahkan Pengertian"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "Tidak ada subjek, tidak dapat membuat {0}baru"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "Tidak ada kata yang ditemukan"
13321339

frontend/viewer/src/locales/ko.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "의 일부를 추가합니다."
173173
msgid "Add Sense"
174174
msgstr "센스 추가"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "제목이 없습니다, 새로 만들 수 없습니다 {0}"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "단어를 찾을 수 없습니다."
13321339

frontend/viewer/src/locales/ms.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Tambah sebahagian daripada"
173173
msgid "Add Sense"
174174
msgstr "Tambah Makna"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "Tiada subjek, tidak dapat membuat {0} baru"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "Tiada perkataan ditemui"
13321339

frontend/viewer/src/locales/sw.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Ongeza sehemu ya"
173173
msgid "Add Sense"
174174
msgstr "Ongeza Maana"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "Hakuna kigezo, haiwezi kutengeneza {0} mpya"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "Hakuna neno liliyopatikana"
13321339

frontend/viewer/src/locales/vi.po

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ msgstr "Thêm phần của"
173173
msgid "Add Sense"
174174
msgstr "Thêm Ý nghĩa (sense)"
175175

176+
#. Subtitle of a placeholder row shown at the end of the entry list while searching (also in the no-results state)
177+
#. The search text is displayed above it as a headword; clicking opens the new-entry dialog pre-filled with that text
178+
#: src/project/browse/EntriesList.svelte
179+
msgid "Add to dictionary"
180+
msgstr ""
181+
176182
#. Dialog title
177183
#: src/lib/entry-editor/NewEntryDialog.svelte
178184
msgid "Add Word"
@@ -1327,6 +1333,7 @@ msgstr "Không có chủ đề, không thể tạo mới {0}"
13271333
#. Relevant view: Lite
13281334
#. Classic view equivalent: "No entries found"
13291335
#: src/lib/entry-editor/EntryOrSensePicker.svelte
1336+
#: src/project/browse/EntriesList.svelte
13301337
msgid "No words found"
13311338
msgstr "Không tìm thấy từ nào"
13321339

frontend/viewer/src/project/browse/EntriesList.svelte

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {Debounced, watch} from 'runed';
44
import EntryRow from './EntryRow.svelte';
55
import Button from '$lib/components/ui/button/button.svelte';
6+
import ListItem from '$lib/components/ListItem.svelte';
67
import {cn} from '$lib/utils';
78
import {t} from 'svelte-i18n-lingui';
89
import DevContent from '$lib/layout/DevContent.svelte';
@@ -19,6 +20,8 @@
1920
import Delayed from '$lib/components/Delayed.svelte';
2021
import {EntryLoaderService} from '$lib/services/entry-loader-service.svelte';
2122
import {onDestroy, untrack} from 'svelte';
23+
import {useViewService} from '$lib/views/view-service.svelte';
24+
import {pt} from '$lib/views/view-text';
2225
2326
let {
2427
search = '',
@@ -49,6 +52,7 @@
4952
const miniLcmApi = $derived(projectContext.maybeApi);
5053
const dialogsService = useDialogsService();
5154
const projectEventBus = useProjectEventBus();
55+
const viewService = useViewService();
5256
5357
// The closures maybe need to be created OUTSIDE untrack so they maintain reactivity
5458
const deps = {
@@ -58,6 +62,7 @@
5862
};
5963
6064
let entryLoader = $derived(!miniLcmApi ? undefined : untrack(() => new EntryLoaderService(miniLcmApi, deps)));
65+
const displayedEntryCount = $derived(entryLoader?.totalCount ?? 0);
6166
6267
// Destroy the previous entryLoader when a new one is created
6368
watch(
@@ -99,17 +104,24 @@
99104
// Generate a random number of skeleton rows
100105
const skeletonRowCount = Math.ceil(Math.random() * 3) + 3;
101106
102-
// Generate index array for virtual list.
103-
// We use a small number of skeletons if the total count is not yet known
104-
// to avoid a "white phase" between initial load and list initialization.
105-
const indexArray = $derived(
106-
entryLoader?.totalCount !== undefined
107-
? Array.from({ length: entryLoader.totalCount }, (_, i) => i)
108-
: Array.from({ length: skeletonRowCount }, (_, i) => i)
109-
);
107+
const canCreateFromSearch = $derived(search?.trim() && !disableNewEntry);
108+
const showTerminalCreateRow = $derived(canCreateFromSearch && displayedEntryCount > 0);
110109
111-
async function handleNewEntry() {
112-
const entry = await dialogsService.createNewEntry(undefined, {
110+
type ListRow = {key: string, index: number, create?: boolean};
111+
const rows: ListRow[] = $derived.by(() => {
112+
if (entryLoader?.totalCount === undefined) {
113+
return Array.from({length: skeletonRowCount}, (_, i) => ({key: `skeleton-${i}`, index: i}));
114+
}
115+
const generation = entryLoader.generation;
116+
const entryRows: ListRow[] = Array.from({length: displayedEntryCount}, (_, i) => ({key: `${generation}-${i}`, index: i}));
117+
if (showTerminalCreateRow) {
118+
entryRows.push({key: `${generation}-create`, index: displayedEntryCount, create: true});
119+
}
120+
return entryRows;
121+
});
122+
123+
async function handleNewEntry(headword: string | undefined = undefined) {
124+
const entry = await dialogsService.createNewEntry(headword, {
113125
publishIn: publication ? [publication] : [],
114126
}, {
115127
semanticDomains: semanticDomain ? [semanticDomain] : [],
@@ -173,6 +185,18 @@
173185
174186
</script>
175187

188+
{#snippet newEntryFromSearchRow(className: string = '')}
189+
<ListItem
190+
class={cn('bg-transparent shadow-none hover:shadow-none border-2 border-dashed border-muted-foreground/40', className)}
191+
onclick={() => handleNewEntry(search)}>
192+
{#snippet icon()}
193+
<Icon icon="i-mdi-plus-thick" class="size-6 text-primary/60" />
194+
{/snippet}
195+
<span class="font-medium text-2xl">{search}</span>
196+
<span class="text-sm text-muted-foreground">{$t`Add to dictionary`}</span>
197+
</ListItem>
198+
{/snippet}
199+
176200
<FabContainer>
177201
<DevContent>
178202
<Button
@@ -184,7 +208,7 @@
184208
/>
185209
</DevContent>
186210
{#if !disableNewEntry}
187-
<PrimaryNewEntryButton onclick={handleNewEntry} shortForm />
211+
<PrimaryNewEntryButton onclick={() => handleNewEntry()} shortForm />
188212
{/if}
189213
</FabContainer>
190214

@@ -197,28 +221,32 @@
197221
{:else}
198222
<div class="h-full">
199223
{#if entryLoader?.totalCount === 0}
200-
<div class="flex items-center justify-center h-full text-muted-foreground">
201-
<p>{$t`No entries found`}</p>
224+
<div class="flex flex-col items-center justify-center h-full gap-4 px-4">
225+
<p class="text-muted-foreground">{pt($t`No entries found`, $t`No words found`, viewService.currentView)}</p>
226+
227+
{#if canCreateFromSearch}
228+
{@render newEntryFromSearchRow('max-w-md')}
229+
{/if}
202230
</div>
203231
{/if}
204232

205233
<VList bind:this={vList}
206-
data={indexArray}
234+
data={rows}
207235
class="h-full p-0.5 md:pr-3 after:h-12 after:block"
208-
getKey={(index: number) => `${entryLoader?.generation ?? EntryLoaderService.DEFAULT_GENERATION}-${index}`}
236+
getKey={(row: ListRow) => row.key}
209237
bufferSize={450}>
210-
{#snippet children(index: number)}
211-
{#key entryLoader?.generation ?? EntryLoaderService.DEFAULT_GENERATION}
238+
{#snippet children(row: ListRow)}
239+
{#if row.create}
240+
{@render newEntryFromSearchRow()}
241+
{:else}
212242
<Delayed
213-
getCached={() => entryLoader?.getCachedEntryByIndex(index)}
214-
load={() => entryLoader?.getOrLoadEntryByIndex(index)}
243+
getCached={() => entryLoader?.getCachedEntryByIndex(row.index)}
244+
load={() => entryLoader?.getOrLoadEntryByIndex(row.index)}
215245
delay={250}
216246
>
217247
{#snippet children(state)}
218248
{#if state.loading || !state.current}
219-
<!-- we want the initial loading state and the first loading entries
220-
to share the same skeletons, so there's no flicker -->
221-
<EntryRow class="mb-2" skeleton={true} />
249+
<EntryRow class="mb-2" skeleton={true}/>
222250
{:else}
223251
{@const entry = state.current}
224252
<EntryMenu {entry} contextMenu>
@@ -231,7 +259,7 @@
231259
{/if}
232260
{/snippet}
233261
</Delayed>
234-
{/key}
262+
{/if}
235263
{/snippet}
236264
</VList>
237265
</div>

frontend/viewer/tests/entries-list-component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export class EntriesListComponent {
2525
async goto(waitForTestUtils = false): Promise<void> {
2626
await this.page.goto('/testing/project-view');
2727
await waitForProjectViewReady(this.page, waitForTestUtils);
28+
const renderedRowCount = await this.entryRows.count();
29+
expect(renderedRowCount).toBeGreaterThan(0);
2830
}
2931

3032
async waitForSkeletonsToResolve(): Promise<void> {

0 commit comments

Comments
 (0)