mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04:00 
			
		
		
		
	feat(web): coordinate input for asset location (#11291)
This commit is contained in:
		
							parent
							
								
									8725656fd2
								
							
						
					
					
						commit
						7d3db11a5c
					
				| @ -0,0 +1,50 @@ | |||||||
|  | import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte'; | ||||||
|  | import { act, render, type RenderResult } from '@testing-library/svelte'; | ||||||
|  | import userEvent from '@testing-library/user-event'; | ||||||
|  | 
 | ||||||
|  | describe('NumberRangeInput component', () => { | ||||||
|  |   const user = userEvent.setup(); | ||||||
|  |   let sut: RenderResult<NumberRangeInput>; | ||||||
|  |   let input: HTMLInputElement; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     sut = render(NumberRangeInput, { id: '', min: -90, max: 90, onInput: () => {} }); | ||||||
|  |     input = sut.getByRole('spinbutton') as HTMLInputElement; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('updates value', async () => { | ||||||
|  |     expect(input.value).toBe(''); | ||||||
|  |     await act(() => sut.component.$set({ value: 10 })); | ||||||
|  |     expect(input.value).toBe('10'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('restricts minimum value', async () => { | ||||||
|  |     await user.type(input, '-91'); | ||||||
|  |     expect(input.value).toBe('-90'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('restricts maximum value', async () => { | ||||||
|  |     await user.type(input, '09990'); | ||||||
|  |     expect(input.value).toBe('90'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('allows entering negative numbers', async () => { | ||||||
|  |     await user.type(input, '-10'); | ||||||
|  |     expect(input.value).toBe('-10'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('allows entering zero', async () => { | ||||||
|  |     await user.type(input, '0'); | ||||||
|  |     expect(input.value).toBe('0'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('allows entering decimal numbers', async () => { | ||||||
|  |     await user.type(input, '-0.09001'); | ||||||
|  |     expect(input.value).toBe('-0.09001'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('ignores text input', async () => { | ||||||
|  |     await user.type(input, 'test'); | ||||||
|  |     expect(input.value).toBe(''); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -12,6 +12,7 @@ | |||||||
|   import SearchBar from '../elements/search-bar.svelte'; |   import SearchBar from '../elements/search-bar.svelte'; | ||||||
|   import { listNavigation } from '$lib/actions/list-navigation'; |   import { listNavigation } from '$lib/actions/list-navigation'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|  |   import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte'; | ||||||
| 
 | 
 | ||||||
|   export let asset: AssetResponseDto | undefined = undefined; |   export let asset: AssetResponseDto | undefined = undefined; | ||||||
| 
 | 
 | ||||||
| @ -34,9 +35,9 @@ | |||||||
|     confirm: Point; |     confirm: Point; | ||||||
|   }>(); |   }>(); | ||||||
| 
 | 
 | ||||||
|   $: lat = asset?.exifInfo?.latitude || 0; |   $: lat = asset?.exifInfo?.latitude ?? undefined; | ||||||
|   $: lng = asset?.exifInfo?.longitude || 0; |   $: lng = asset?.exifInfo?.longitude ?? undefined; | ||||||
|   $: zoom = lat && lng ? 15 : 1; |   $: zoom = lat !== undefined && lng !== undefined ? 15 : 1; | ||||||
| 
 | 
 | ||||||
|   $: { |   $: { | ||||||
|     if (places) { |     if (places) { | ||||||
| @ -148,7 +149,7 @@ | |||||||
|         {/if} |         {/if} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|     <label for="datetime">{$t('pick_a_location')}</label> |     <span>{$t('pick_a_location')}</span> | ||||||
|     <div class="h-[500px] min-h-[300px] w-full"> |     <div class="h-[500px] min-h-[300px] w-full"> | ||||||
|       {#await import('../shared-components/map/map.svelte')} |       {#await import('../shared-components/map/map.svelte')} | ||||||
|         {#await delay(timeToLoadTheMap) then} |         {#await delay(timeToLoadTheMap) then} | ||||||
| @ -157,10 +158,9 @@ | |||||||
|             <LoadingSpinner /> |             <LoadingSpinner /> | ||||||
|           </div> |           </div> | ||||||
|         {/await} |         {/await} | ||||||
|       {:then component} |       {:then { default: Map }} | ||||||
|         <svelte:component |         <Map | ||||||
|           this={component.default} |           mapMarkers={lat !== undefined && lng !== undefined && asset | ||||||
|           mapMarkers={lat && lng && asset |  | ||||||
|             ? [ |             ? [ | ||||||
|                 { |                 { | ||||||
|                   id: asset.id, |                   id: asset.id, | ||||||
| @ -181,5 +181,16 @@ | |||||||
|         /> |         /> | ||||||
|       {/await} |       {/await} | ||||||
|     </div> |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="grid sm:grid-cols-2 gap-4 text-sm text-left mt-4"> | ||||||
|  |       <CoordinatesInput | ||||||
|  |         lat={point ? point.lat : lat} | ||||||
|  |         lng={point ? point.lng : lng} | ||||||
|  |         onUpdate={(lat, lng) => { | ||||||
|  |           point = { lat, lng }; | ||||||
|  |           addClipMapMarker(lng, lat); | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </ConfirmDialog> | </ConfirmDialog> | ||||||
|  | |||||||
| @ -0,0 +1,27 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import NumberRangeInput from '$lib/components/shared-components/number-range-input.svelte'; | ||||||
|  |   import { generateId } from '$lib/utils/generate-id'; | ||||||
|  |   import { t } from 'svelte-i18n'; | ||||||
|  | 
 | ||||||
|  |   export let lat: number | null | undefined = undefined; | ||||||
|  |   export let lng: number | null | undefined = undefined; | ||||||
|  |   export let onUpdate: (lat: number, lng: number) => void; | ||||||
|  | 
 | ||||||
|  |   const id = generateId(); | ||||||
|  | 
 | ||||||
|  |   const onInput = () => { | ||||||
|  |     if (lat != null && lng != null) { | ||||||
|  |       onUpdate(lat, lng); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div> | ||||||
|  |   <label class="immich-form-label" for="latitude-input-{id}">{$t('latitude')}</label> | ||||||
|  |   <NumberRangeInput id="latitude-input-{id}" min={-90} max={90} {onInput} bind:value={lat} /> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <div> | ||||||
|  |   <label class="immich-form-label" for="longitude-input-{id}">{$t('longitude')}</label> | ||||||
|  |   <NumberRangeInput id="longitude-input-{id}" min={-180} max={180} {onInput} bind:value={lng} /> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,28 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import { clamp } from 'lodash-es'; | ||||||
|  | 
 | ||||||
|  |   export let id: string; | ||||||
|  |   export let min: number; | ||||||
|  |   export let max: number; | ||||||
|  |   export let step: number | string = 'any'; | ||||||
|  |   export let required = true; | ||||||
|  |   export let value: number | null = null; | ||||||
|  |   export let onInput: (value: number | null) => void; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <input | ||||||
|  |   type="number" | ||||||
|  |   class="immich-form-input w-full" | ||||||
|  |   {id} | ||||||
|  |   {min} | ||||||
|  |   {max} | ||||||
|  |   {step} | ||||||
|  |   {required} | ||||||
|  |   bind:value | ||||||
|  |   on:input={() => { | ||||||
|  |     if (value !== null && (value < min || value > max)) { | ||||||
|  |       value = clamp(value, min, max); | ||||||
|  |     } | ||||||
|  |     onInput(value); | ||||||
|  |   }} | ||||||
|  | /> | ||||||
| @ -740,6 +740,7 @@ | |||||||
|   "language_setting_description": "Select your preferred language", |   "language_setting_description": "Select your preferred language", | ||||||
|   "last_seen": "Last seen", |   "last_seen": "Last seen", | ||||||
|   "latest_version": "Latest Version", |   "latest_version": "Latest Version", | ||||||
|  |   "latitude": "Latitude", | ||||||
|   "leave": "Leave", |   "leave": "Leave", | ||||||
|   "let_others_respond": "Let others respond", |   "let_others_respond": "Let others respond", | ||||||
|   "level": "Level", |   "level": "Level", | ||||||
| @ -786,6 +787,7 @@ | |||||||
|   "login_has_been_disabled": "Login has been disabled.", |   "login_has_been_disabled": "Login has been disabled.", | ||||||
|   "logout_all_device_confirmation": "Are you sure you want to log out all devices?", |   "logout_all_device_confirmation": "Are you sure you want to log out all devices?", | ||||||
|   "logout_this_device_confirmation": "Are you sure you want to log out this device?", |   "logout_this_device_confirmation": "Are you sure you want to log out this device?", | ||||||
|  |   "longitude": "Longitude", | ||||||
|   "look": "Look", |   "look": "Look", | ||||||
|   "loop_videos": "Loop videos", |   "loop_videos": "Loop videos", | ||||||
|   "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", |   "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user