Skip to content

Commit 2662c78

Browse files
committed
Add simple search modal, fix preview modal not closing on cancel
1 parent 752f13f commit 2662c78

File tree

6 files changed

+296
-6
lines changed

6 files changed

+296
-6
lines changed

src/main.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,18 @@ export default class MediaDbPlugin extends Plugin {
6565
// register command to open search modal
6666
this.addCommand({
6767
id: 'open-media-db-search-modal',
68-
name: 'Add new Media DB entry',
68+
name: 'Create Media DB entry',
69+
callback: () => this.createEntryWithSearchModal(),
70+
});
71+
this.addCommand({
72+
id: 'open-media-db-advanced-search-modal',
73+
name: 'Create Media DB entry (advanced search)',
6974
callback: () => this.createEntryWithAdvancedSearchModal(),
7075
});
7176
// register command to open id search modal
7277
this.addCommand({
7378
id: 'open-media-db-id-search-modal',
74-
name: 'Add new Media DB entry by id',
79+
name: 'Create Media DB entry by id',
7580
callback: () => this.createEntryWithIdSearchModal(),
7681
});
7782
// register command to update the open note
@@ -152,12 +157,40 @@ export default class MediaDbPlugin extends Plugin {
152157
}
153158

154159
async createEntryWithSearchModal() {
160+
let types: string[] = [];
161+
let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openSearchModal({}, async (searchModalData) => {
162+
types = searchModalData.types;
163+
const apis = this.apiManager.apis.filter(x => x.hasTypeOverlap(searchModalData.types)).map(x => x.apiName);
164+
return await this.apiManager.query(searchModalData.query, apis);
165+
});
166+
167+
if (!apiSearchResults) {
168+
// TODO: add new notice saying no results found?
169+
return;
170+
}
171+
172+
// filter the results
173+
apiSearchResults = apiSearchResults.filter(x => types.contains(x.type));
174+
175+
let selectResults: MediaTypeModel[];
176+
let proceed: boolean;
177+
178+
while (!proceed) {
179+
selectResults = await this.modalHelper.openSelectModal({elements: apiSearchResults}, async (selectModalData) => {
180+
return await this.queryDetails(selectModalData.selected);
181+
});
182+
if (!selectResults) {
183+
return;
184+
}
185+
186+
proceed = await this.modalHelper.openPreviewModal({elements: selectResults}, async (previewModalData) => {
187+
return previewModalData.confirmed;
188+
});
189+
}
155190

191+
await this.createMediaDbNotes(selectResults);
156192
}
157193

158-
/**
159-
* TODO: further refactor: extract it into own method, pass the action (api query) as lambda as well as an options object
160-
*/
161194
async createEntryWithAdvancedSearchModal() {
162195
let apiSearchResults: MediaTypeModel[] = await this.modalHelper.openAdvancedSearchModal({}, async (advancedSearchModalData) => {
163196
return await this.apiManager.query(advancedSearchModalData.query, advancedSearchModalData.apis);

src/modals/MediaDbPreviewModal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class MediaDbPreviewModal extends Modal {
5959
const bottomSettingRow = new Setting(contentEl);
6060
bottomSettingRow.addButton(btn => {
6161
btn.setButtonText('Cancel');
62-
btn.onClick(() => this.closeCallback());
62+
btn.onClick(() => this.close());
6363
btn.buttonEl.addClass('media-db-plugin-button');
6464
this.cancelButton = btn;
6565
});

src/modals/MediaDbSearchModal.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import {ButtonComponent, Modal, Notice, Setting, TextComponent, ToggleComponent} from 'obsidian';
2+
import {MediaTypeModel} from '../models/MediaTypeModel';
3+
import MediaDbPlugin from '../main';
4+
import {
5+
ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS,
6+
AdvancedSearchModalData,
7+
AdvancedSearchModalOptions,
8+
SEARCH_MODAL_DEFAULT_OPTIONS,
9+
SearchModalData,
10+
SearchModalOptions,
11+
} from '../utils/ModalHelper';
12+
import {MEDIA_TYPES} from '../utils/MediaTypeManager';
13+
import {unCamelCase} from '../utils/Utils';
14+
15+
export class MediaDbSearchModal extends Modal {
16+
plugin: MediaDbPlugin;
17+
18+
query: string;
19+
isBusy: boolean;
20+
title: string;
21+
selectedTypes: { name: string, selected: boolean }[];
22+
23+
searchBtn: ButtonComponent;
24+
25+
submitCallback?: (res: SearchModalData) => void;
26+
closeCallback?: (err?: Error) => void;
27+
28+
29+
constructor(plugin: MediaDbPlugin, searchModalOptions: SearchModalOptions) {
30+
searchModalOptions = Object.assign({}, SEARCH_MODAL_DEFAULT_OPTIONS, searchModalOptions);
31+
super(plugin.app);
32+
33+
this.plugin = plugin;
34+
this.selectedTypes = [];
35+
this.title = searchModalOptions.modalTitle;
36+
this.query = searchModalOptions.prefilledSearchString;
37+
38+
for (const mediaType of MEDIA_TYPES) {
39+
this.selectedTypes.push({name: mediaType, selected: searchModalOptions.preselectedTypes.contains(mediaType)});
40+
}
41+
}
42+
43+
setSubmitCallback(submitCallback: (res: SearchModalData) => void): void {
44+
this.submitCallback = submitCallback;
45+
}
46+
47+
setCloseCallback(closeCallback: (err?: Error) => void): void {
48+
this.closeCallback = closeCallback;
49+
}
50+
51+
keyPressCallback(event: KeyboardEvent) {
52+
if (event.key === 'Enter') {
53+
this.search();
54+
}
55+
}
56+
57+
async search(): Promise<MediaTypeModel[]> {
58+
if (!this.query || this.query.length < 3) {
59+
new Notice('MDB | Query too short');
60+
return;
61+
}
62+
63+
const types: string[] = this.selectedTypes.filter(x => x.selected).map(x => x.name);
64+
65+
if (types.length === 0) {
66+
new Notice('MDB | No Type selected');
67+
return;
68+
}
69+
70+
if (!this.isBusy) {
71+
this.isBusy = true;
72+
this.searchBtn.setDisabled(false);
73+
this.searchBtn.setButtonText('Searching...');
74+
75+
this.submitCallback({query: this.query, types: types});
76+
}
77+
}
78+
79+
onOpen() {
80+
const {contentEl} = this;
81+
82+
contentEl.createEl('h2', {text: this.title});
83+
84+
const placeholder = 'Search by title';
85+
const searchComponent = new TextComponent(contentEl);
86+
searchComponent.inputEl.style.width = '100%';
87+
searchComponent.setPlaceholder(placeholder);
88+
searchComponent.setValue(this.query);
89+
searchComponent.onChange(value => (this.query = value));
90+
searchComponent.inputEl.addEventListener('keydown', this.keyPressCallback.bind(this));
91+
92+
contentEl.appendChild(searchComponent.inputEl);
93+
searchComponent.inputEl.focus();
94+
95+
contentEl.createDiv({cls: 'media-db-plugin-spacer'});
96+
contentEl.createEl('h3', {text: 'APIs to search'});
97+
98+
for (const mediaType of MEDIA_TYPES) {
99+
const apiToggleListElementWrapper = contentEl.createEl('div', {cls: 'media-db-plugin-list-wrapper'});
100+
101+
const apiToggleTextWrapper = apiToggleListElementWrapper.createEl('div', {cls: 'media-db-plugin-list-text-wrapper'});
102+
apiToggleTextWrapper.createEl('span', {text: unCamelCase(mediaType), cls: 'media-db-plugin-list-text'});
103+
104+
const apiToggleComponentWrapper = apiToggleListElementWrapper.createEl('div', {cls: 'media-db-plugin-list-toggle'});
105+
106+
const apiToggleComponent = new ToggleComponent(apiToggleComponentWrapper);
107+
apiToggleComponent.setTooltip(unCamelCase(mediaType));
108+
apiToggleComponent.setValue(this.selectedTypes.find(x => x.name === mediaType).selected);
109+
apiToggleComponent.onChange((value) => {
110+
this.selectedTypes.find(x => x.name === mediaType).selected = value;
111+
});
112+
apiToggleComponentWrapper.appendChild(apiToggleComponent.toggleEl);
113+
}
114+
115+
contentEl.createDiv({cls: 'media-db-plugin-spacer'});
116+
117+
new Setting(contentEl)
118+
.addButton(btn => {
119+
btn.setButtonText('Cancel');
120+
btn.onClick(() => this.close());
121+
btn.buttonEl.addClass('media-db-plugin-button');
122+
})
123+
.addButton(btn => {
124+
btn.setButtonText('Ok');
125+
btn.setCta();
126+
btn.onClick(() => {
127+
this.search();
128+
});
129+
btn.buttonEl.addClass('media-db-plugin-button');
130+
this.searchBtn = btn;
131+
});
132+
}
133+
134+
onClose() {
135+
this.closeCallback();
136+
const {contentEl} = this;
137+
contentEl.empty();
138+
}
139+
140+
}

src/modals/MediaDbSearchResultModal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class MediaDbSearchResultModal extends SelectModal<MediaTypeModel> {
6262
}
6363

6464
onClose() {
65+
6566
this.closeCallback();
6667
}
6768
}

src/utils/ModalHelper.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Notice} from 'obsidian';
66
import MediaDbPlugin from '../main';
77
import {MediaDbPreviewModal} from 'src/modals/MediaDbPreviewModal';
88
import {CreateNoteOptions} from './Utils';
9+
import {MediaDbSearchModal} from '../modals/MediaDbSearchModal';
910

1011

1112
export enum ModalResultCode {
@@ -15,6 +16,17 @@ export enum ModalResultCode {
1516
ERROR,
1617
}
1718

19+
/**
20+
* Object containing the data {@link ModalHelper.createSearchModal} returns.
21+
* On {@link ModalResultCode.SUCCESS} this contains {@link SearchModalData}.
22+
* On {@link ModalResultCode.ERROR} this contains a reference to that error.
23+
*/
24+
export interface SearchModalResult {
25+
code: ModalResultCode.SUCCESS | ModalResultCode.CLOSE | ModalResultCode.ERROR,
26+
data?: SearchModalData,
27+
error?: Error,
28+
}
29+
1830
/**
1931
* Object containing the data {@link ModalHelper.createAdvancedSearchModal} returns.
2032
* On {@link ModalResultCode.SUCCESS} this contains {@link AdvancedSearchModalData}.
@@ -59,6 +71,16 @@ export interface PreviewModalResult {
5971
error?: Error,
6072
}
6173

74+
/**
75+
* The data the search modal returns.
76+
* - query: the query string
77+
* - types: the selected APIs
78+
*/
79+
export interface SearchModalData {
80+
query: string,
81+
types: string[],
82+
}
83+
6284
/**
6385
* The data the advanced search modal returns.
6486
* - query: the query string
@@ -95,6 +117,18 @@ export interface PreviewModalData {
95117
confirmed: boolean,
96118
}
97119

120+
/**
121+
* Options for the search modal.
122+
* - modalTitle: the title of the modal
123+
* - preselectedTypes: a list of preselected Types
124+
* - prefilledSearchString: prefilled query
125+
*/
126+
export interface SearchModalOptions {
127+
modalTitle?: string,
128+
preselectedTypes?: string[],
129+
prefilledSearchString?: string,
130+
}
131+
98132
/**
99133
* Options for the advanced search modal.
100134
* - modalTitle: the title of the modal
@@ -144,6 +178,12 @@ export interface PreviewModalOptions {
144178
createNoteOptions?: CreateNoteOptions,
145179
}
146180

181+
export const SEARCH_MODAL_DEFAULT_OPTIONS: SearchModalOptions = {
182+
modalTitle: 'Media DB Search',
183+
preselectedTypes: [],
184+
prefilledSearchString: '',
185+
};
186+
147187
export const ADVANCED_SEARCH_MODAL_DEFAULT_OPTIONS: AdvancedSearchModalOptions = {
148188
modalTitle: 'Media DB Advanced Search',
149189
preselectedAPIs: [],
@@ -180,6 +220,68 @@ export class ModalHelper {
180220
this.plugin = plugin;
181221
}
182222

223+
/**
224+
* Creates an {@link MediaDbSearchModal}, then sets callbacks and awaits them,
225+
* returning either the user input once submitted or nothing once closed.
226+
* The modal needs ot be manually closed by calling `close()` on the modal reference.
227+
*
228+
* @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS}
229+
* @returns the user input or nothing and a reference to the modal.
230+
*/
231+
async createSearchModal(searchModalOptions: SearchModalOptions): Promise<{ searchModalResult: SearchModalResult, searchModal: MediaDbSearchModal }> {
232+
const modal = new MediaDbSearchModal(this.plugin, searchModalOptions);
233+
const res: SearchModalResult = await new Promise((resolve, reject) => {
234+
modal.setSubmitCallback(res => resolve({code: ModalResultCode.SUCCESS, data: res}));
235+
modal.setCloseCallback(err => {
236+
if (err) {
237+
resolve({code: ModalResultCode.ERROR, error: err});
238+
}
239+
resolve({code: ModalResultCode.CLOSE});
240+
});
241+
242+
modal.open();
243+
});
244+
return {searchModalResult: res, searchModal: modal};
245+
}
246+
247+
/**
248+
* Opens an {@link MediaDbSearchModal} and awaits its result,
249+
* then executes the `submitCallback` returning the callbacks result and closing the modal.
250+
*
251+
* @param searchModalOptions the options for the modal, see {@link SEARCH_MODAL_DEFAULT_OPTIONS}
252+
* @param submitCallback the callback that gets executed after the modal has been submitted, but after it has been closed
253+
* @returns the user input or nothing and a reference to the modal.
254+
*/
255+
async openSearchModal(searchModalOptions: SearchModalOptions, submitCallback: (searchModalData: SearchModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
256+
const {searchModalResult, searchModal} = await this.createSearchModal(searchModalOptions);
257+
console.debug(`MDB | searchModal closed with code ${searchModalResult.code}`)
258+
259+
if (searchModalResult.code === ModalResultCode.ERROR) {
260+
// there was an error in the modal itself
261+
console.warn(searchModalResult.error);
262+
new Notice(searchModalResult.error.toString());
263+
searchModal.close();
264+
return undefined;
265+
}
266+
267+
if (searchModalResult.code === ModalResultCode.CLOSE) {
268+
// modal is already being closed
269+
return undefined;
270+
}
271+
272+
try {
273+
let callbackRes: MediaTypeModel[];
274+
callbackRes = await submitCallback(searchModalResult.data);
275+
searchModal.close();
276+
return callbackRes;
277+
} catch (e) {
278+
console.warn(e);
279+
new Notice(e.toString());
280+
searchModal.close();
281+
return undefined;
282+
}
283+
}
284+
183285
/**
184286
* Creates an {@link MediaDbAdvancedSearchModal}, then sets callbacks and awaits them,
185287
* returning either the user input once submitted or nothing once closed.
@@ -214,6 +316,7 @@ export class ModalHelper {
214316
*/
215317
async openAdvancedSearchModal(advancedSearchModalOptions: AdvancedSearchModalOptions, submitCallback: (advancedSearchModalData: AdvancedSearchModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
216318
const {advancedSearchModalResult, advancedSearchModal} = await this.createAdvancedSearchModal(advancedSearchModalOptions);
319+
console.debug(`MDB | advencedSearchModal closed with code ${advancedSearchModalResult.code}`)
217320

218321
if (advancedSearchModalResult.code === ModalResultCode.ERROR) {
219322
// there was an error in the modal itself
@@ -275,6 +378,7 @@ export class ModalHelper {
275378
*/
276379
async openIdSearchModal(idSearchModalOptions: IdSearchModalOptions, submitCallback: (idSearchModalData: IdSearchModalData) => Promise<MediaTypeModel>): Promise<MediaTypeModel> {
277380
const {idSearchModalResult, idSearchModal} = await this.createIdSearchModal(idSearchModalOptions);
381+
console.debug(`MDB | idSearchModal closed with code ${idSearchModalResult.code}`)
278382

279383
if (idSearchModalResult.code === ModalResultCode.ERROR) {
280384
// there was an error in the modal itself
@@ -337,6 +441,7 @@ export class ModalHelper {
337441
*/
338442
async openSelectModal(selectModalOptions: SelectModalOptions, submitCallback: (selectModalData: SelectModalData) => Promise<MediaTypeModel[]>): Promise<MediaTypeModel[]> {
339443
const {selectModalResult, selectModal} = await this.createSelectModal(selectModalOptions);
444+
console.debug(`MDB | selectModal closed with code ${selectModalResult.code}`)
340445

341446
if (selectModalResult.code === ModalResultCode.ERROR) {
342447
// there was an error in the modal itself
@@ -388,6 +493,7 @@ export class ModalHelper {
388493

389494
async openPreviewModal(previewModalOptions: PreviewModalOptions, submitCallback: (previewModalData: PreviewModalData) => Promise<boolean>): Promise<boolean> {
390495
const {previewModalResult, previewModal} = await this.createPreviewModal(previewModalOptions);
496+
console.debug(`MDB | previewModal closed with code ${previewModalResult.code}`)
391497

392498
if (previewModalResult.code === ModalResultCode.ERROR) {
393499
// there was an error in the modal itself

0 commit comments

Comments
 (0)