@playpilot/tpi 8.30.0 → 8.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editorial.mount.js +8 -8
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +7 -7
- package/package.json +1 -1
- package/src/lib/api/externalPages.ts +1 -0
- package/src/lib/api/participants.ts +6 -3
- package/src/lib/api/providers.ts +1 -1
- package/src/lib/api/search.ts +9 -0
- package/src/lib/api/titles.ts +1 -1
- package/src/lib/enums/Sorting.ts +1 -0
- package/src/routes/components/Editorial/Editor.svelte +0 -2
- package/src/routes/components/Editorial/EditorItem.svelte +2 -2
- package/src/routes/components/Editorial/ManualInjection.svelte +26 -19
- package/src/routes/components/Editorial/Search/ParticipantSearchItem.svelte +86 -0
- package/src/routes/components/Editorial/Search/TypeSearch.svelte +120 -0
- package/src/routes/components/Participants/Participant.svelte +73 -4
- package/src/routes/components/Tabs.svelte +12 -23
- package/src/routes/elements/+page.svelte +6 -0
- package/src/tests/lib/api/externalPages.test.js +134 -1
- package/src/tests/lib/api/participants.test.js +17 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +1 -2
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +43 -6
- package/src/tests/routes/components/Editorial/Search/{TitleSearch.test.js → TypeSearch.test.js} +21 -9
- package/src/tests/routes/components/Participants/Participant.test.js +3 -2
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +0 -96
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
2
2
|
import { waitFor } from '@testing-library/svelte'
|
|
3
3
|
|
|
4
|
-
import { fetchLinkInjections, pollLinkInjections, runAiBasedOnResponse } from '$lib/api/externalPages'
|
|
4
|
+
import { fetchLinkInjections, pollLinkInjections, runAiBasedOnResponse, saveLinkInjections } from '$lib/api/externalPages'
|
|
5
5
|
import { authorize, isEditorialModeEnabled } from '$lib/api/auth'
|
|
6
6
|
import { Language } from '$lib/enums/Language'
|
|
7
7
|
import { api } from '$lib/api/api'
|
|
8
|
+
import { generateInjection } from '../../helpers'
|
|
8
9
|
|
|
9
10
|
vi.mock('$lib/api/api', () => ({
|
|
10
11
|
api: vi.fn(),
|
|
@@ -379,4 +380,136 @@ describe('$lib/api/externalPages', () => {
|
|
|
379
380
|
expect(api).toHaveBeenCalled()
|
|
380
381
|
})
|
|
381
382
|
})
|
|
383
|
+
|
|
384
|
+
describe('saveLinkInjections', () => {
|
|
385
|
+
beforeEach(() => {
|
|
386
|
+
// @ts-ignore
|
|
387
|
+
window.PlayPilotLinkInjections = { token: 'a' }
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const manualInjection = { ...generateInjection('Some injection', 'injection'), manual: true }
|
|
391
|
+
const aiInjection = generateInjection('Some other injection', 'other injection')
|
|
392
|
+
|
|
393
|
+
it('Should call api with POST method and only manual injections included', async () => {
|
|
394
|
+
vi.mocked(api).mockResolvedValueOnce({ ok: true })
|
|
395
|
+
|
|
396
|
+
// @ts-ignore
|
|
397
|
+
await saveLinkInjections([manualInjection, aiInjection])
|
|
398
|
+
|
|
399
|
+
expect(api).toHaveBeenCalledWith(
|
|
400
|
+
expect.stringContaining('api-token'),
|
|
401
|
+
expect.objectContaining({
|
|
402
|
+
method: 'POST',
|
|
403
|
+
body: expect.objectContaining({
|
|
404
|
+
manual_injections: [
|
|
405
|
+
{
|
|
406
|
+
sid: manualInjection.sid,
|
|
407
|
+
title: manualInjection.title,
|
|
408
|
+
sentence: manualInjection.sentence,
|
|
409
|
+
phrase_before: manualInjection.phrase_before,
|
|
410
|
+
phrase_after: manualInjection.phrase_after,
|
|
411
|
+
playpilot_url: manualInjection.playpilot_url,
|
|
412
|
+
after_article: false,
|
|
413
|
+
after_article_style: null,
|
|
414
|
+
in_text: true,
|
|
415
|
+
inactive: false,
|
|
416
|
+
removed: false,
|
|
417
|
+
type: manualInjection.type,
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
}),
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('Should apply given values for after_article, in_text, inactive and removed instead of defaults', async () => {
|
|
426
|
+
vi.mocked(api).mockResolvedValueOnce({ ok: true })
|
|
427
|
+
|
|
428
|
+
const injection = {
|
|
429
|
+
...manualInjection,
|
|
430
|
+
after_article: true,
|
|
431
|
+
after_article_style: 'modal_button',
|
|
432
|
+
in_text: false,
|
|
433
|
+
inactive: true,
|
|
434
|
+
removed: true,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// @ts-ignore
|
|
438
|
+
await saveLinkInjections([injection])
|
|
439
|
+
|
|
440
|
+
expect(api).toHaveBeenCalledWith(
|
|
441
|
+
expect.any(String),
|
|
442
|
+
expect.objectContaining({
|
|
443
|
+
body: expect.objectContaining({
|
|
444
|
+
manual_injections: [
|
|
445
|
+
expect.objectContaining({
|
|
446
|
+
after_article: true,
|
|
447
|
+
after_article_style: 'modal_button',
|
|
448
|
+
in_text: false,
|
|
449
|
+
inactive: true,
|
|
450
|
+
removed: true,
|
|
451
|
+
}),
|
|
452
|
+
],
|
|
453
|
+
}),
|
|
454
|
+
}),
|
|
455
|
+
)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('Should save with given type', async () => {
|
|
459
|
+
vi.mocked(api).mockResolvedValueOnce({ ok: true })
|
|
460
|
+
|
|
461
|
+
// @ts-ignore
|
|
462
|
+
await saveLinkInjections([{ ...manualInjection, type: 'participant' }])
|
|
463
|
+
|
|
464
|
+
expect(api).toHaveBeenCalledWith(
|
|
465
|
+
expect.any(String),
|
|
466
|
+
expect.objectContaining({
|
|
467
|
+
body: expect.objectContaining({
|
|
468
|
+
manual_injections: [
|
|
469
|
+
expect.objectContaining({
|
|
470
|
+
type: 'participant',
|
|
471
|
+
}),
|
|
472
|
+
],
|
|
473
|
+
}),
|
|
474
|
+
}),
|
|
475
|
+
)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('Should include content_text_selector from window.PlayPilotLinkInjections.selector', async () => {
|
|
479
|
+
vi.mocked(api).mockResolvedValueOnce({ ok: true })
|
|
480
|
+
|
|
481
|
+
// @ts-ignore
|
|
482
|
+
window.PlayPilotLinkInjections = { token: 'a', selector: '.some-element' }
|
|
483
|
+
|
|
484
|
+
// @ts-ignore
|
|
485
|
+
await saveLinkInjections([manualInjection])
|
|
486
|
+
|
|
487
|
+
expect(api).toHaveBeenCalledWith(
|
|
488
|
+
expect.any(String),
|
|
489
|
+
expect.objectContaining({
|
|
490
|
+
body: expect.objectContaining({
|
|
491
|
+
content_text_selector: '.some-element',
|
|
492
|
+
}),
|
|
493
|
+
}),
|
|
494
|
+
)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('Should return the original linkInjections array unchanged, including non-manual injections', async () => {
|
|
498
|
+
vi.mocked(api).mockResolvedValueOnce({ ok: true })
|
|
499
|
+
|
|
500
|
+
const linkInjections = [manualInjection, aiInjection]
|
|
501
|
+
|
|
502
|
+
// @ts-ignore
|
|
503
|
+
const result = await saveLinkInjections(linkInjections)
|
|
504
|
+
|
|
505
|
+
expect(result).toEqual(linkInjections)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('Should throw when the response is faulty', async () => {
|
|
509
|
+
vi.mocked(api).mockResolvedValueOnce(undefined)
|
|
510
|
+
|
|
511
|
+
// @ts-ignore
|
|
512
|
+
await expect(async () => await saveLinkInjections([manualInjection])).rejects.toThrowError('saveLinkInjections response was faulty')
|
|
513
|
+
})
|
|
514
|
+
})
|
|
382
515
|
})
|
|
@@ -5,6 +5,7 @@ import { fetchTitlesForParticipant } from '$lib/api/participants'
|
|
|
5
5
|
import { participants, title } from '$lib/fakeData'
|
|
6
6
|
import { getApiToken } from '$lib/token'
|
|
7
7
|
import { fakeFetch } from '../../helpers'
|
|
8
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
8
9
|
|
|
9
10
|
vi.mock('$lib/token', () => ({
|
|
10
11
|
getApiToken: vi.fn(),
|
|
@@ -57,5 +58,21 @@ describe('$lib/api/participants', () => {
|
|
|
57
58
|
|
|
58
59
|
expect(api).toHaveBeenCalledWith(`/titles/browse?api-token=some-token&language=en-US®ion=nl&include_count=false&participant_sid=${participants[0].sid}&page=1`)
|
|
59
60
|
})
|
|
61
|
+
|
|
62
|
+
it('Should use given sorting', async () => {
|
|
63
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
64
|
+
|
|
65
|
+
await fetchTitlesForParticipant(participants[0], { page: 5, sorting: Sorting.New })
|
|
66
|
+
|
|
67
|
+
expect(api).toHaveBeenCalledWith(expect.stringContaining(`ordering=${Sorting.New}`))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('Should not add ordering when not sorting is given', async () => {
|
|
71
|
+
vi.mocked(api).mockResolvedValueOnce({ results: [title] })
|
|
72
|
+
|
|
73
|
+
await fetchTitlesForParticipant(participants[0], { page: 5 })
|
|
74
|
+
|
|
75
|
+
expect(api).toHaveBeenCalledWith(expect.not.stringContaining('ordering'))
|
|
76
|
+
})
|
|
60
77
|
})
|
|
61
78
|
})
|
|
@@ -207,7 +207,6 @@ describe('EditorItem.svelte', () => {
|
|
|
207
207
|
|
|
208
208
|
expect(queryByLabelText('Expand')).not.toBeTruthy()
|
|
209
209
|
expect(getByText(fakeLinkInjections[4].participant_details?.name || '-')).toBeTruthy()
|
|
210
|
-
expect(container.querySelector('.
|
|
211
|
-
expect(container.querySelector('.placeholder-image')).toBeTruthy()
|
|
210
|
+
expect(container.querySelector('.participant')).toBeTruthy()
|
|
212
211
|
})
|
|
213
212
|
})
|
|
@@ -2,9 +2,10 @@ import { fireEvent, render, waitFor } from '@testing-library/svelte'
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import ManualInjection from '../../../../routes/components/Editorial/ManualInjection.svelte'
|
|
5
|
-
import { searchTitles } from '$lib/api/search'
|
|
6
|
-
import { title } from '$lib/fakeData'
|
|
5
|
+
import { searchParticipants, searchTitles } from '$lib/api/search'
|
|
6
|
+
import { participants, title } from '$lib/fakeData'
|
|
7
7
|
import { getIndexOfSelection } from '$lib/selection'
|
|
8
|
+
import { participantUrl, titleUrl } from '$lib/routes'
|
|
8
9
|
|
|
9
10
|
function createSelectionMock() {
|
|
10
11
|
const container = document.querySelector('div')
|
|
@@ -26,6 +27,7 @@ function createSelectionMock() {
|
|
|
26
27
|
|
|
27
28
|
vi.mock('$lib/api/search', () => ({
|
|
28
29
|
searchTitles: vi.fn(),
|
|
30
|
+
searchParticipants: vi.fn(),
|
|
29
31
|
}))
|
|
30
32
|
|
|
31
33
|
vi.mock('$lib/selection', () => ({
|
|
@@ -93,7 +95,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
93
95
|
sid: title.sid,
|
|
94
96
|
title: 'text',
|
|
95
97
|
sentence: 'Some text in a sentence',
|
|
96
|
-
playpilot_url:
|
|
98
|
+
playpilot_url: titleUrl(title),
|
|
97
99
|
key: expect.any(String),
|
|
98
100
|
title_details: title,
|
|
99
101
|
manual: true,
|
|
@@ -136,7 +138,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
136
138
|
sid: title.sid,
|
|
137
139
|
title: 'text',
|
|
138
140
|
sentence: 'Some text in a sentence',
|
|
139
|
-
playpilot_url:
|
|
141
|
+
playpilot_url: titleUrl(title),
|
|
140
142
|
key: expect.any(String),
|
|
141
143
|
title_details: title,
|
|
142
144
|
manual: true,
|
|
@@ -179,7 +181,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
179
181
|
sid: title.sid,
|
|
180
182
|
title: 'Some title',
|
|
181
183
|
sentence: 'Some title',
|
|
182
|
-
playpilot_url:
|
|
184
|
+
playpilot_url: titleUrl(title),
|
|
183
185
|
key: expect.any(String),
|
|
184
186
|
title_details: title,
|
|
185
187
|
manual: true,
|
|
@@ -222,7 +224,7 @@ describe('ManualInjection.svelte', () => {
|
|
|
222
224
|
sid: title.sid,
|
|
223
225
|
title: 'Some other paragraph',
|
|
224
226
|
sentence: 'Some other paragraph',
|
|
225
|
-
playpilot_url:
|
|
227
|
+
playpilot_url: titleUrl(title),
|
|
226
228
|
key: expect.any(String),
|
|
227
229
|
title_details: title,
|
|
228
230
|
manual: true,
|
|
@@ -285,4 +287,39 @@ describe('ManualInjection.svelte', () => {
|
|
|
285
287
|
|
|
286
288
|
await waitFor(() => expect(searchTitles).toHaveBeenCalled())
|
|
287
289
|
})
|
|
290
|
+
|
|
291
|
+
it('Should save participants when selected', async () => {
|
|
292
|
+
createSelectionMock()
|
|
293
|
+
|
|
294
|
+
const participant = participants[0]
|
|
295
|
+
|
|
296
|
+
vi.mocked(searchParticipants).mockResolvedValueOnce([participant])
|
|
297
|
+
vi.mocked(getIndexOfSelection).mockReturnValueOnce({ start: 5, end: 10 })
|
|
298
|
+
|
|
299
|
+
const onsave = vi.fn()
|
|
300
|
+
const { getByText, getByLabelText } = render(ManualInjection, { onsave })
|
|
301
|
+
|
|
302
|
+
await fireEvent.mouseUp(window)
|
|
303
|
+
await fireEvent.click(getByText('Cast & Crew'))
|
|
304
|
+
|
|
305
|
+
await waitFor(async () => {
|
|
306
|
+
await fireEvent.input(getByLabelText('Search...'), { target: { value: 'test' } })
|
|
307
|
+
await fireEvent.click(getByText(participants[0].name))
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
await fireEvent.click(getByText('Add playlink'))
|
|
311
|
+
|
|
312
|
+
expect(onsave).toHaveBeenCalledWith({
|
|
313
|
+
sid: participants[0].sid,
|
|
314
|
+
title: 'text',
|
|
315
|
+
sentence: 'Some text in a sentence',
|
|
316
|
+
playpilot_url: participantUrl(participant),
|
|
317
|
+
key: expect.any(String),
|
|
318
|
+
participant_details: participant,
|
|
319
|
+
manual: true,
|
|
320
|
+
phrase_before: 'Some',
|
|
321
|
+
phrase_after: 'in a',
|
|
322
|
+
type: 'participant',
|
|
323
|
+
})
|
|
324
|
+
})
|
|
288
325
|
})
|
package/src/tests/routes/components/Editorial/Search/{TitleSearch.test.js → TypeSearch.test.js}
RENAMED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { fireEvent, render, waitFor } from '@testing-library/svelte'
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { searchTitles } from '$lib/api/search'
|
|
4
|
+
import TypeSearch from '../../../../../routes/components/Editorial/Search/TypeSearch.svelte'
|
|
5
|
+
import { searchTitles, searchParticipants } from '$lib/api/search'
|
|
6
6
|
import { title } from '$lib/fakeData'
|
|
7
7
|
|
|
8
8
|
vi.mock('$lib/api/search', () => ({
|
|
9
9
|
searchTitles: vi.fn(),
|
|
10
|
+
searchParticipants: vi.fn(),
|
|
10
11
|
}))
|
|
11
12
|
|
|
12
|
-
describe('
|
|
13
|
+
describe('TypeSearch.svelte', () => {
|
|
13
14
|
beforeEach(() => {
|
|
14
15
|
vi.resetAllMocks()
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
it('Should search for titles when inputting text', async () => {
|
|
18
|
-
const { getByRole } = render(
|
|
19
|
+
const { getByRole } = render(TypeSearch)
|
|
19
20
|
|
|
20
21
|
await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
|
|
21
22
|
|
|
@@ -25,7 +26,7 @@ describe('TitleSearch.svelte', () => {
|
|
|
25
26
|
it('Should show empty state when no results are returned', async () => {
|
|
26
27
|
vi.mocked(searchTitles).mockResolvedValueOnce([])
|
|
27
28
|
|
|
28
|
-
const { getByRole, getByText } = render(
|
|
29
|
+
const { getByRole, getByText } = render(TypeSearch)
|
|
29
30
|
|
|
30
31
|
await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
|
|
31
32
|
|
|
@@ -37,7 +38,7 @@ describe('TitleSearch.svelte', () => {
|
|
|
37
38
|
it('Should show results when given', async () => {
|
|
38
39
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
39
40
|
|
|
40
|
-
const { getByRole, getByText, getByTestId } = render(
|
|
41
|
+
const { getByRole, getByText, getByTestId } = render(TypeSearch)
|
|
41
42
|
|
|
42
43
|
await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
|
|
43
44
|
|
|
@@ -52,15 +53,26 @@ describe('TitleSearch.svelte', () => {
|
|
|
52
53
|
vi.mocked(searchTitles).mockResolvedValueOnce([title])
|
|
53
54
|
|
|
54
55
|
const onselect = vi.fn()
|
|
55
|
-
const { getByRole, getByText, queryByTestId } = render(
|
|
56
|
+
const { getByRole, getByText, queryByTestId } = render(TypeSearch, { onselect, query: '' })
|
|
56
57
|
|
|
57
58
|
await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
|
|
58
59
|
|
|
59
60
|
await waitFor(async () => {
|
|
60
|
-
await fireEvent.click(
|
|
61
|
-
|
|
61
|
+
await fireEvent.click(getByText(title.title))
|
|
62
|
+
|
|
63
|
+
expect(onselect).toHaveBeenCalledWith(title, 'title')
|
|
62
64
|
expect(queryByTestId('search-results')).not.toBeTruthy()
|
|
63
65
|
expect(getByText(title.title)).toBeTruthy()
|
|
64
66
|
})
|
|
65
67
|
})
|
|
68
|
+
|
|
69
|
+
it('Should search for participants after changing tab when inputting text', async () => {
|
|
70
|
+
const { getByRole, getByText } = render(TypeSearch)
|
|
71
|
+
|
|
72
|
+
await fireEvent.click(getByText('Cast & Crew'))
|
|
73
|
+
await fireEvent.input(getByRole('textbox'), { target: { value: 'test' } })
|
|
74
|
+
|
|
75
|
+
expect(searchTitles).not.toHaveBeenCalled()
|
|
76
|
+
expect(searchParticipants).toHaveBeenCalled()
|
|
77
|
+
})
|
|
66
78
|
})
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
|
4
4
|
import Participant from '../../../../routes/components/Participants/Participant.svelte'
|
|
5
5
|
import { participants, title } from '$lib/fakeData'
|
|
6
6
|
import { fetchTitlesForParticipant } from '$lib/api/participants'
|
|
7
|
+
import { Sorting } from '$lib/enums/Sorting'
|
|
7
8
|
|
|
8
9
|
vi.mock('$lib/tracking', () => ({
|
|
9
10
|
track: vi.fn(),
|
|
@@ -21,7 +22,7 @@ describe('Participant.svelte', () => {
|
|
|
21
22
|
it('Should call fetchTitlesForParticipant on mount', () => {
|
|
22
23
|
render(Participant, { participant: participants[0] })
|
|
23
24
|
|
|
24
|
-
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1 })
|
|
25
|
+
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 1, sorting: Sorting.Popular })
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
it('Should render fetched titles after showing skeletons', async () => {
|
|
@@ -71,7 +72,7 @@ describe('Participant.svelte', () => {
|
|
|
71
72
|
|
|
72
73
|
await fireEvent.click(getByText('Show more'))
|
|
73
74
|
|
|
74
|
-
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2 })
|
|
75
|
+
expect(fetchTitlesForParticipant).toHaveBeenCalledWith(participants[0], { page: 2, sorting: Sorting.Popular })
|
|
75
76
|
})
|
|
76
77
|
|
|
77
78
|
it('Should render as small variant when given', () => {
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { searchTitles } from '$lib/api/search'
|
|
3
|
-
import type { TitleData } from '$lib/types/title'
|
|
4
|
-
import TextInput from '../TextInput.svelte'
|
|
5
|
-
import TitleSearchItem from './TitleSearchItem.svelte'
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
onselect?: (title: TitleData) => void
|
|
9
|
-
query: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let { onselect = () => null, query = $bindable() }: Props = $props()
|
|
13
|
-
|
|
14
|
-
let results: TitleData[] = $state([])
|
|
15
|
-
let selectedResult: TitleData | null = $state(null)
|
|
16
|
-
let loading = $state(false)
|
|
17
|
-
|
|
18
|
-
$effect(() => {
|
|
19
|
-
if (query) search(query)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
async function search(query: string): Promise<void> {
|
|
23
|
-
loading = true
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
results = await searchTitles(query)
|
|
27
|
-
} finally {
|
|
28
|
-
loading = false
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function select(title: TitleData): void {
|
|
33
|
-
query = title.title
|
|
34
|
-
selectedResult = title
|
|
35
|
-
|
|
36
|
-
onselect(title)
|
|
37
|
-
}
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
<div class="search">
|
|
41
|
-
<TextInput
|
|
42
|
-
bind:value={query}
|
|
43
|
-
oninput={() => selectedResult = null}
|
|
44
|
-
name="search"
|
|
45
|
-
label="Search..." />
|
|
46
|
-
|
|
47
|
-
{#if query && !selectedResult}
|
|
48
|
-
<div class="results" data-testid="search-results">
|
|
49
|
-
{#each results as title (title.sid)}
|
|
50
|
-
<TitleSearchItem {title} onclick={() => select(title)} />
|
|
51
|
-
{:else}
|
|
52
|
-
{#if !loading}
|
|
53
|
-
<em class="empty">No results found</em>
|
|
54
|
-
{/if}
|
|
55
|
-
{/each}
|
|
56
|
-
</div>
|
|
57
|
-
{/if}
|
|
58
|
-
|
|
59
|
-
{#if selectedResult}
|
|
60
|
-
<div class="selected">
|
|
61
|
-
<TitleSearchItem title={selectedResult} hoverable={false} />
|
|
62
|
-
</div>
|
|
63
|
-
{/if}
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<style lang="scss">
|
|
67
|
-
.search {
|
|
68
|
-
position: relative;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.results {
|
|
72
|
-
z-index: 1;
|
|
73
|
-
position: absolute;
|
|
74
|
-
top: calc(100% + margin(0.5));
|
|
75
|
-
left: 0;
|
|
76
|
-
right: 0;
|
|
77
|
-
height: 30vh;
|
|
78
|
-
max-height: margin(10);
|
|
79
|
-
border-radius: margin(0.5);
|
|
80
|
-
background: theme(lighter);
|
|
81
|
-
scrollbar-width: thin;
|
|
82
|
-
overflow: auto;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.empty {
|
|
86
|
-
padding: margin(0.5);
|
|
87
|
-
font-size: margin(0.75);
|
|
88
|
-
color: theme(text-color-alt);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.selected {
|
|
92
|
-
margin: margin(0.5) 0;
|
|
93
|
-
border-radius: 0.5rem;
|
|
94
|
-
border: 2px solid theme(green);
|
|
95
|
-
}
|
|
96
|
-
</style>
|