Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI field and editor improvements #1445

Merged
merged 6 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ backend/FwLite/FwLiteShared/wwwroot/viewer

*.csproj.user
*.log
failedSyncs/
14 changes: 7 additions & 7 deletions backend/FwLite/LcmCrdt/Objects/PreDefinedData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
Expand Down
9 changes: 8 additions & 1 deletion frontend/viewer/src/ProjectView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IEntry | undefined> {
Expand Down
2 changes: 2 additions & 0 deletions frontend/viewer/src/app.postcss
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
}
16 changes: 9 additions & 7 deletions frontend/viewer/src/lib/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@
}
</script>

<div id="entry" class:hide-empty={!$viewSettings.showEmptyFields}>
<EntryEditor
on:change={e => onChange(e.detail)}
on:delete={e => onDelete(e.detail)}
entry={entry}
{readonly}/>
<div id="entry" class:hide-unused={!$viewSettings.showEmptyFields}>
{#key entry.id}
<EntryEditor
on:change={e => onChange(e.detail)}
on:delete={e => onDelete(e.detail)}
entry={entry}
{readonly}/>
{/key}
</div>

<style lang="postcss">
:global(.hide-empty :is(.empty, .ws-field-wrapper:has(.empty))) {
:global(.hide-unused :is(.unused, .ws-field-wrapper:has(.unused))) {
display: none !important;
}
</style>
3 changes: 3 additions & 0 deletions frontend/viewer/src/lib/entry-editor/NewEntryDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,6 +67,8 @@
}
entry = defaultEntry();
}

initFeatures({ write: true }); // hide history buttons
</script>

<Dialog bind:open on:close={onClosing} {loading} persistent={loading}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -54,7 +56,7 @@

<div
class="complex-form-components-field field"
class:empty
class:unused={!$hasHadValue}
class:hidden={!$currentView.fields[id].show}
style:grid-area={id}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;

Expand All @@ -48,7 +50,7 @@

<div
class="complex-forms-field field"
class:empty
class:unused={!$hasHadValue}
class:hidden={!$currentView.fields[id].show}
style:grid-area={id}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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;
</script>

<div class="multi-field field" class:collapse-field={collapse} class:empty class:hidden={hide} style:grid-area={id}>
<div class="multi-field field" class:collapse-field={collapse} class:unused={!$hasHadValue} class:hidden={hide} style:grid-area={id}>
<FieldTitle {id} {name} />
<div class="fields">
{#each writingSystems as ws, idx (ws.wsId)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import {makeHasHadValueTracker} from '$lib/utils';

/* eslint-disable @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents */
import {createEventDispatcher} from 'svelte';
import type {WritingSystemSelection} from '../../config-types';
Expand Down Expand Up @@ -101,13 +103,16 @@
const writingSystemService = useWritingSystemService();

$: [ws] = writingSystemService.pickWritingSystems(wsType);
$: empty = !value?.length;

let hasHadValueTracker = makeHasHadValueTracker();
let hasHadValue = hasHadValueTracker.store;
$: hasHadValueTracker.pushAndGet(value?.length);
</script>

{#key options}
<MapBind bind:in={value} bind:out={ids} map={toIds} unmap={fromIds} />
{/key}
<div class="single-field field" class:empty class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<div class="single-field field" class:unused={!$hasHadValue} class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<FieldTitle {id} {name}/>
<div class="fields">
<CrdtMultiOptionField on:change={onChange} bind:value={ids} options={uiOptions} placeholder={ws.abbreviation} {readonly} {preserveOrder} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,10 +16,13 @@
const writingSystemService = useWritingSystemService();

$: [ws] = writingSystemService.pickWritingSystems(wsType);
$: empty = !value;

let hasHadValueTracker = makeHasHadValueTracker();
let hasHadValue = hasHadValueTracker.store;
$: hasHadValueTracker.pushAndGet(value);
</script>

<div class="single-field field" class:empty class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<div class="single-field field" class:unused={!$hasHadValue} class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<FieldTitle id={id} {name}/>
<div class="fields">
<CrdtTextField on:change bind:value placeholder={ws.abbreviation} {readonly} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
import {makeHasHadValueTracker} from '$lib/utils';

/* eslint-disable @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents */
import { createEventDispatcher } from 'svelte';
import MapBind from '../../utils/MapBind.svelte';
Expand Down Expand Up @@ -90,13 +92,16 @@
const writingSystemService = useWritingSystemService();

$: [ws] = writingSystemService.pickWritingSystems(wsType);
$: empty = !value;

let hasHadValueTracker = makeHasHadValueTracker();
let hasHadValue = hasHadValueTracker.store;
$: hasHadValueTracker.pushAndGet(value);
</script>

{#key options}
<MapBind bind:in={value} bind:out={valueId} map={getValueId} unmap={getValueById} />
{/key}
<div class="single-field field" class:empty class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<div class="single-field field" class:unused={!$hasHadValue} class:hidden={!$currentView.fields[id].show} style:grid-area={id}>
<FieldTitle {id} {name}/>
<div class="fields">
<CrdtOptionField on:change={onChange} bind:value={valueId} options={uiOptions} placeholder={ws.abbreviation} {readonly} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +30,8 @@
return aIndex - bIndex;
});
}

