mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 07:47:41 +09:00 
			
		
		
		
	feat(web): add search filter for camera lens model. (#21792)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				CLI Build / CLI Publish (push) Has been cancelled
				
			
		
			
				
	
				CLI Build / Docker (push) Has been cancelled
				
			
		
			
				
	
				CodeQL / Analyze (javascript) (push) Has been cancelled
				
			
		
			
				
	
				CodeQL / Analyze (python) (push) Has been cancelled
				
			
		
			
				
	
				Docker / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML () (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (-armnn) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (-cuda) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (-openvino) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (-rknn) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (-rocm) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag Server () (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (armnn, linux/arm64, -armnn) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (cpu) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (cuda, linux/amd64, -cuda) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (openvino, linux/amd64, -openvino) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (rknn, linux/arm64, -rknn) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (rocm, linux/amd64, {"linux/amd64": "mich"}, -rocm) (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push Server (push) Has been cancelled
				
			
		
			
				
	
				Docker / Docker Build & Push Server Success (push) Has been cancelled
				
			
		
			
				
	
				Docker / Docker Build & Push ML Success (push) Has been cancelled
				
			
		
			
				
	
				Docs build / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Docs build / Docs Build (push) Has been cancelled
				
			
		
			
				
	
				Zizmor / Zizmor (push) Has been cancelled
				
			
		
			
				
	
				Static Code Analysis / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
				
			
		
			
				
	
				Test / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Test / Test & Lint Server (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test CLI (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test CLI (Windows) (push) Has been cancelled
				
			
		
			
				
	
				Test / Lint Web (push) Has been cancelled
				
			
		
			
				
	
				Test / Test Web (push) Has been cancelled
				
			
		
			
				
	
				Test / Test i18n (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Lint (push) Has been cancelled
				
			
		
			
				
	
				Test / Medium Tests (Server) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Server & CLI) (ubuntu-24.04-arm) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Server & CLI) (ubuntu-latest) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Web) (ubuntu-24.04-arm) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Web) (ubuntu-latest) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests Success (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test Mobile (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test ML (push) Has been cancelled
				
			
		
			
				
	
				Test / .github Files Formatting (push) Has been cancelled
				
			
		
			
				
	
				Test / ShellCheck (push) Has been cancelled
				
			
		
			
				
	
				Test / OpenAPI Clients (push) Has been cancelled
				
			
		
			
				
	
				Test / SQL Schema Checks (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	CLI Build / CLI Publish (push) Has been cancelled
				
			CLI Build / Docker (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CodeQL / Analyze (python) (push) Has been cancelled
				
			Docker / pre-job (push) Has been cancelled
				
			Docker / Re-Tag ML () (push) Has been cancelled
				
			Docker / Re-Tag ML (-armnn) (push) Has been cancelled
				
			Docker / Re-Tag ML (-cuda) (push) Has been cancelled
				
			Docker / Re-Tag ML (-openvino) (push) Has been cancelled
				
			Docker / Re-Tag ML (-rknn) (push) Has been cancelled
				
			Docker / Re-Tag ML (-rocm) (push) Has been cancelled
				
			Docker / Re-Tag Server () (push) Has been cancelled
				
			Docker / Build and Push ML (armnn, linux/arm64, -armnn) (push) Has been cancelled
				
			Docker / Build and Push ML (cpu) (push) Has been cancelled
				
			Docker / Build and Push ML (cuda, linux/amd64, -cuda) (push) Has been cancelled
				
			Docker / Build and Push ML (openvino, linux/amd64, -openvino) (push) Has been cancelled
				
			Docker / Build and Push ML (rknn, linux/arm64, -rknn) (push) Has been cancelled
				
			Docker / Build and Push ML (rocm, linux/amd64, {"linux/amd64": "mich"}, -rocm) (push) Has been cancelled
				
			Docker / Build and Push Server (push) Has been cancelled
				
			Docker / Docker Build & Push Server Success (push) Has been cancelled
				
			Docker / Docker Build & Push ML Success (push) Has been cancelled
				
			Docs build / pre-job (push) Has been cancelled
				
			Docs build / Docs Build (push) Has been cancelled
				
			Zizmor / Zizmor (push) Has been cancelled
				
			Static Code Analysis / pre-job (push) Has been cancelled
				
			Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
				
			Test / pre-job (push) Has been cancelled
				
			Test / Test & Lint Server (push) Has been cancelled
				
			Test / Unit Test CLI (push) Has been cancelled
				
			Test / Unit Test CLI (Windows) (push) Has been cancelled
				
			Test / Lint Web (push) Has been cancelled
				
			Test / Test Web (push) Has been cancelled
				
			Test / Test i18n (push) Has been cancelled
				
			Test / End-to-End Lint (push) Has been cancelled
				
			Test / Medium Tests (Server) (push) Has been cancelled
				
			Test / End-to-End Tests (Server & CLI) (ubuntu-24.04-arm) (push) Has been cancelled
				
			Test / End-to-End Tests (Server & CLI) (ubuntu-latest) (push) Has been cancelled
				
			Test / End-to-End Tests (Web) (ubuntu-24.04-arm) (push) Has been cancelled
				
			Test / End-to-End Tests (Web) (ubuntu-latest) (push) Has been cancelled
				
			Test / End-to-End Tests Success (push) Has been cancelled
				
			Test / Unit Test Mobile (push) Has been cancelled
				
			Test / Unit Test ML (push) Has been cancelled
				
			Test / .github Files Formatting (push) Has been cancelled
				
			Test / ShellCheck (push) Has been cancelled
				
			Test / OpenAPI Clients (push) Has been cancelled
				
			Test / SQL Schema Checks (push) Has been cancelled
				
			This commit is contained in:
		| @@ -1710,6 +1710,7 @@ | ||||
|   "search_by_description_example": "Hiking day in Sapa", | ||||
|   "search_by_filename": "Search by file name or extension", | ||||
|   "search_by_filename_example": "i.e. IMG_1234.JPG or PNG", | ||||
|   "search_camera_lens_model": "Search lens model...", | ||||
|   "search_camera_make": "Search camera make...", | ||||
|   "search_camera_model": "Search camera model...", | ||||
|   "search_city": "Search city...", | ||||
|   | ||||
							
								
								
									
										13
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								mobile/openapi/lib/api/search_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -123,12 +123,14 @@ class SearchApi { | ||||
|   /// * [bool] includeNull: | ||||
|   ///   This property was added in v111.0.0 | ||||
|   /// | ||||
|   /// * [String] lensModel: | ||||
|   /// | ||||
|   /// * [String] make: | ||||
|   /// | ||||
|   /// * [String] model: | ||||
|   /// | ||||
|   /// * [String] state: | ||||
|   Future<Response> getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? make, String? model, String? state, }) async { | ||||
|   Future<Response> getSearchSuggestionsWithHttpInfo(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final apiPath = r'/search/suggestions'; | ||||
| 
 | ||||
| @@ -145,6 +147,9 @@ class SearchApi { | ||||
|     if (includeNull != null) { | ||||
|       queryParams.addAll(_queryParams('', 'includeNull', includeNull)); | ||||
|     } | ||||
|     if (lensModel != null) { | ||||
|       queryParams.addAll(_queryParams('', 'lensModel', lensModel)); | ||||
|     } | ||||
|     if (make != null) { | ||||
|       queryParams.addAll(_queryParams('', 'make', make)); | ||||
|     } | ||||
| @@ -181,13 +186,15 @@ class SearchApi { | ||||
|   /// * [bool] includeNull: | ||||
|   ///   This property was added in v111.0.0 | ||||
|   /// | ||||
|   /// * [String] lensModel: | ||||
|   /// | ||||
|   /// * [String] make: | ||||
|   /// | ||||
|   /// * [String] model: | ||||
|   /// | ||||
|   /// * [String] state: | ||||
|   Future<List<String>?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? make, String? model, String? state, }) async { | ||||
|     final response = await getSearchSuggestionsWithHttpInfo(type,  country: country, includeNull: includeNull, make: make, model: model, state: state, ); | ||||
|   Future<List<String>?> getSearchSuggestions(SearchSuggestionType type, { String? country, bool? includeNull, String? lensModel, String? make, String? model, String? state, }) async { | ||||
|     final response = await getSearchSuggestionsWithHttpInfo(type,  country: country, includeNull: includeNull, lensModel: lensModel, make: make, model: model, state: state, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ class SearchSuggestionType { | ||||
|   static const city = SearchSuggestionType._(r'city'); | ||||
|   static const cameraMake = SearchSuggestionType._(r'camera-make'); | ||||
|   static const cameraModel = SearchSuggestionType._(r'camera-model'); | ||||
|   static const cameraLensModel = SearchSuggestionType._(r'camera-lens-model'); | ||||
| 
 | ||||
|   /// List of all possible values in this [enum][SearchSuggestionType]. | ||||
|   static const values = <SearchSuggestionType>[ | ||||
| @@ -36,6 +37,7 @@ class SearchSuggestionType { | ||||
|     city, | ||||
|     cameraMake, | ||||
|     cameraModel, | ||||
|     cameraLensModel, | ||||
|   ]; | ||||
| 
 | ||||
|   static SearchSuggestionType? fromJson(dynamic value) => SearchSuggestionTypeTypeTransformer().decode(value); | ||||
| @@ -79,6 +81,7 @@ class SearchSuggestionTypeTypeTransformer { | ||||
|         case r'city': return SearchSuggestionType.city; | ||||
|         case r'camera-make': return SearchSuggestionType.cameraMake; | ||||
|         case r'camera-model': return SearchSuggestionType.cameraModel; | ||||
|         case r'camera-lens-model': return SearchSuggestionType.cameraLensModel; | ||||
|         default: | ||||
|           if (!allowNull) { | ||||
|             throw ArgumentError('Unknown enum value to decode: $data'); | ||||
|   | ||||
| @@ -6458,6 +6458,14 @@ | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "lensModel", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "make", | ||||
|             "required": false, | ||||
| @@ -13941,7 +13949,8 @@ | ||||
|           "state", | ||||
|           "city", | ||||
|           "camera-make", | ||||
|           "camera-model" | ||||
|           "camera-model", | ||||
|           "camera-lens-model" | ||||
|         ], | ||||
|         "type": "string" | ||||
|       }, | ||||
|   | ||||
| @@ -3566,9 +3566,10 @@ export function searchAssetStatistics({ statisticsSearchDto }: { | ||||
| /** | ||||
|  * This endpoint requires the `asset.read` permission. | ||||
|  */ | ||||
| export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: { | ||||
| export function getSearchSuggestions({ country, includeNull, lensModel, make, model, state, $type }: { | ||||
|     country?: string; | ||||
|     includeNull?: boolean; | ||||
|     lensModel?: string; | ||||
|     make?: string; | ||||
|     model?: string; | ||||
|     state?: string; | ||||
| @@ -3580,6 +3581,7 @@ export function getSearchSuggestions({ country, includeNull, make, model, state, | ||||
|     }>(`/search/suggestions${QS.query(QS.explode({ | ||||
|         country, | ||||
|         includeNull, | ||||
|         lensModel, | ||||
|         make, | ||||
|         model, | ||||
|         state, | ||||
| @@ -4919,7 +4921,8 @@ export enum SearchSuggestionType { | ||||
|     State = "state", | ||||
|     City = "city", | ||||
|     CameraMake = "camera-make", | ||||
|     CameraModel = "camera-model" | ||||
|     CameraModel = "camera-model", | ||||
|     CameraLensModel = "camera-lens-model" | ||||
| } | ||||
| export enum SharedLinkType { | ||||
|     Album = "ALBUM", | ||||
|   | ||||
| @@ -249,6 +249,7 @@ export enum SearchSuggestionType { | ||||
|   CITY = 'city', | ||||
|   CAMERA_MAKE = 'camera-make', | ||||
|   CAMERA_MODEL = 'camera-model', | ||||
|   CAMERA_LENS_MODEL = 'camera-lens-model', | ||||
| } | ||||
|  | ||||
| export class SearchSuggestionRequestDto { | ||||
| @@ -271,6 +272,10 @@ export class SearchSuggestionRequestDto { | ||||
|   @Optional() | ||||
|   model?: string; | ||||
|  | ||||
|   @IsString() | ||||
|   @Optional() | ||||
|   lensModel?: string; | ||||
|  | ||||
|   @ValidateBoolean({ optional: true }) | ||||
|   @PropertyLifecycle({ addedAt: 'v111.0.0' }) | ||||
|   includeNull?: boolean; | ||||
|   | ||||
| @@ -290,3 +290,15 @@ where | ||||
|   and "visibility" = $2 | ||||
|   and "deletedAt" is null | ||||
|   and "model" is not null | ||||
|  | ||||
| -- SearchRepository.getCameraLensModels | ||||
| select distinct | ||||
|   on ("lensModel") "lensModel" | ||||
| from | ||||
|   "asset_exif" | ||||
|   inner join "asset" on "asset"."id" = "asset_exif"."assetId" | ||||
| where | ||||
|   "ownerId" = any ($1::uuid[]) | ||||
|   and "visibility" = $2 | ||||
|   and "deletedAt" is null | ||||
|   and "lensModel" is not null | ||||
|   | ||||
| @@ -160,10 +160,17 @@ export interface GetCitiesOptions extends GetStatesOptions { | ||||
|  | ||||
| export interface GetCameraModelsOptions { | ||||
|   make?: string; | ||||
|   lensModel?: string; | ||||
| } | ||||
|  | ||||
| export interface GetCameraMakesOptions { | ||||
|   model?: string; | ||||
|   lensModel?: string; | ||||
| } | ||||
|  | ||||
| export interface GetCameraLensModelsOptions { | ||||
|   make?: string; | ||||
|   model?: string; | ||||
| } | ||||
|  | ||||
| @Injectable() | ||||
| @@ -457,25 +464,40 @@ export class SearchRepository { | ||||
|     return res.map((row) => row.city!); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) | ||||
|   async getCameraMakes(userIds: string[], { model }: GetCameraMakesOptions): Promise<string[]> { | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) | ||||
|   async getCameraMakes(userIds: string[], { model, lensModel }: GetCameraMakesOptions): Promise<string[]> { | ||||
|     const res = await this.getExifField('make', userIds) | ||||
|       .$if(!!model, (qb) => qb.where('model', '=', model!)) | ||||
|       .$if(!!lensModel, (qb) => qb.where('lensModel', '=', lensModel!)) | ||||
|       .execute(); | ||||
|  | ||||
|     return res.map((row) => row.make!); | ||||
|   } | ||||
|  | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) | ||||
|   async getCameraModels(userIds: string[], { make }: GetCameraModelsOptions): Promise<string[]> { | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) | ||||
|   async getCameraModels(userIds: string[], { make, lensModel }: GetCameraModelsOptions): Promise<string[]> { | ||||
|     const res = await this.getExifField('model', userIds) | ||||
|       .$if(!!make, (qb) => qb.where('make', '=', make!)) | ||||
|       .$if(!!lensModel, (qb) => qb.where('lensModel', '=', lensModel!)) | ||||
|       .execute(); | ||||
|  | ||||
|     return res.map((row) => row.model!); | ||||
|   } | ||||
|  | ||||
|   private getExifField<K extends 'city' | 'state' | 'country' | 'make' | 'model'>(field: K, userIds: string[]) { | ||||
|   @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) | ||||
|   async getCameraLensModels(userIds: string[], { make, model }: GetCameraLensModelsOptions): Promise<string[]> { | ||||
|     const res = await this.getExifField('lensModel', userIds) | ||||
|       .$if(!!make, (qb) => qb.where('make', '=', make!)) | ||||
|       .$if(!!model, (qb) => qb.where('model', '=', model!)) | ||||
|       .execute(); | ||||
|  | ||||
|     return res.map((row) => row.lensModel!); | ||||
|   } | ||||
|  | ||||
|   private getExifField<K extends 'city' | 'state' | 'country' | 'make' | 'model' | 'lensModel'>( | ||||
|     field: K, | ||||
|     userIds: string[], | ||||
|   ) { | ||||
|     return this.db | ||||
|       .selectFrom('asset_exif') | ||||
|       .select(field) | ||||
|   | ||||
| @@ -179,6 +179,26 @@ describe(SearchService.name, () => { | ||||
|       ).resolves.toEqual(['Fujifilm X100VI', null]); | ||||
|       expect(mocks.search.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); | ||||
|     }); | ||||
|  | ||||
|     it('should return search suggestions for camera lens model', async () => { | ||||
|       mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']); | ||||
|       mocks.partner.getAll.mockResolvedValue([]); | ||||
|  | ||||
|       await expect( | ||||
|         sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_LENS_MODEL }), | ||||
|       ).resolves.toEqual(['10-24mm']); | ||||
|       expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); | ||||
|     }); | ||||
|  | ||||
|     it('should return search suggestions for camera lens model (including null)', async () => { | ||||
|       mocks.search.getCameraLensModels.mockResolvedValue(['10-24mm']); | ||||
|       mocks.partner.getAll.mockResolvedValue([]); | ||||
|  | ||||
|       await expect( | ||||
|         sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_LENS_MODEL }), | ||||
|       ).resolves.toEqual(['10-24mm', null]); | ||||
|       expect(mocks.search.getCameraLensModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('searchSmart', () => { | ||||
|   | ||||
| @@ -177,6 +177,9 @@ export class SearchService extends BaseService { | ||||
|       case SearchSuggestionType.CAMERA_MODEL: { | ||||
|         return this.searchRepository.getCameraModels(userIds, dto); | ||||
|       } | ||||
|       case SearchSuggestionType.CAMERA_LENS_MODEL: { | ||||
|         return this.searchRepository.getCameraLensModels(userIds, dto); | ||||
|       } | ||||
|       default: { | ||||
|         return Promise.resolve([]); | ||||
|       } | ||||
|   | ||||
| @@ -2,12 +2,11 @@ | ||||
|   export interface SearchCameraFilter { | ||||
|     make?: string; | ||||
|     model?: string; | ||||
|     lensModel?: string; | ||||
|   } | ||||
| </script> | ||||
|  | ||||
| <script lang="ts"> | ||||
|   import { run } from 'svelte/legacy'; | ||||
|  | ||||
|   import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte'; | ||||
|   import { handlePromiseError } from '$lib/utils'; | ||||
|   import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk'; | ||||
| @@ -21,6 +20,7 @@ | ||||
|  | ||||
|   let makes: string[] = $state([]); | ||||
|   let models: string[] = $state([]); | ||||
|   let lensModels: string[] = $state([]); | ||||
|  | ||||
|   async function updateMakes() { | ||||
|     const results: Array<string | null> = await getSearchSuggestions({ | ||||
| @@ -48,14 +48,35 @@ | ||||
|       filters.model = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async function updateLensModels(make?: string, model?: string) { | ||||
|     const results: Array<string | null> = await getSearchSuggestions({ | ||||
|       $type: SearchSuggestionType.CameraLensModel, | ||||
|       make, | ||||
|       model, | ||||
|       includeNull: true, | ||||
|     }); | ||||
|  | ||||
|     lensModels = results.map((result) => result ?? ''); | ||||
|  | ||||
|     if (filters.lensModel && !lensModels.includes(filters.lensModel)) { | ||||
|       filters.lensModel = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   let makeFilter = $derived(filters.make); | ||||
|   let modelFilter = $derived(filters.model); | ||||
|   run(() => { | ||||
|   let lensModelFilter = $derived(filters.lensModel); | ||||
|  | ||||
|   $effect(() => { | ||||
|     handlePromiseError(updateMakes()); | ||||
|   }); | ||||
|   run(() => { | ||||
|   $effect(() => { | ||||
|     handlePromiseError(updateModels(makeFilter)); | ||||
|   }); | ||||
|   $effect(() => { | ||||
|     handlePromiseError(updateLensModels(makeFilter, modelFilter)); | ||||
|   }); | ||||
| </script> | ||||
|  | ||||
| <div id="camera-selection"> | ||||
| @@ -81,5 +102,15 @@ | ||||
|         selectedOption={asSelectedOption(modelFilter)} | ||||
|       /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="w-full"> | ||||
|       <Combobox | ||||
|         label={$t('lens_model')} | ||||
|         onSelect={(option) => (filters.lensModel = option?.value)} | ||||
|         options={asComboboxOptions(lensModels)} | ||||
|         placeholder={$t('search_camera_lens_model')} | ||||
|         selectedOption={asSelectedOption(lensModelFilter)} | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -90,6 +90,7 @@ | ||||
|     camera: { | ||||
|       make: withNullAsUndefined(searchQuery.make), | ||||
|       model: withNullAsUndefined(searchQuery.model), | ||||
|       lensModel: withNullAsUndefined(searchQuery.lensModel), | ||||
|     }, | ||||
|     date: { | ||||
|       takenAfter: searchQuery.takenAfter ? toStartOfDayDate(searchQuery.takenAfter) : undefined, | ||||
| @@ -147,6 +148,7 @@ | ||||
|       city: filter.location.city, | ||||
|       make: filter.camera.make, | ||||
|       model: filter.camera.model, | ||||
|       lensModel: filter.camera.lensModel, | ||||
|       takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined, | ||||
|       takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('day').toISO() || undefined, | ||||
|       visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user