@playpilot/tpi 8.30.0 → 8.31.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 +5 -5
- package/package.json +1 -1
- package/src/lib/api/externalPages.ts +1 -0
- package/src/lib/api/participants.ts +1 -1
- 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/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/Tabs.svelte +12 -23
- package/src/tests/lib/api/externalPages.test.js +134 -1
- 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/routes/components/Editorial/Search/TitleSearch.svelte +0 -96
package/package.json
CHANGED
|
@@ -151,6 +151,7 @@ export async function saveLinkInjections(linkInjections: LinkInjection[]): Promi
|
|
|
151
151
|
in_text: linkInjection.in_text ?? true,
|
|
152
152
|
inactive: !!linkInjection.inactive,
|
|
153
153
|
removed: !!linkInjection.removed,
|
|
154
|
+
type: linkInjection.type,
|
|
154
155
|
}))
|
|
155
156
|
|
|
156
157
|
const response = await fetchLinkInjections({
|
|
@@ -10,7 +10,7 @@ export async function fetchTitlesForParticipant(participant: ParticipantData, {
|
|
|
10
10
|
const params = {
|
|
11
11
|
language: getLanguage(),
|
|
12
12
|
region: await getRegionBasedOnIp(),
|
|
13
|
-
include_count:
|
|
13
|
+
include_count: false,
|
|
14
14
|
participant_sid: participant.sid,
|
|
15
15
|
page,
|
|
16
16
|
}
|
package/src/lib/api/providers.ts
CHANGED
|
@@ -12,7 +12,7 @@ export async function fetchProviders(): Promise<PlaylinkData[]> {
|
|
|
12
12
|
const params = {
|
|
13
13
|
region: await getRegionBasedOnIp(),
|
|
14
14
|
page: 1,
|
|
15
|
-
include_count:
|
|
15
|
+
include_count: false,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const response = await api<PlaylinkData[]>(`/providers/?api-token=${apiToken}&` + paramsToString(params))
|
package/src/lib/api/search.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getApiToken } from '$lib/token'
|
|
2
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
2
3
|
import type { TitleData } from '../types/title'
|
|
3
4
|
import { api } from './api'
|
|
4
5
|
|
|
@@ -9,3 +10,11 @@ export async function searchTitles(query: string): Promise<TitleData[]> {
|
|
|
9
10
|
|
|
10
11
|
return await api<TitleData[]>(`/search/titles/?api-token=${apiToken}&query=${query}`)
|
|
11
12
|
}
|
|
13
|
+
|
|
14
|
+
export async function searchParticipants(query: string): Promise<ParticipantData[]> {
|
|
15
|
+
const apiToken = getApiToken()
|
|
16
|
+
|
|
17
|
+
if (!apiToken) throw new Error('No token was provided')
|
|
18
|
+
|
|
19
|
+
return await api<ParticipantData[]>(`/search/participants/?api-token=${apiToken}&query=${query}`)
|
|
20
|
+
}
|
package/src/lib/api/titles.ts
CHANGED
|
@@ -45,8 +45,6 @@
|
|
|
45
45
|
const position: Position = JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }')
|
|
46
46
|
const height: number = parseInt(localStorage.getItem(editorHeightKey) || '0')
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
48
|
let editorElement: HTMLElement | null = $state(null)
|
|
51
49
|
let manualInjectionActive = $state(false)
|
|
52
50
|
let saving = $state(false)
|
|
@@ -113,6 +113,8 @@
|
|
|
113
113
|
<div class="header">
|
|
114
114
|
{#if type === 'title'}
|
|
115
115
|
<img class="poster" src={removeImageUrlPrefix(title_details!.standing_poster)} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
116
|
+
{:else if type === 'participant'}
|
|
117
|
+
<img class="poster participant" src={participant_details!.image} alt="" width="32" height="32" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
116
118
|
{:else}
|
|
117
119
|
<div class="placeholder-image"></div>
|
|
118
120
|
{/if}
|
|
@@ -207,7 +209,6 @@
|
|
|
207
209
|
.poster {
|
|
208
210
|
display: block;
|
|
209
211
|
width: margin(2);
|
|
210
|
-
height: margin(3);
|
|
211
212
|
border-radius: margin(0.25);
|
|
212
213
|
background: theme(content);
|
|
213
214
|
|
|
@@ -355,4 +356,3 @@
|
|
|
355
356
|
}
|
|
356
357
|
}
|
|
357
358
|
</style>
|
|
358
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { LinkInjection } from '$lib/types/injection'
|
|
2
|
+
import type { LinkInjection, LinkInjectionDataType } from '$lib/types/injection'
|
|
3
3
|
import type { TitleData } from '$lib/types/title'
|
|
4
4
|
import { onMount } from 'svelte'
|
|
5
5
|
import { playPilotBaseUrl } from '$lib/constants'
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
import RoundButton from '../RoundButton.svelte'
|
|
13
13
|
import Alert from './Alert.svelte'
|
|
14
14
|
import TextInput from './TextInput.svelte'
|
|
15
|
-
import
|
|
15
|
+
import TypeSearch from './Search/TypeSearch.svelte'
|
|
16
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
17
|
+
import { participantUrl, titleUrl } from '$lib/routes'
|
|
16
18
|
|
|
17
19
|
interface Props {
|
|
18
20
|
onsave: (linkInjection: LinkInjection) => void
|
|
@@ -25,7 +27,8 @@
|
|
|
25
27
|
|
|
26
28
|
let currentSelection = $state('')
|
|
27
29
|
let selectionSentence = $state('')
|
|
28
|
-
let
|
|
30
|
+
let selectedItem: TitleData | ParticipantData | null = $state(null)
|
|
31
|
+
let selectedType: LinkInjectionDataType = $state('title')
|
|
29
32
|
let phraseAfter = $state('')
|
|
30
33
|
let phraseBefore = $state('')
|
|
31
34
|
let error = $state('')
|
|
@@ -145,8 +148,9 @@
|
|
|
145
148
|
return total
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
function onselect(title: TitleData): void {
|
|
149
|
-
|
|
151
|
+
function onselect(title: TitleData | ParticipantData, type: LinkInjectionDataType): void {
|
|
152
|
+
selectedItem = title
|
|
153
|
+
selectedType = type
|
|
150
154
|
|
|
151
155
|
// Remove selection that was made anywhere on the page. This is to avoid the input from being overwritten
|
|
152
156
|
// by the selection right after selecting a result.
|
|
@@ -155,25 +159,28 @@
|
|
|
155
159
|
|
|
156
160
|
function save(): void {
|
|
157
161
|
if (!currentSelection) return
|
|
158
|
-
if (!
|
|
162
|
+
if (!selectedItem) return
|
|
159
163
|
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const linkInjection: LinkInjection = {
|
|
164
|
-
sid: selectedTitle.sid,
|
|
164
|
+
const linkInjection: Partial<LinkInjection> = {
|
|
165
|
+
sid: selectedItem.sid,
|
|
165
166
|
title: currentSelection,
|
|
166
167
|
sentence: selectionSentence,
|
|
167
168
|
phrase_before: phraseBefore,
|
|
168
169
|
phrase_after: phraseAfter,
|
|
169
|
-
|
|
170
|
-
key: generateInjectionKey(selectedTitle.sid),
|
|
171
|
-
title_details: selectedTitle,
|
|
170
|
+
key: generateInjectionKey(selectedItem.sid),
|
|
172
171
|
manual: true,
|
|
173
|
-
type:
|
|
172
|
+
type: selectedType,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selectedType === 'title') {
|
|
176
|
+
linkInjection.title_details = selectedItem as TitleData
|
|
177
|
+
linkInjection.playpilot_url = titleUrl(selectedItem as TitleData)
|
|
178
|
+
} else if (selectedType === 'participant') {
|
|
179
|
+
linkInjection.participant_details = selectedItem as ParticipantData
|
|
180
|
+
linkInjection.playpilot_url = participantUrl(selectedItem as ParticipantData)
|
|
174
181
|
}
|
|
175
182
|
|
|
176
|
-
onsave(linkInjection)
|
|
183
|
+
onsave(linkInjection as LinkInjection)
|
|
177
184
|
onclose()
|
|
178
185
|
}
|
|
179
186
|
</script>
|
|
@@ -198,13 +205,13 @@
|
|
|
198
205
|
{/if}
|
|
199
206
|
|
|
200
207
|
{#if !error}
|
|
201
|
-
<label for="text">Paired
|
|
208
|
+
<label for="text">Paired link</label>
|
|
202
209
|
{#key selectionSentence + currentSelection}
|
|
203
|
-
<
|
|
210
|
+
<TypeSearch {onselect} bind:query />
|
|
204
211
|
{/key}
|
|
205
212
|
{/if}
|
|
206
213
|
|
|
207
|
-
<button class="save" onclick={save} disabled={!currentSelection || !
|
|
214
|
+
<button class="save" onclick={save} disabled={!currentSelection || !selectedItem}>
|
|
208
215
|
Add playlink
|
|
209
216
|
</button>
|
|
210
217
|
</section>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// In many parts this is a copy of ./TitleSearchItem.svelte
|
|
3
|
+
// Perhaps worth unifying the two in some sense, but for now I decided to keep it simple.
|
|
4
|
+
|
|
5
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
6
|
+
import { removeImageUrlPrefix } from '$lib/image'
|
|
7
|
+
import { participantUrl } from '$lib/routes'
|
|
8
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
9
|
+
import IconNewTab from '../../Icons/IconNewTab.svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
participant: ParticipantData
|
|
13
|
+
hoverable?: boolean
|
|
14
|
+
onclick?: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { participant, hoverable = true, onclick = () => null }: Props = $props()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<button class="item" class:hoverable {onclick}>
|
|
21
|
+
<img class="image" src={participant.image || imagePlaceholderDataUrl} alt="" width="42" height="42" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
22
|
+
|
|
23
|
+
<div class="content">
|
|
24
|
+
<div class="name">{participant.name}</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<a
|
|
28
|
+
href={participantUrl(participant)}
|
|
29
|
+
target="_blank"
|
|
30
|
+
class="open-in-new-tab"
|
|
31
|
+
onclick={event => event.stopImmediatePropagation()}>
|
|
32
|
+
<IconNewTab />
|
|
33
|
+
</a>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<style lang="scss">
|
|
37
|
+
.item {
|
|
38
|
+
appearance: none;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: margin(1);
|
|
42
|
+
width: 100%;
|
|
43
|
+
background: transparent;
|
|
44
|
+
border: 0;
|
|
45
|
+
padding: margin(0.5);
|
|
46
|
+
border-bottom: 1px solid theme(content);
|
|
47
|
+
color: theme(text-color-alt);
|
|
48
|
+
font-family: inherit;
|
|
49
|
+
text-align: left;
|
|
50
|
+
font-size: margin(0.85);
|
|
51
|
+
|
|
52
|
+
&.hoverable {
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
|
|
55
|
+
&:hover {
|
|
56
|
+
background: theme(lighter);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:last-child {
|
|
61
|
+
border-bottom: 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.image {
|
|
66
|
+
flex: 0 0 auto;
|
|
67
|
+
height: auto;
|
|
68
|
+
width: margin(1.75);
|
|
69
|
+
aspect-ratio: 1/1;
|
|
70
|
+
border-radius: margin(0.25);
|
|
71
|
+
background: theme(content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.name {
|
|
75
|
+
color: theme(text-color);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.open-in-new-tab {
|
|
79
|
+
margin-left: auto;
|
|
80
|
+
color: theme(text-color-alt);
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
color: theme(text-color);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { searchTitles, searchParticipants } from '$lib/api/search'
|
|
3
|
+
import type { LinkInjectionDataType } from '$lib/types/injection'
|
|
4
|
+
import type { ParticipantData } from '$lib/types/participant'
|
|
5
|
+
import type { TitleData } from '$lib/types/title'
|
|
6
|
+
import Tabs from '../../Tabs.svelte'
|
|
7
|
+
import TextInput from '../TextInput.svelte'
|
|
8
|
+
import ParticipantSearchItem from './ParticipantSearchItem.svelte'
|
|
9
|
+
import TitleSearchItem from './TitleSearchItem.svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
onselect?: (item: TitleData | ParticipantData, type: LinkInjectionDataType) => void
|
|
13
|
+
query: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let { onselect = () => null, query = $bindable() }: Props = $props()
|
|
17
|
+
|
|
18
|
+
const tabs: { label: string, value: LinkInjectionDataType }[] = [{ label: 'Movies & Shows', value: 'title' }, { label: 'Cast & Crew', value: 'participant' }]
|
|
19
|
+
|
|
20
|
+
let results: TitleData[] | ParticipantData[] = $state([])
|
|
21
|
+
let selectedResult: TitleData | ParticipantData | null = $state(null)
|
|
22
|
+
let currentTab: LinkInjectionDataType = $state(tabs[0].value)
|
|
23
|
+
let loading = $state(false)
|
|
24
|
+
|
|
25
|
+
$effect(() => {
|
|
26
|
+
if (query) search(query)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
async function search(query: string): Promise<void> {
|
|
30
|
+
loading = true
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
results = currentTab === 'title' ? await searchTitles(query) : await searchParticipants(query)
|
|
34
|
+
} finally {
|
|
35
|
+
loading = false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function select(item: TitleData | ParticipantData): void {
|
|
40
|
+
query = currentTab === 'title' ? (item as TitleData).title : (item as ParticipantData).name
|
|
41
|
+
selectedResult = item
|
|
42
|
+
|
|
43
|
+
onselect(item, currentTab)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setTab(value: string): void {
|
|
47
|
+
currentTab = value as LinkInjectionDataType
|
|
48
|
+
query = ''
|
|
49
|
+
selectedResult = null
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<Tabs options={tabs} onclick={(value) => setTab(value)} />
|
|
54
|
+
|
|
55
|
+
<div class="search">
|
|
56
|
+
<TextInput
|
|
57
|
+
bind:value={query}
|
|
58
|
+
oninput={() => selectedResult = null}
|
|
59
|
+
name="search"
|
|
60
|
+
label="Search..." />
|
|
61
|
+
|
|
62
|
+
{#if query && !selectedResult}
|
|
63
|
+
<div class="results" data-testid="search-results">
|
|
64
|
+
{#each results as item (item.sid)}
|
|
65
|
+
{#if currentTab === 'title'}
|
|
66
|
+
<TitleSearchItem title={item as TitleData} onclick={() => select(item as TitleData)} />
|
|
67
|
+
{:else if currentTab === 'participant'}
|
|
68
|
+
<ParticipantSearchItem participant={item as ParticipantData} onclick={() => select(item as ParticipantData)} />
|
|
69
|
+
{/if}
|
|
70
|
+
{:else}
|
|
71
|
+
{#if !loading}
|
|
72
|
+
<em class="empty">No results found</em>
|
|
73
|
+
{/if}
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
|
|
78
|
+
{#if selectedResult}
|
|
79
|
+
<div class="selected">
|
|
80
|
+
{#if currentTab === 'title'}
|
|
81
|
+
<TitleSearchItem title={selectedResult as TitleData} hoverable={false} />
|
|
82
|
+
{:else if currentTab === 'participant'}
|
|
83
|
+
<ParticipantSearchItem participant={selectedResult as ParticipantData} hoverable={false} />
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style lang="scss">
|
|
90
|
+
.search {
|
|
91
|
+
position: relative;
|
|
92
|
+
margin-top: margin(0.5);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.results {
|
|
96
|
+
z-index: 1;
|
|
97
|
+
position: absolute;
|
|
98
|
+
top: calc(100% + margin(0.5));
|
|
99
|
+
left: 0;
|
|
100
|
+
right: 0;
|
|
101
|
+
height: 30vh;
|
|
102
|
+
max-height: margin(10);
|
|
103
|
+
border-radius: margin(0.5);
|
|
104
|
+
background: theme(lighter);
|
|
105
|
+
scrollbar-width: thin;
|
|
106
|
+
overflow: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.empty {
|
|
110
|
+
padding: margin(0.5);
|
|
111
|
+
font-size: margin(0.75);
|
|
112
|
+
color: theme(text-color-alt);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.selected {
|
|
116
|
+
margin: margin(0.5) 0;
|
|
117
|
+
border-radius: 0.5rem;
|
|
118
|
+
border: 2px solid theme(green);
|
|
119
|
+
}
|
|
120
|
+
</style>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Button from './Button.svelte'
|
|
3
|
+
|
|
2
4
|
interface Option {
|
|
3
5
|
label: string
|
|
4
6
|
value: string
|
|
@@ -6,16 +8,22 @@
|
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
options: Option[]
|
|
11
|
+
onclick?: (value: string) => void
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const { options }: Props = $props()
|
|
14
|
+
const { options, onclick = () => null }: Props = $props()
|
|
15
|
+
|
|
16
|
+
let active = $derived(options[0]?.value)
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
function onchange(value: string): void {
|
|
19
|
+
active = value
|
|
20
|
+
onclick(value)
|
|
21
|
+
}
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
24
|
<div class="tabs">
|
|
17
25
|
{#each options as { label, value }}
|
|
18
|
-
<
|
|
26
|
+
<Button variant={value === active ? 'filled' : 'border'} onclick={() => onchange(value)}>{label}</Button>
|
|
19
27
|
{/each}
|
|
20
28
|
</div>
|
|
21
29
|
|
|
@@ -23,25 +31,6 @@
|
|
|
23
31
|
.tabs {
|
|
24
32
|
display: flex;
|
|
25
33
|
gap: margin(0.5);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.tab {
|
|
29
|
-
cursor: pointer;
|
|
30
|
-
appearance: none;
|
|
31
|
-
background: transparent;
|
|
32
|
-
padding: margin(0.25) margin(0.5);
|
|
33
|
-
border: 1px solid currentColor;
|
|
34
|
-
border-radius: theme(tabs-border-radius, border-radius-small);
|
|
35
|
-
transition: opacity 100ms;
|
|
36
|
-
font-family: inherit;
|
|
37
|
-
font-weight: theme(tabs-font-weight, 500);
|
|
38
|
-
color: theme(tabs-text-color, text-color-alt);
|
|
39
|
-
font-size: inherit;
|
|
40
|
-
|
|
41
|
-
&.active,
|
|
42
|
-
&:hover {
|
|
43
|
-
background: theme(tabs-background-active, content);
|
|
44
|
-
color: theme(tabs-text-color-active, text-color);
|
|
45
|
-
}
|
|
34
|
+
font-size: theme(font-size-base);
|
|
46
35
|
}
|
|
47
36
|
</style>
|
|
@@ -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
|
})
|
|
@@ -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
|
})
|