let hasHadValue = makeHasHadValueTracker();
</script>

<CrdtField on:change={(e) => dispatch('change', { value: e.detail.value})} bind:value bind:unsavedChanges let:editorValue let:onEditorValueChange viewMergeButtonPortal={append}>
Expand Down Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,8 @@
let append: HTMLElement;

$: sortedOptions = [...options].sort((a, b) => a.label.localeCompare(b.label));

let hasHadValue = makeHasHadValueTracker();
</script>

<CrdtField on:change bind:value bind:unsavedChanges let:editorValue let:onEditorValueChange viewMergeButtonPortal={append}>
Expand All @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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');
Expand All @@ -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}>
Expand Down
26 changes: 14 additions & 12 deletions frontend/viewer/src/lib/history/HistoryView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,20 @@
<ShowEmptyFieldsSwitch bind:value={showEmptyFields} />
</div>
</div>
<div class="col-start-2 row-start-2 overflow-auto p-3 pt-2 border rounded h-max max-h-full" class:hide-empty={!showEmptyFields}>
{#if record.entityName === 'Entry'}
<EntryEditor entry={record.entity} modalMode readonly/>
{:else if record.entityName === 'Sense'}
<div class="editor-grid">
<SenseEditor sense={record.entity} readonly/>
</div>
{:else if record.entityName === 'ExampleSentence'}
<div class="editor-grid">
<ExampleEditor example={record.entity} readonly/>
</div>
{/if}
<div class="col-start-2 row-start-2 overflow-auto p-3 pt-2 border rounded h-max max-h-full" class:hide-unused={!showEmptyFields}>
{#key record}
{#if record.entityName === 'Entry'}
<EntryEditor entry={record.entity} modalMode readonly/>
{:else if record.entityName === 'Sense'}
<div class="editor-grid">
<SenseEditor sense={record.entity} readonly/>
</div>
{:else if record.entityName === 'ExampleSentence'}
<div class="editor-grid">
<ExampleEditor example={record.entity} readonly/>
</div>
{/if}
{/key}
</div>
{/if}
</div>
Expand Down
16 changes: 16 additions & 0 deletions frontend/viewer/src/lib/services/selected-entry-service.ts
Original file line number Diff line number Diff line change
@@ -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<IEntry | undefined>): Readable<IEntry | undefined> {
let previousSelectedEntry = get(selectedEntry);
return derived(selectedEntry, ($selectedEntry, set) => {
if (previousSelectedEntry?.id !== $selectedEntry?.id) {
previousSelectedEntry = $selectedEntry;
set($selectedEntry);
}
});
}
Loading
Loading