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 { listNavigation } from '$lib/actions/list-navigation'; | ||||
|   import { t } from 'svelte-i18n'; | ||||
|   import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte'; | ||||
| 
 | ||||
|   export let asset: AssetResponseDto | undefined = undefined; | ||||
| 
 | ||||
| @ -34,9 +35,9 @@ | ||||
|     confirm: Point; | ||||
|   }>(); | ||||
| 
 | ||||
|   $: lat = asset?.exifInfo?.latitude || 0; | ||||
|   $: lng = asset?.exifInfo?.longitude || 0; | ||||
|   $: zoom = lat && lng ? 15 : 1; | ||||
|   $: lat = asset?.exifInfo?.latitude ?? undefined; | ||||
|   $: lng = asset?.exifInfo?.longitude ?? undefined; | ||||
|   $: zoom = lat !== undefined && lng !== undefined ? 15 : 1; | ||||
| 
 | ||||
|   $: { | ||||
|     if (places) { | ||||
| @ -148,7 +149,7 @@ | ||||
|         {/if} | ||||
|       </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"> | ||||
|       {#await import('../shared-components/map/map.svelte')} | ||||
|         {#await delay(timeToLoadTheMap) then} | ||||
| @ -157,10 +158,9 @@ | ||||
|             <LoadingSpinner /> | ||||
|           </div> | ||||
|         {/await} | ||||
|       {:then component} | ||||
|         <svelte:component | ||||
|           this={component.default} | ||||
|           mapMarkers={lat && lng && asset | ||||
|       {:then { default: Map }} | ||||
|         <Map | ||||
|           mapMarkers={lat !== undefined && lng !== undefined && asset | ||||
|             ? [ | ||||
|                 { | ||||
|                   id: asset.id, | ||||
| @ -181,5 +181,16 @@ | ||||
|         /> | ||||
|       {/await} | ||||
|     </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> | ||||
| </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", | ||||
|   "last_seen": "Last seen", | ||||
|   "latest_version": "Latest Version", | ||||
|   "latitude": "Latitude", | ||||
|   "leave": "Leave", | ||||
|   "let_others_respond": "Let others respond", | ||||
|   "level": "Level", | ||||
| @ -786,6 +787,7 @@ | ||||
|   "login_has_been_disabled": "Login has been disabled.", | ||||
|   "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?", | ||||
|   "longitude": "Longitude", | ||||
|   "look": "Look", | ||||
|   "loop_videos": "Loop videos", | ||||
|   "loop_videos_description": "Enable to automatically loop a video in the detail viewer.", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user