Skip to content

Commit 9fe905a

Browse files
authored
Merge branch 'dev' into issue/809
2 parents 02c4323 + a8481f8 commit 9fe905a

File tree

6 files changed

+1037
-14
lines changed

6 files changed

+1037
-14
lines changed

client/src/store/modules/script.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,25 @@ export default {
205205
Vue.$toast.error('Unable to delete cue');
206206
}
207207
},
208+
async SEARCH_CUES(context, { identifier, cueTypeId }) {
209+
const params = new URLSearchParams();
210+
params.append('identifier', identifier);
211+
params.append('cue_type_id', cueTypeId);
212+
213+
const response = await fetch(`${makeURL('/api/v1/show/cues/search')}?${params}`, {
214+
method: 'GET',
215+
headers: {
216+
'Content-Type': 'application/json',
217+
},
218+
});
219+
220+
if (response.ok) {
221+
const result = await response.json();
222+
return result;
223+
}
224+
log.error('Unable to search for cue');
225+
throw new Error('Cue search failed');
226+
},
208227
async GET_CUTS(context) {
209228
const response = await fetch(`${makeURL('/api/v1/show/script/cuts')}`, {
210229
method: 'GET',

client/src/vue_components/show/config/cues/CueEditor.vue

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@
55
>
66
<b-row class="script-row">
77
<b-col cols="2">
8-
<b-button
9-
v-b-modal.go-to-page
10-
variant="success"
11-
>
12-
Go to Page
13-
</b-button>
8+
<b-button-group>
9+
<b-button
10+
v-b-modal.go-to-page-cue-editor
11+
variant="success"
12+
>
13+
Go to Page
14+
</b-button>
15+
<b-button
16+
v-b-modal.jump-to-cue
17+
variant="success"
18+
>
19+
Go to Cue
20+
</b-button>
21+
</b-button-group>
1422
</b-col>
1523
<b-col
1624
cols="2"
@@ -95,9 +103,10 @@
95103
/>
96104
</div>
97105
</b-modal>
106+
<jump-to-cue-modal @navigate="handleJumpToCue" />
98107
<b-modal
99-
id="go-to-page"
100-
ref="go-to-page"
108+
id="go-to-page-cue-editor"
109+
ref="go-to-page-cue-editor"
101110
title="Go to Page"
102111
size="sm"
103112
:hide-header-close="changingPage"
@@ -138,12 +147,13 @@ import log from 'loglevel';
138147
139148
import { makeURL } from '@/js/utils';
140149
import ScriptLineCueEditor from '@/vue_components/show/config/cues/ScriptLineCueEditor.vue';
150+
import JumpToCueModal from '@/vue_components/show/config/cues/JumpToCueModal.vue';
141151
import { minValue, required } from 'vuelidate/lib/validators';
142152
import { notNull, notNullAndGreaterThanZero } from '@/js/customValidators';
143153
144154
export default {
145155
name: 'CueEditor',
146-
components: { ScriptLineCueEditor },
156+
components: { ScriptLineCueEditor, JumpToCueModal },
147157
data() {
148158
return {
149159
currentEditPage: 1,
@@ -293,6 +303,10 @@ export default {
293303
this.currentEditPage = pageNo;
294304
await this.LOAD_SCRIPT_PAGE(parseInt(pageNo, 10) + 1);
295305
},
306+
async handleJumpToCue(pageNumber) {
307+
await this.goToPageInner(pageNumber);
308+
this.$bvModal.hide('jump-to-cue');
309+
},
296310
...mapMutations(['REMOVE_PAGE', 'ADD_BLANK_LINE', 'SET_LINE']),
297311
...mapActions(['GET_SCENE_LIST', 'GET_ACT_LIST', 'GET_CHARACTER_LIST',
298312
'GET_CHARACTER_GROUP_LIST', 'LOAD_SCRIPT_PAGE', 'ADD_BLANK_PAGE', 'GET_SCRIPT_CONFIG_STATUS',
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
<template>
2+
<b-modal
3+
id="jump-to-cue"
4+
ref="jump-to-cue"
5+
title="Jump to Cue"
6+
size="md"
7+
:hide-header-close="searching || showResults"
8+
:hide-footer="searching"
9+
:no-close-on-backdrop="searching"
10+
:no-close-on-esc="searching"
11+
@ok="performCueSearch"
12+
@hidden="resetCueSearch"
13+
>
14+
<!-- Search Form -->
15+
<b-form
16+
v-if="!showResults"
17+
v-show="!searching"
18+
@submit.stop.prevent="performCueSearch"
19+
>
20+
<b-form-group
21+
label="Cue Type"
22+
label-for="cue-type-input"
23+
:invalid-feedback="cueTypeError"
24+
:state="cueTypeErrorState"
25+
>
26+
<b-form-select
27+
id="cue-type-input"
28+
v-model="$v.cueSearchForm.cueTypeId.$model"
29+
:options="cueTypeOptions"
30+
:state="cueTypeErrorState"
31+
:disabled="searching"
32+
/>
33+
</b-form-group>
34+
35+
<b-form-group
36+
label="Identifier"
37+
label-for="identifier-input"
38+
:invalid-feedback="identifierError"
39+
:state="identifierErrorState"
40+
>
41+
<b-form-input
42+
id="identifier-input"
43+
v-model="$v.cueSearchForm.identifier.$model"
44+
placeholder="e.g., 1, 42, 1.5"
45+
:state="identifierErrorState"
46+
:disabled="searching"
47+
/>
48+
</b-form-group>
49+
50+
<!-- General Error Message -->
51+
<b-alert
52+
v-if="generalError"
53+
variant="danger"
54+
show
55+
>
56+
{{ generalError }}
57+
</b-alert>
58+
</b-form>
59+
60+
<!-- Loading Spinner -->
61+
<div
62+
v-if="searching"
63+
class="text-center"
64+
>
65+
<b-spinner
66+
variant="primary"
67+
label="Searching..."
68+
/>
69+
<p class="mt-2">
70+
Searching for cue...
71+
</p>
72+
</div>
73+
74+
<!-- Multiple Matches - Selection List -->
75+
<div v-else-if="multipleMatches">
76+
<p>Found {{ cueSearchResults.exact_matches.length }} cues matching "{{ cueSearchForm.identifier }}":</p>
77+
<b-list-group>
78+
<b-list-group-item
79+
v-for="(match, idx) in cueSearchResults.exact_matches"
80+
:key="idx"
81+
button
82+
@click="navigateToMatch(match)"
83+
>
84+
<strong>{{ match.cue_type.prefix }} {{ match.cue.ident }}</strong>
85+
- Page {{ match.location.page }}
86+
</b-list-group-item>
87+
</b-list-group>
88+
</div>
89+
90+
<!-- Suggestions (No Exact Match) -->
91+
<div v-else-if="showSuggestions">
92+
<b-alert
93+
variant="warning"
94+
show
95+
>
96+
No exact match found for "{{ cueSearchForm.identifier }}"
97+
</b-alert>
98+
<p>Did you mean one of these?</p>
99+
<b-list-group>
100+
<b-list-group-item
101+
v-for="(suggestion, idx) in cueSearchResults.suggestions"
102+
:key="idx"
103+
button
104+
@click="navigateToMatch(suggestion)"
105+
>
106+
<strong>{{ suggestion.cue_type.prefix }} {{ suggestion.cue.ident }}</strong>
107+
- Page {{ suggestion.location.page }}
108+
<b-badge
109+
variant="secondary"
110+
class="float-right"
111+
>
112+
{{ Math.round(suggestion.similarity_score * 100) }}% match
113+
</b-badge>
114+
</b-list-group-item>
115+
</b-list-group>
116+
</div>
117+
118+
<!-- No Matches At All -->
119+
<div v-else-if="noMatches">
120+
<b-alert
121+
variant="danger"
122+
show
123+
>
124+
No cues found matching "{{ cueSearchForm.identifier }}" for the selected cue type.
125+
</b-alert>
126+
</div>
127+
128+
<!-- Footer for results -->
129+
<template
130+
v-if="showResults"
131+
#modal-footer
132+
>
133+
<b-button
134+
variant="secondary"
135+
@click="resetCueSearch"
136+
>
137+
New Search
138+
</b-button>
139+
<b-button
140+
variant="primary"
141+
@click="$bvModal.hide('jump-to-cue')"
142+
>
143+
Cancel
144+
</b-button>
145+
</template>
146+
</b-modal>
147+
</template>
148+
149+
<script>
150+
import { required } from 'vuelidate/lib/validators';
151+
import { mapActions, mapGetters } from 'vuex';
152+
import log from 'loglevel';
153+
import Vue from 'vue';
154+
import { notNull } from '@/js/customValidators';
155+
156+
export default {
157+
name: 'JumpToCueModal',
158+
data() {
159+
return {
160+
cueSearchForm: {
161+
cueTypeId: null,
162+
identifier: '',
163+
},
164+
cueSearchResults: null,
165+
generalError: null,
166+
searching: false,
167+
showResults: false,
168+
};
169+
},
170+
validations: {
171+
cueSearchForm: {
172+
cueTypeId: {
173+
required,
174+
notNull,
175+
},
176+
identifier: {
177+
required,
178+
},
179+
},
180+
},
181+
computed: {
182+
...mapGetters(['CUE_TYPES']),
183+
cueTypeOptions() {
184+
const options = [{ value: null, text: 'Select a cue type...', disabled: true }];
185+
this.CUE_TYPES.forEach((type) => {
186+
options.push({
187+
value: type.id,
188+
text: `${type.prefix} - ${type.description || 'No description'}`,
189+
});
190+
});
191+
return options;
192+
},
193+
multipleMatches() {
194+
return this.cueSearchResults?.exact_matches?.length > 1;
195+
},
196+
showSuggestions() {
197+
return (
198+
this.cueSearchResults?.exact_matches?.length === 0
199+
&& this.cueSearchResults?.suggestions?.length > 0
200+
);
201+
},
202+
noMatches() {
203+
return (
204+
this.cueSearchResults?.exact_matches?.length === 0
205+
&& this.cueSearchResults?.suggestions?.length === 0
206+
);
207+
},
208+
cueTypeErrorState() {
209+
const { $dirty, $error } = this.$v.cueSearchForm.cueTypeId;
210+
return $dirty ? !$error : null;
211+
},
212+
cueTypeError() {
213+
if (this.$v.cueSearchForm.cueTypeId.$dirty) {
214+
if (!this.$v.cueSearchForm.cueTypeId.required || !this.$v.cueSearchForm.cueTypeId.notNull) {
215+
return 'Cue type is required';
216+
}
217+
}
218+
return '';
219+
},
220+
identifierErrorState() {
221+
const { $dirty, $error } = this.$v.cueSearchForm.identifier;
222+
return $dirty ? !$error : null;
223+
},
224+
identifierError() {
225+
if (this.$v.cueSearchForm.identifier.$dirty && !this.$v.cueSearchForm.identifier.required) {
226+
return 'Identifier is required';
227+
}
228+
return '';
229+
},
230+
},
231+
methods: {
232+
...mapActions(['SEARCH_CUES']),
233+
async performCueSearch(event) {
234+
if (event) {
235+
event.preventDefault();
236+
}
237+
this.$v.$touch();
238+
if (this.$v.$anyError) {
239+
return;
240+
}
241+
242+
this.searching = true;
243+
this.generalError = null;
244+
try {
245+
const result = await this.SEARCH_CUES({
246+
identifier: this.cueSearchForm.identifier.trim(),
247+
cueTypeId: this.cueSearchForm.cueTypeId,
248+
});
249+
this.cueSearchResults = result;
250+
251+
if (result.exact_matches?.length === 1) {
252+
await this.navigateToMatch(result.exact_matches[0]);
253+
this.$bvModal.hide('jump-to-cue');
254+
} else {
255+
this.showResults = true;
256+
}
257+
} catch (error) {
258+
this.generalError = 'Search failed. Please try again.';
259+
log.error('Cue search error:', error);
260+
} finally {
261+
this.searching = false;
262+
}
263+
},
264+
async navigateToMatch(match) {
265+
const targetPage = match.location.page;
266+
this.$emit('navigate', targetPage);
267+
Vue.$toast.success(
268+
`Jumped to ${match.cue_type.prefix} ${match.cue.ident} on page ${targetPage}`,
269+
);
270+
},
271+
resetCueSearch() {
272+
this.cueSearchForm.identifier = '';
273+
this.cueSearchForm.cueTypeId = null;
274+
this.cueSearchResults = null;
275+
this.generalError = null;
276+
this.showResults = false;
277+
this.$v.$reset();
278+
},
279+
},
280+
};
281+
</script>

client/src/vue_components/show/config/script/ScriptEditor.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<b-row class="script-row">
1212
<b-col cols="2">
1313
<b-button
14-
v-b-modal.go-to-page
14+
v-b-modal.go-to-page-script-editor
1515
variant="success"
1616
>
1717
Go to Page
@@ -206,8 +206,8 @@
206206
</div>
207207
</b-modal>
208208
<b-modal
209-
id="go-to-page"
210-
ref="go-to-page"
209+
id="go-to-page-script-editor"
210+
ref="go-to-page-script-editor"
211211
title="Go to Page"
212212
size="sm"
213213
:hide-header-close="changingPage"

0 commit comments

Comments
 (0)