Skip to content

Commit

Permalink
Populate UI with parts-of-speech
Browse files Browse the repository at this point in the history
  • Loading branch information
myieye committed Jun 18, 2024
1 parent c351dcd commit 8ebf4f9
Show file tree
Hide file tree
Showing 18 changed files with 168 additions and 48 deletions.
4 changes: 2 additions & 2 deletions backend/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,15 @@ public Task<WritingSystem> UpdateWritingSystem(WritingSystemId id, WritingSystem

public async IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()
{
foreach (var partOfSpeech in _partOfSpeechRepository.AllInstances())
foreach (var partOfSpeech in _partOfSpeechRepository.AllInstances().OrderBy(p => p.Name.BestAnalysisAlternative.Text))
{
yield return new PartOfSpeech { Id = partOfSpeech.Guid, Name = FromLcmMultiString(partOfSpeech.Name) };
}
}

public async IAsyncEnumerable<SemanticDomain> GetSemanticDomains()
{
foreach (var semanticDomain in _semanticDomainRepository.AllInstances())
foreach (var semanticDomain in _semanticDomainRepository.AllInstances().OrderBy(p => p.Name.BestAnalysisAlternative.Text))
{
yield return new SemanticDomain
{
Expand Down
10 changes: 10 additions & 0 deletions backend/LocalWebApp/Hubs/FwDataMiniLcmHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ public async Task<WritingSystem> UpdateWritingSystem(WritingSystemId id, Writing
return writingSystem;
}

public IAsyncEnumerable<PartOfSpeech> GetPartsOfSpeech()
{
return lexboxApi.GetPartsOfSpeech();
}

public IAsyncEnumerable<SemanticDomain> GetSemanticDomains()
{
return lexboxApi.GetSemanticDomains();
}

public IAsyncEnumerable<Entry> GetEntriesForExemplar(string exemplar, QueryOptions? options = null)
{
throw new NotImplementedException();
Expand Down
21 changes: 18 additions & 3 deletions frontend/viewer/src/ProjectView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import {AppBar, Button, ProgressCircle} from 'svelte-ux';
import {mdiArrowCollapseLeft, mdiArrowCollapseRight, mdiArrowLeft, mdiEyeSettingsOutline} from '@mdi/js';
import Editor from './lib/Editor.svelte';
import {headword} from './lib/utils';
import {headword, pickBestAlternative} from './lib/utils';
import {views} from './lib/config-data';
import {useLexboxApi} from './lib/services/service-provider';
import type {IEntry} from './lib/mini-lcm';
import {setContext} from 'svelte';
import {derived, writable, type Readable} from 'svelte/store';
import {derived, readable, writable, type Readable} from 'svelte/store';
import {deriveAsync} from './lib/utils/time';
import {type ViewConfig, type LexboxPermissions, type ViewOptions, type LexboxFeatures} from './lib/config-types';
import ViewOptionsDrawer from './lib/layout/ViewOptionsDrawer.svelte';
Expand All @@ -18,6 +18,7 @@
import NewEntryDialog from './lib/entry-editor/NewEntryDialog.svelte';
import SearchBar from './lib/search-bar/SearchBar.svelte';
import ActivityView from './lib/activity/ActivityView.svelte';
import type { OptionProvider } from './lib/services/option-provider';
export let loading = false;
Expand Down Expand Up @@ -73,6 +74,20 @@
trigger.update(t => t + 1);
}
const partsOfSpeech = deriveAsync(connected, isConnected => {
if (!isConnected) return Promise.resolve(null);
return lexboxApi.GetPartsOfSpeech();
});
const semanticDomains = deriveAsync(connected, isConnected => {
if (!isConnected) return Promise.resolve(null);
return lexboxApi.GetSemanticDomains();
});
const optionProvider: OptionProvider = {
partsOfSpeech: derived([writingSystems, partsOfSpeech], ([ws, pos]) => pos?.map(option => ({ value: option.id, label: pickBestAlternative(option.name, ws?.analysis[0]) })) ?? []),
semanticDomains: derived([writingSystems, semanticDomains], ([ws, sd]) => sd?.map(option => ({ value: option, label: pickBestAlternative(option.name, ws?.analysis[0]) })) ?? []),
};
setContext('optionProvider', optionProvider);
const _entries = deriveAsync(derived([search, connected, selectedIndexExemplar, trigger], s => s), ([s, isConnected, exemplar]) => {
return fetchEntries(s, isConnected, exemplar);
}, undefined, 200);
Expand Down Expand Up @@ -119,7 +134,7 @@
}
$: _loading = !$entries || !$writingSystems || loading;
$: _loading = !$entries || !$writingSystems || !$partsOfSpeech || !$semanticDomains || loading;
function onEntryCreated(entry: IEntry) {
$entries?.push(entry);//need to add it before refresh, otherwise it won't get selected because it's not in the list
Expand Down
21 changes: 13 additions & 8 deletions frontend/viewer/src/lib/config-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseEntityFieldConfig, CustomFieldConfig, FieldConfig, ViewConfigF
import type { IEntry, IExampleSentence, ISense } from './mini-lcm';

import type { I18nType } from './i18n';
import type { ConditionalPickDeep, ValueOf } from 'type-fest';

const allFieldConfigs = ({
entry: {
Expand All @@ -16,8 +17,8 @@ const allFieldConfigs = ({
sense: {
gloss: { id: 'gloss', type: 'multi', ws: 'analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Gloss_field_Sense.htm' },
definition: { id: 'definition', type: 'multi', ws: 'analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/definition_field.htm' },
partOfSpeech: { id: 'partOfSpeech', type: 'option', optionType: 'part-of-speech', ws: 'first-analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Grammatical_Info_field.htm' },
semanticDomain: { id: 'semanticDomain', type: 'multi-option', optionType: 'semantic-domain', ws: 'first-analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/semantic_domains_field.htm' }
partOfSpeechId: { id: 'partOfSpeechId', type: 'option', optionType: 'part-of-speech', ws: 'first-analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Grammatical_Info_field.htm' },
semanticDomains: { id: 'semanticDomains', type: 'multi-option', optionType: 'semantic-domain', ws: 'first-analysis', helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/semantic_domains_field.htm' }
},
customSense: {
custom1: { id: 'sense-custom-001', type: 'multi', ws: 'first-analysis', name: 'Custom sense', custom: true },
Expand All @@ -38,15 +39,19 @@ const allFieldConfigs = ({
customExample: Record<string, CustomFieldConfig>,
};

export function allFields(viewConfig: ViewConfig): FieldConfig[] {
type FieldOptionType<T extends { optionType: string }> = T['optionType'];
export type WellKnownOptionType = FieldOptionType<ValueOf<ConditionalPickDeep<typeof allFieldConfigs, { optionType?: string }>['sense']>>;
export type OptionType = WellKnownOptionType | Omit<string, WellKnownOptionType>;

export function allFields(viewConfig: ViewConfig): Readonly<FieldConfig[]> {
return [
...Object.values(viewConfig.entry),
...Object.values(viewConfig.customEntry ?? {}),
...Object.values(viewConfig.sense),
...Object.values(viewConfig.customSense ?? {}),
...Object.values(viewConfig.example),
...Object.values<never>(viewConfig.customExample ?? {}),
];
] as const;
}

type FieldsWithViewConfigProps<T extends Record<string, NonNullable<object>>> =
Expand Down Expand Up @@ -88,8 +93,8 @@ export const views: ViewConfig[] = [
},
sense: {
gloss: allFieldConfigs.sense.gloss,
partOfSpeech: allFieldConfigs.sense.partOfSpeech,
semanticDomain: configure(allFieldConfigs.sense.semanticDomain, { extra: true }),
partOfSpeechId: allFieldConfigs.sense.partOfSpeechId,
semanticDomains: configure(allFieldConfigs.sense.semanticDomains, { extra: true }),
},
example: {
sentence: allFieldConfigs.example.sentence,
Expand All @@ -106,8 +111,8 @@ export const views: ViewConfig[] = [
sense: {
gloss: allFieldConfigs.sense.gloss,
definition: allFieldConfigs.sense.definition,
partOfSpeech: allFieldConfigs.sense.partOfSpeech,
semanticDomain: allFieldConfigs.sense.semanticDomain,
partOfSpeechId: allFieldConfigs.sense.partOfSpeechId,
semanticDomains: allFieldConfigs.sense.semanticDomains,
},
example: {
sentence: allFieldConfigs.example.sentence,
Expand Down
17 changes: 9 additions & 8 deletions frontend/viewer/src/lib/config-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IEntry, IExampleSentence, IMultiString, ISense } from './mini-lcm';
import type { IEntry, IExampleSentence, IMultiString, ISense, SemanticDomain } from './mini-lcm';

import type { ConditionalKeys } from 'type-fest';
import type { LexboxApiFeatures } from './services/lexbox-api';
Expand All @@ -21,22 +21,23 @@ export type CustomFieldConfig = BaseFieldConfig & {
custom: true;
}

export type OptionFieldConfig = {
type: `option`;
optionType: string;
ws: `first-${WritingSystemType}`;
}

export type BaseEntityFieldConfig<T> = (({
type: 'multi';
id: ConditionalKeys<T, IMultiString>;
} | {
type: 'single';
id: ConditionalKeys<T, string>;
ws: `first-${WritingSystemType}`;
} | {
type: `option`;
optionType: string;
id: ConditionalKeys<T, string>;
ws: `first-${WritingSystemType}`;
} | {
} | (OptionFieldConfig & {id: ConditionalKeys<T, string>}) | {
type: `multi-option`;
optionType: string;
id: ConditionalKeys<T, string[]>;
id: ConditionalKeys<T, string[] | SemanticDomain[]>;
ws: `first-${WritingSystemType}`;
}) & BaseFieldConfig & {
id: WellKnownFieldId,
Expand Down
8 changes: 3 additions & 5 deletions frontend/viewer/src/lib/entry-editor/CrdtOptionField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@
export let value: string;
export let unsavedChanges = false;
export let options: MenuOption[] | undefined = undefined;
export let label: string | undefined = undefined;
export let labelPlacement: ComponentProps<TextField>['labelPlacement'] = undefined;
export let placeholder: string | undefined = undefined;
export let readonly: true | undefined = undefined;
export let readonly: boolean | undefined = undefined;
let append: HTMLElement;
let demoOptions: MenuOption[] | undefined;
$: demoOptions = demoOptions ?? [{label: value, value: value}, {label: 'Another option', value: 'Another option'}];
</script>

<CrdtField on:change bind:value bind:unsavedChanges let:editorValue let:onEditorValueChange viewMergeButtonPortal={append}>
<SelectField
on:change={(e) => onEditorValueChange(e.detail.value, true)}
value={editorValue}
disabled={readonly}
options={demoOptions ?? []}
{options}
clearSearchOnOpen={false}
clearable={false}
search={() => Promise.resolve()}
Expand Down
10 changes: 6 additions & 4 deletions frontend/viewer/src/lib/entry-editor/FieldEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import type { MultiString } from '../mini-lcm';
import type { FieldConfig } from '../config-types';
import type { FieldConfig, OptionFieldConfig } from '../config-types';
import MultiOptionEditor from './MultiOptionEditor.svelte';
import SingleOptionEditor from './SingleOptionEditor.svelte';
import SingleFieldEditor from './SingleFieldEditor.svelte';
Expand All @@ -11,13 +11,15 @@
export let value: unknown;
export let field: FieldConfig;
$: state = {value, field};
function isMultiString(value: unknown): value is MultiString {
return field.type === 'multi';
}
function isSingleString(value: unknown): value is string {
return field.type === 'single';
}
function isSingleOption(value: unknown): value is string {
function isSingleOption(state: {value: unknown, field: FieldConfig}): state is {value: string, field: FieldConfig & OptionFieldConfig} {
return field.type === 'option';
}
function isMultiOption(value: unknown): value is string[] {
Expand All @@ -29,8 +31,8 @@
<MultiFieldEditor on:change {field} bind:value />
{:else if isSingleString(value)}
<SingleFieldEditor on:change {field} bind:value />
{:else if isSingleOption(value)}
<SingleOptionEditor on:change {field} bind:value />
{:else if isSingleOption(state)}
<SingleOptionEditor on:change field={state.field} bind:value={state.value} />
{:else if isMultiOption(value)}
<MultiOptionEditor on:change {field} bind:value />
{/if}
Expand Down
27 changes: 23 additions & 4 deletions frontend/viewer/src/lib/entry-editor/SingleOptionEditor.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
<script lang="ts">
import type { OptionType } from '../config-data';
import type { OptionProvider } from '../services/option-provider';
import CrdtOptionField from './CrdtOptionField.svelte';
import FieldTitle from './FieldTitle.svelte';
import type { WritingSystems } from '../mini-lcm';
import type { Readable } from 'svelte/store';
import { readable, type Readable } from 'svelte/store';
import { getContext } from 'svelte';
import { pickWritingSystems } from '../utils';
import type { FieldConfig, ViewConfig } from '../config-types';
import type { FieldConfig, OptionFieldConfig, ViewConfig } from '../config-types';
import type { MenuOption } from 'svelte-ux';
type T = $$Generic<{}>;
export let field: FieldConfig;
export let field: FieldConfig & OptionFieldConfig;
export let value: string;
let options: Readable<MenuOption[]> = readable([]);
$: options = pickOptions(field);
const optionProvider = getContext<OptionProvider>('optionProvider');
function pickOptions(field: FieldConfig & OptionFieldConfig): Readable<MenuOption[]> {
switch (field.optionType as OptionType) {
case 'part-of-speech':
return optionProvider.partsOfSpeech;
default:
throw new Error(`No options for single-option field ${field.id} (Option type: ${field.optionType})`);
}
}
const allWritingSystems = getContext<Readable<WritingSystems>>('writingSystems');
const viewConfig = getContext<Readable<ViewConfig>>('viewConfig');
Expand All @@ -22,6 +41,6 @@
<div class="single-field field" class:empty class:extra={'extra' in field && field.extra}>
<FieldTitle {field} />
<div class="fields">
<CrdtOptionField on:change bind:value placeholder={ws.abbreviation} readonly={field.readonly || $viewConfig.readonly} />
<CrdtOptionField on:change bind:value options={$options} placeholder={ws.abbreviation} readonly={field.readonly || $viewConfig.readonly} />
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
/* eslint-disable */
/* tslint:disable */

import { HubConnection } from '@microsoft/signalr';
import type {Entry, ExampleSentence, PartOfSpeech, QueryOptions, SemanticDomain, Sense, WritingSystem, WritingSystems} from '../../mini-lcm';
import type { ILexboxApiHub, ILexboxClient } from './Lexbox.ClientServer.Hubs';
import type {WritingSystems, QueryOptions, Entry, Sense, ExampleSentence, WritingSystem} from '../../mini-lcm';

import { HubConnection } from '@microsoft/signalr';
import type { JsonOperation } from '../Lexbox.ClientServer.Hubs';
import type {WritingSystemType} from '../../services/lexbox-api';


// components

export type Disposable = {
Expand Down Expand Up @@ -94,6 +94,40 @@ class ILexboxApiHub_HubProxy implements ILexboxApiHub {
return await this.connection.invoke("UpdateWritingSystem", wsId, type, update);
}

public readonly GetPartsOfSpeech = async (): Promise<PartOfSpeech[]> => {
return new Promise((resolve, reject) => {
let partsOfSpeech: PartOfSpeech[] = [];
this.connection.stream<PartOfSpeech>('GetPartsOfSpeech').subscribe({
next(value: PartOfSpeech) {
partsOfSpeech.push(value);
},
error(err: any) {
reject(err);
},
complete() {
resolve(partsOfSpeech);
}
});
});
}

public readonly GetSemanticDomains = async (): Promise<SemanticDomain[]> => {
return new Promise((resolve, reject) => {
let semanticDomains: SemanticDomain[] = [];
this.connection.stream<SemanticDomain>('GetSemanticDomains').subscribe({
next(value: SemanticDomain) {
semanticDomains.push(value);
},
error(err: any) {
reject(err);
},
complete() {
resolve(semanticDomains);
}
});
});
}

public readonly GetEntries = async (options: QueryOptions): Promise<Entry[]> => {
return await this.connection.invoke("GetEntries", options);
}
Expand Down
8 changes: 4 additions & 4 deletions frontend/viewer/src/lib/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const defaultI18n = {
'note': 'Note',
'definition': 'Definition',
'gloss': 'Gloss',
'partOfSpeech': 'Grammatical Info.',
'semanticDomain': 'Semantic domain',
'partOfSpeechId': 'Grammatical Info.',
'semanticDomains': 'Semantic domain',
'sentence': 'Sentence',
'translation': 'Translation',
'reference': 'Reference',
Expand All @@ -22,12 +22,12 @@ const defaultI18n = {
const weSayI18n = {
'lexemeForm': 'Word',
'gloss': 'Definition',
'partOfSpeech': 'Part of speech',
'partOfSpeechId': 'Part of speech',
};

const languageForgeI18n = {
'lexemeForm': 'Word',
'partOfSpeech': 'Part of speech',
'partOfSpeechId': 'Part of speech',
};

const i18nMap = ({
Expand Down
5 changes: 3 additions & 2 deletions frontend/viewer/src/lib/mini-lcm/i-sense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

import { type IMultiString } from './i-multi-string';
import { type IExampleSentence } from './i-example-sentence';
import type { SemanticDomain } from './semantic-domain';

export interface ISense {
id: string;
definition: IMultiString;
gloss: IMultiString;
partOfSpeech: string;
semanticDomain: string[];
partOfSpeechId: string;
semanticDomains: SemanticDomain[];
exampleSentences: IExampleSentence[];
}
Loading

0 comments on commit 8ebf4f9

Please sign in to comment.