From 547bcdf146070020f574b284015a4d1f7c01da57 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 3 Feb 2025 17:30:11 +0100 Subject: [PATCH 1/6] Exclude failed syncs from git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d53539bf9..d92bc4e85 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ backend/FwLite/FwLiteShared/wwwroot/viewer *.csproj.user *.log +failedSyncs/ From 3dafe30594184d7b16ffd20d98bd11f45c60afcc Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 3 Feb 2025 17:30:45 +0100 Subject: [PATCH 2/6] Update example semantic domains to use canonical Guids, so they are acceptably predefined --- backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs index a9bc4984a..02e57b008 100644 --- a/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs +++ b/backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs @@ -20,13 +20,13 @@ internal static async Task PredefinedSemanticDomains(DataModel dataModel, Guid c //todo load from xml instead of hardcoding and use real IDs await dataModel.AddChanges(clientId, [ - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d0"), new MultiString() { { "en", "Universe, Creation" } }, "1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d1"), new MultiString() { { "en", "Sky" } }, "1.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d2"), new MultiString() { { "en", "World" } }, "1.2", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d3"), new MultiString() { { "en", "Person" } }, "2", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d4"), new MultiString() { { "en", "Body" } }, "2.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", true), - new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", true), + new CreateSemanticDomainChange(new Guid("63403699-07c1-43f3-a47c-069d6e4316e5"), new MultiString() { { "en", "Universe, Creation" } }, "1", true), + new CreateSemanticDomainChange(new Guid("999581c4-1611-4acb-ae1b-5e6c1dfe6f0c"), new MultiString() { { "en", "Sky" } }, "1.1", true), + new CreateSemanticDomainChange(new Guid("dc1a2c6f-1b32-4631-8823-36dacc8cb7bb"), new MultiString() { { "en", "World" } }, "1.2", true), + new CreateSemanticDomainChange(new Guid("1bd42665-0610-4442-8d8d-7c666fee3a6d"), new MultiString() { { "en", "Person" } }, "2", true), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d4"), new MultiString() { { "en", "Body" } }, "2.1", false), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d5"), new MultiString() { { "en", "Head" } }, "2.1.1", false), + new CreateSemanticDomainChange(new Guid("46e4fe08-ffa0-4c8b-bf88-2c56f38904d6"), new MultiString() { { "en", "Eye" } }, "2.1.1.1", false), ], new Guid("023faebb-711b-4d2f-a14f-a15621fc66bc")); } From 6e3d5e6a3204246b53c942c377ada8ff8807775b Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 3 Feb 2025 17:33:41 +0100 Subject: [PATCH 3/6] Don't hide fields after first render --- frontend/viewer/src/lib/Editor.svelte | 16 +++++++----- .../ComplexFormComponents.svelte | 8 +++--- .../field-editors/ComplexForms.svelte | 8 +++--- .../field-editors/MultiFieldEditor.svelte | 10 ++++--- .../field-editors/MultiOptionEditor.svelte | 9 +++++-- .../field-editors/SingleFieldEditor.svelte | 8 ++++-- .../field-editors/SingleOptionEditor.svelte | 9 +++++-- .../inputs/CrdtMultiOptionField.svelte | 5 +++- .../inputs/CrdtOptionField.svelte | 5 +++- .../entry-editor/inputs/CrdtTextField.svelte | 5 +++- .../viewer/src/lib/history/HistoryView.svelte | 26 ++++++++++--------- frontend/viewer/src/lib/utils.ts | 14 ++++++++++ 12 files changed, 86 insertions(+), 37 deletions(-) diff --git a/frontend/viewer/src/lib/Editor.svelte b/frontend/viewer/src/lib/Editor.svelte index c3503dc05..ae64de254 100644 --- a/frontend/viewer/src/lib/Editor.svelte +++ b/frontend/viewer/src/lib/Editor.svelte @@ -53,16 +53,18 @@ } -
- onChange(e.detail)} - on:delete={e => onDelete(e.detail)} - entry={entry} - {readonly}/> +
+ {#key entry.id} + onChange(e.detail)} + on:delete={e => onDelete(e.detail)} + entry={entry} + {readonly}/> + {/key}
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte index 09c7be742..9c6606342 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexFormComponents.svelte @@ -2,7 +2,7 @@ import FieldTitle from '../FieldTitle.svelte'; import { useCurrentView } from '$lib/services/view-service'; import EntryOrSensePicker, { type EntrySenseSelection } from '../EntryOrSensePicker.svelte'; - import { randomId } from '$lib/utils'; + import { makeHasHadValueTracker, randomId } from '$lib/utils'; import { createEventDispatcher } from 'svelte'; import EntryOrSenseItemList from '../EntryOrSenseItemList.svelte'; import { Button } from 'svelte-ux'; @@ -22,7 +22,9 @@ let currentView = useCurrentView(); const writingSystemService = useWritingSystemService(); - $: empty = !value?.length; + let hasHadValueTracker = makeHasHadValueTracker(); + let hasHadValue = hasHadValueTracker.store; + $: hasHadValueTracker.pushAndGet(value?.length); let openPicker = false; @@ -54,7 +56,7 @@
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte index c6acacba4..d2db47a1a 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/ComplexForms.svelte @@ -2,7 +2,7 @@ import FieldTitle from '../FieldTitle.svelte'; import { useCurrentView } from '$lib/services/view-service'; import EntryOrSensePicker, { type EntrySenseSelection } from '../EntryOrSensePicker.svelte'; - import { randomId } from '$lib/utils'; + import { makeHasHadValueTracker, randomId } from '$lib/utils'; import { createEventDispatcher } from 'svelte'; import EntryOrSenseItemList from '../EntryOrSenseItemList.svelte'; import { Button } from 'svelte-ux'; @@ -22,7 +22,9 @@ let currentView = useCurrentView(); let writingSystemService = useWritingSystemService(); - $: empty = !value?.length; + let hasHadValueTracker = makeHasHadValueTracker(); + let hasHadValue = hasHadValueTracker.store; + $: hasHadValueTracker.pushAndGet(value?.length); let openPicker = false; @@ -48,7 +50,7 @@
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/MultiFieldEditor.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/MultiFieldEditor.svelte index 1f0d24ee8..d9f93898d 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/MultiFieldEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/MultiFieldEditor.svelte @@ -6,6 +6,7 @@ import type {WritingSystemSelection} from '../../config-types'; import {useCurrentView} from '../../services/view-service'; import {useWritingSystemService} from '../../writing-system-service'; + import {makeHasHadValueTracker} from '$lib/utils'; const dispatch = createEventDispatcher<{ change: { value: IMultiString }; @@ -24,12 +25,15 @@ let currentView = useCurrentView(); $: writingSystems = writingSystemService.pickWritingSystems(wsType); - $: empty = !writingSystems.some((ws) => value[ws.wsId] || unsavedChanges[ws.wsId]); - $: collapse = empty && writingSystems.length > 1; + + let hasHadValueTracker = makeHasHadValueTracker(); + let hasHadValue = hasHadValueTracker.store; + $: hasHadValueTracker.pushAndGet(writingSystems.some((ws) => value[ws.wsId] || unsavedChanges[ws.wsId])); + $: collapse = !$hasHadValue && writingSystems.length > 1; $: hide = !$currentView.fields[id].show; -
+
{#each writingSystems as ws, idx (ws.wsId)} diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/MultiOptionEditor.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/MultiOptionEditor.svelte index 4d779b364..fbfbc3932 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/MultiOptionEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/MultiOptionEditor.svelte @@ -1,4 +1,6 @@ {#key options} {/key} -
+
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/SingleFieldEditor.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/SingleFieldEditor.svelte index 1d9f8d3b2..7a0633851 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/SingleFieldEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/SingleFieldEditor.svelte @@ -4,6 +4,7 @@ import CrdtTextField from '../inputs/CrdtTextField.svelte'; import {useCurrentView} from '../../services/view-service'; import {useWritingSystemService} from '../../writing-system-service'; + import {makeHasHadValueTracker} from '$lib/utils'; export let id: string; export let name: string | undefined = undefined; @@ -15,10 +16,13 @@ const writingSystemService = useWritingSystemService(); $: [ws] = writingSystemService.pickWritingSystems(wsType); - $: empty = !value; + + let hasHadValueTracker = makeHasHadValueTracker(); + let hasHadValue = hasHadValueTracker.store; + $: hasHadValueTracker.pushAndGet(value); -
+
diff --git a/frontend/viewer/src/lib/entry-editor/field-editors/SingleOptionEditor.svelte b/frontend/viewer/src/lib/entry-editor/field-editors/SingleOptionEditor.svelte index 0db20b307..1f069620e 100644 --- a/frontend/viewer/src/lib/entry-editor/field-editors/SingleOptionEditor.svelte +++ b/frontend/viewer/src/lib/entry-editor/field-editors/SingleOptionEditor.svelte @@ -1,4 +1,6 @@ {#key options} {/key} -
+
diff --git a/frontend/viewer/src/lib/entry-editor/inputs/CrdtMultiOptionField.svelte b/frontend/viewer/src/lib/entry-editor/inputs/CrdtMultiOptionField.svelte index d9619e6c1..b26ac9a04 100644 --- a/frontend/viewer/src/lib/entry-editor/inputs/CrdtMultiOptionField.svelte +++ b/frontend/viewer/src/lib/entry-editor/inputs/CrdtMultiOptionField.svelte @@ -3,6 +3,7 @@ import {createEventDispatcher, type ComponentProps} from 'svelte'; import {MultiSelectField, type MenuOption, type TextField} from 'svelte-ux'; import CrdtField from './CrdtField.svelte'; + import {makeHasHadValueTracker} from '$lib/utils'; const dispatch = createEventDispatcher<{ change: { value: string[] }; // Generics aren't working properly in CrdtField, so we make the type excplicit here @@ -29,6 +30,8 @@ return aIndex - bIndex; }); } + + let hasHadValue = makeHasHadValueTracker(); dispatch('change', { value: e.detail.value})} bind:value bind:unsavedChanges let:editorValue let:onEditorValueChange viewMergeButtonPortal={append}> @@ -56,7 +59,7 @@ clearSearchOnOpen={false} clearable={false} class="ws-field" - classes={{ root: `${editorValue ? '' : 'empty'} ${readonly ? 'readonly' : ''}`, field: 'field-container' }} + classes={{ root: `${hasHadValue.pushAndGet(editorValue) ? '' : 'unused'} ${readonly ? 'readonly' : ''}`, field: 'field-container' }} {label} {labelPlacement} {placeholder}> diff --git a/frontend/viewer/src/lib/entry-editor/inputs/CrdtOptionField.svelte b/frontend/viewer/src/lib/entry-editor/inputs/CrdtOptionField.svelte index 58846dcb2..8454cf45b 100644 --- a/frontend/viewer/src/lib/entry-editor/inputs/CrdtOptionField.svelte +++ b/frontend/viewer/src/lib/entry-editor/inputs/CrdtOptionField.svelte @@ -2,6 +2,7 @@ import type { ComponentProps } from 'svelte'; import CrdtField from './CrdtField.svelte'; import { SelectField, type TextField, type MenuOption } from 'svelte-ux'; + import {makeHasHadValueTracker} from '$lib/utils'; export let value: string | undefined; export let unsavedChanges = false; @@ -13,6 +14,8 @@ let append: HTMLElement; $: sortedOptions = [...options].sort((a, b) => a.label.localeCompare(b.label)); + + let hasHadValue = makeHasHadValueTracker(); @@ -26,7 +29,7 @@ clearable={false} search={() => Promise.resolve()} class="ws-field" - classes={{ root: `${editorValue ? '' : 'empty'} ${readonly ? 'readonly' : ''}`, field: 'field-container' }} + classes={{ root: `${hasHadValue.pushAndGet(editorValue) ? '' : 'unused'} ${readonly ? 'readonly' : ''}`, field: 'field-container' }} {label} {labelPlacement} {placeholder}> diff --git a/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte b/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte index 657c1dca2..77ef68268 100644 --- a/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte +++ b/frontend/viewer/src/lib/entry-editor/inputs/CrdtTextField.svelte @@ -2,6 +2,7 @@ import type { ComponentProps } from 'svelte'; import CrdtField from './CrdtField.svelte'; import { TextField, autoFocus as autoFocusFunc } from 'svelte-ux'; + import {makeHasHadValueTracker} from '$lib/utils'; export let value: string | number | null | undefined; export let unsavedChanges = false; @@ -12,6 +13,8 @@ export let autofocus: boolean = false; let append: HTMLElement; + let hasHadValue = makeHasHadValueTracker(); + // Labels don't always fit (beause WS's can be long and ugly), so a title might be important sometimes function addTitleToLabel(textElem: HTMLElement): void { const labelElem = textElem.closest('label')?.querySelector('.label'); @@ -31,7 +34,7 @@ value={editorValue} disabled={readonly} class="ws-field gap-2 text-right" - classes={{ root: `${editorValue ? '' : 'empty'} ${readonly ? 'readonly' : ''}`, input: 'field-input', container: 'field-container' }} + classes={{ root: `${hasHadValue.pushAndGet(editorValue) ? '' : 'unused'} ${readonly ? 'readonly' : ''}`, input: 'field-input', container: 'field-container' }} {label} {labelPlacement} {placeholder}> diff --git a/frontend/viewer/src/lib/history/HistoryView.svelte b/frontend/viewer/src/lib/history/HistoryView.svelte index 5d3bdcbe1..4772b2442 100644 --- a/frontend/viewer/src/lib/history/HistoryView.svelte +++ b/frontend/viewer/src/lib/history/HistoryView.svelte @@ -94,18 +94,20 @@
-
- {#if record.entityName === 'Entry'} - - {:else if record.entityName === 'Sense'} -
- -
- {:else if record.entityName === 'ExampleSentence'} -
- -
- {/if} +
+ {#key record} + {#if record.entityName === 'Entry'} + + {:else if record.entityName === 'Sense'} +
+ +
+ {:else if record.entityName === 'ExampleSentence'} +
+ +
+ {/if} + {/key}
{/if}
diff --git a/frontend/viewer/src/lib/utils.ts b/frontend/viewer/src/lib/utils.ts index 20db9b3c1..1c8d94a11 100644 --- a/frontend/viewer/src/lib/utils.ts +++ b/frontend/viewer/src/lib/utils.ts @@ -1,4 +1,5 @@ import type {IEntry, IExampleSentence, ISense, IWritingSystem, WritingSystemType} from '$lib/dotnet-types'; +import {get, writable, type Readable} from 'svelte/store'; export function randomId(): string { return crypto.randomUUID(); @@ -62,3 +63,16 @@ export function defaultWritingSystem(type: WritingSystemType): IWritingSystem { type }; } + +export function makeHasHadValueTracker(): { store: Readable, pushAndGet(currValueOrHasValue?: unknown): boolean } { + const hasHadValueStore = writable(); + function pushAndGet(currValueOrHasValue?: unknown) { + hasHadValueStore.update(hasHadValue => { + if (hasHadValue) return true; + return !!currValueOrHasValue; + }); + return get(hasHadValueStore); + } + + return { store: hasHadValueStore, pushAndGet }; +} From a15d236e03fb31d5e7ecc8c8563f9107984d3891 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 4 Feb 2025 10:09:56 +0100 Subject: [PATCH 4/6] Scroll to top of editor when a different entry is selected --- frontend/viewer/src/ProjectView.svelte | 9 ++++++++- .../src/lib/services/selected-entry-service.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 frontend/viewer/src/lib/services/selected-entry-service.ts diff --git a/frontend/viewer/src/ProjectView.svelte b/frontend/viewer/src/ProjectView.svelte index e49f06f6e..8d7fe2020 100644 --- a/frontend/viewer/src/ProjectView.svelte +++ b/frontend/viewer/src/ProjectView.svelte @@ -41,6 +41,7 @@ import {asScottyPortal, initScottyPortalContext} from '$lib/layout/Scotty.svelte'; import {initProjectViewState} from '$lib/services/project-view-state-service'; import NewEntryButton from '$lib/entry-editor/NewEntryButton.svelte'; + import {getSelectedEntryChangedStore} from '$lib/services/selected-entry-service'; const dispatch = createEventDispatcher<{ loaded: boolean; @@ -274,7 +275,13 @@ }; }); - + const selectedEntryChanged = getSelectedEntryChangedStore(selectedEntry); + onDestroy(selectedEntryChanged.subscribe(() => { // reactive syntax was not reliable + const scrolledDown = (editorElem?.getBoundingClientRect()?.y ?? 0) < 0; + // we don't want to scroll the app-bar out of view, but we also don't want to scroll it into view + // i.e. the project-view should look the same, we just want to make sure we're at the top of the editor + if (scrolledDown) editorElem?.scrollIntoView({block: 'start', inline: 'nearest', behavior: 'instant'}); + })); let newEntryDialog: NewEntryDialog; async function openNewEntryDialog(lexemeForm?: string, options?: NewEntryDialogOptions): Promise { diff --git a/frontend/viewer/src/lib/services/selected-entry-service.ts b/frontend/viewer/src/lib/services/selected-entry-service.ts new file mode 100644 index 000000000..8472ab664 --- /dev/null +++ b/frontend/viewer/src/lib/services/selected-entry-service.ts @@ -0,0 +1,16 @@ +import type {IEntry} from '$lib/dotnet-types'; +import {derived, get, type Readable} from 'svelte/store'; + +/** + * Returns a store like selectedEntry, but only emits when a completely different entry is selected (or no entry is selected), + * rather than when the currently selected entry experiences a change (which may result in selectedEntry emitting). + */ +export function getSelectedEntryChangedStore(selectedEntry: Readable): Readable { + let previousSelectedEntry = get(selectedEntry); + return derived(selectedEntry, ($selectedEntry, set) => { + if (previousSelectedEntry?.id !== $selectedEntry?.id) { + previousSelectedEntry = $selectedEntry; + set($selectedEntry); + } + }); +} From 63980c50cc7f0b94e85efb667a2dd0f0d5ab7d24 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 4 Feb 2025 10:25:24 +0100 Subject: [PATCH 5/6] Prevent page-width resize when opening dialog --- frontend/viewer/src/app.postcss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/viewer/src/app.postcss b/frontend/viewer/src/app.postcss index 1004eb2cc..a916eda5f 100644 --- a/frontend/viewer/src/app.postcss +++ b/frontend/viewer/src/app.postcss @@ -130,4 +130,6 @@ html:has(.Dialog) { @apply overflow-hidden; + /* scrollbar-gutter: stable; prevents a page-width resize if there IS a scrollbar, + but it causes a page-width reisze if there ISN'T a scrollbar. */ } From 67717dd34d81cf17b46f634a3610986a65da5228 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 4 Feb 2025 10:36:21 +0100 Subject: [PATCH 6/6] Hide history buttons in new entry dialog --- frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte index e14b77918..6e9135960 100644 --- a/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte +++ b/frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte @@ -10,6 +10,7 @@ import EntryEditor from './object-editors/EntryEditor.svelte'; import OverrideFields from '$lib/OverrideFields.svelte'; import {useWritingSystemService} from '$lib/writing-system-service'; + import {initFeatures} from '$lib/services/feature-service'; let open = false; let loading = false; @@ -66,6 +67,8 @@ } entry = defaultEntry(); } + + initFeatures({ write: true }); // hide history buttons