mirror of
https://github.com/immich-app/immich.git
synced 2025-03-01 15:11:21 +01:00
feat(web): coordinate input for asset location (#11291)
This commit is contained in:
parent
8725656fd2
commit
7d3db11a5c
5 changed files with 126 additions and 8 deletions
|
@ -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…
Add table
Reference in a new issue