Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
const viewService = useViewService();
const multiWindowService = useMultiWindowService();

interface Props extends Omit<ItemListProps<T>, 'getDisplayName'> {
interface Props extends Omit<ItemListProps<T>, 'getDisplayName' | 'menuItems'> {
getEntryId: (item: T) => string;
getHeadword: (item: T) => string | undefined;
extraMenuItems?: ItemListProps<T>['menuItems'];
}

let { items = $bindable(), getEntryId, getHeadword, ...rest }: Props = $props();
let { items = $bindable(), getEntryId, getHeadword, extraMenuItems, ...rest }: Props = $props();
</script>

<ItemList
Expand All @@ -41,5 +42,6 @@
{$t`Open in new Window`}
</DropdownMenu.Item>
{/if}
{@render extraMenuItems?.(entry)}
{/snippet}
</ItemList>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script lang="ts">
import EntryOrSensePicker, {type EntrySenseSelection} from '../EntryOrSensePicker.svelte';
import {randomId} from '$lib/utils';
import EntryOrSenseItemList from '../EntryOrSenseItemList.svelte';
import VariantTypesMenuItems from './VariantTypesMenuItems.svelte';
import type {IEntry, ISense, IVariant} from '$lib/dotnet-types';
import {UNSPECIFIED_VARIANT_TYPE_ID, useVariantTypes, useWritingSystemService} from '$project/data';
import {Button} from '$lib/components/ui/button';
import {t} from 'svelte-i18n-lingui';
import {pt} from '$lib/views/view-text';
import {useViewService} from '$lib/views/view-service.svelte';
import type {ReadonlyDeep} from 'type-fest';

type Props = {
value: IVariant[];
readonly: boolean;
entry: ReadonlyDeep<IEntry>;
onchange: (value: IVariant[]) => void;
}

let {
value = $bindable(),
readonly,
entry,
onchange,
}: Props = $props();

const writingSystemService = useWritingSystemService();
const variantTypes = useVariantTypes();
const viewService = useViewService();

function addVariantOf(selection: EntrySenseSelection) {
// FLEx assigns the Unspecified Variant type when the user doesn't pick one
const unspecified = variantTypes.current.find((vt) => vt.id === UNSPECIFIED_VARIANT_TYPE_ID);
const variant: IVariant = {
id: randomId(),
variantEntryId: entry.id,
variantHeadword: writingSystemService.headword(entry),
mainEntryId: selection.entry.id,
mainSenseId: selection.sense?.id,
mainHeadword: writingSystemService.headword(selection.entry),
types: unspecified ? [unspecified] : [],
hideMinorEntry: false,
comment: {},
};
value = [...value, variant];
onchange?.(value);
}

function disableEntry(e: IEntry): false | { disableSenses?: true, reason: string } {
if (e.id === entry.id) return { reason: pt($t`Current Entry`, $t`Current Word`, viewService.currentView), disableSenses: true };
if (entry.variantOf.some((v) => v.mainEntryId === e.id && !v.mainSenseId)) return { reason: $t`Variant of` };
// the reverse link would make a cycle, which FieldWorks rejects
if (entry.variants.some((v) => v.variantEntryId === e.id)) return { reason: $t`Variant`, disableSenses: true };
return false;
}

function disableSense(s: ISense, e: IEntry): false | string {
if (entry.variantOf.some((v) => v.mainEntryId === e.id && v.mainSenseId === s.id)) return $t`Variant of`;
return false;
}
</script>

<EntryOrSenseItemList bind:items={value} {readonly} {onchange} getEntryId={(v) => v.mainEntryId} getHeadword={(v) => v.mainHeadword}>
{#snippet extraMenuItems(variant)}
<!-- reassign so the nested types edit reliably re-renders (see EntryEditor's `entry = entry`) -->
<VariantTypesMenuItems {variant} {readonly} onchange={() => { value = [...value]; onchange?.(value); }} />
{/snippet}
{#snippet actions()}
<EntryOrSensePicker title={$t`Add variant of`} pick={(e) => addVariantOf(e)}
{disableEntry} {disableSense}>
{#snippet trigger({ props })}
<Button {...props} icon="i-mdi-plus" size="xs">
{$t`Variant of`}
</Button>
{/snippet}
</EntryOrSensePicker>
{/snippet}
</EntryOrSenseItemList>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import {Icon} from '$lib/components/ui/icon';
import {t} from 'svelte-i18n-lingui';
import type {IVariant} from '$lib/dotnet-types';
import {useVariantTypes, useWritingSystemService} from '$project/data';

type Props = {
variant: IVariant;
readonly: boolean;
onchange?: () => void;
}

let {variant, readonly, onchange}: Props = $props();

const variantTypes = useVariantTypes();
const writingSystemService = useWritingSystemService();

function hasType(id: string): boolean {
return variant.types.some((type) => type.id === id);
}

function toggleType(id: string) {
const type = variantTypes.current.find((vt) => vt.id === id);
if (!type) return;
if (hasType(id)) {
variant.types = variant.types.filter((existing) => existing.id !== id);
} else {
variant.types = [...variant.types, type];
}
onchange?.();
}
</script>

{#if !readonly && variantTypes.current.length}
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>
<Icon icon="i-mdi-tag-outline" />
{$t`Variant type`}
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
{#each variantTypes.current as type (type.id)}
<DropdownMenu.CheckboxItem
checked={hasType(type.id)}
closeOnSelect={false}
onCheckedChange={() => toggleType(type.id)}>
{writingSystemService.pickBestAlternative(type.name, 'analysis')}
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
{/if}
73 changes: 73 additions & 0 deletions frontend/viewer/src/lib/entry-editor/field-editors/Variants.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script lang="ts">
import EntryOrSensePicker, {type EntrySenseSelection} from '../EntryOrSensePicker.svelte';
import {randomId} from '$lib/utils';
import EntryOrSenseItemList from '../EntryOrSenseItemList.svelte';
import VariantTypesMenuItems from './VariantTypesMenuItems.svelte';
import type {IEntry, IVariant} from '$lib/dotnet-types';
import {UNSPECIFIED_VARIANT_TYPE_ID, useVariantTypes, useWritingSystemService} from '$project/data';
import {Button} from '$lib/components/ui/button';
import {t} from 'svelte-i18n-lingui';
import {pt} from '$lib/views/view-text';
import {useViewService} from '$lib/views/view-service.svelte';
import type {ReadonlyDeep} from 'type-fest';

type Props = {
value: IVariant[];
readonly: boolean;
entry: ReadonlyDeep<IEntry>;
onchange: (value: IVariant[]) => void;
}

let {
value = $bindable(),
readonly,
entry,
onchange,
}: Props = $props();

const writingSystemService = useWritingSystemService();
const variantTypes = useVariantTypes();
const viewService = useViewService();

function addVariant(selection: EntrySenseSelection) {
// FLEx assigns the Unspecified Variant type when the user doesn't pick one
const unspecified = variantTypes.current.find((vt) => vt.id === UNSPECIFIED_VARIANT_TYPE_ID);
const variant: IVariant = {
id: randomId(),
variantEntryId: selection.entry.id,
variantHeadword: writingSystemService.headword(selection.entry),
mainEntryId: entry.id,
mainHeadword: writingSystemService.headword(entry),
types: unspecified ? [unspecified] : [],
hideMinorEntry: false,
comment: {},
};
value = [...value, variant];
onchange?.(value);
}

function disableEntry(e: IEntry): false | { reason: string } {
if (e.id === entry.id) return { reason: pt($t`Current Entry`, $t`Current Word`, viewService.currentView) };
if (entry.variants.some((v) => v.variantEntryId === e.id)) return { reason: $t`Variant` };
// the reverse link would make a cycle, which FieldWorks rejects
if (entry.variantOf.some((v) => v.mainEntryId === e.id)) return { reason: $t`Variant of` };
return false;
}
</script>

<EntryOrSenseItemList bind:items={value} {readonly} {onchange} getEntryId={(v) => v.variantEntryId} getHeadword={(v) => v.variantHeadword}>
{#snippet extraMenuItems(variant)}
<!-- reassign so the nested types edit reliably re-renders (see EntryEditor's `entry = entry`) -->
<VariantTypesMenuItems {variant} {readonly} onchange={() => { value = [...value]; onchange?.(value); }} />
{/snippet}
{#snippet actions()}
<EntryOrSensePicker title={$t`Add variant`} mode="only-entries" pick={(e) => addVariant(e)}
{disableEntry}>
{#snippet trigger({ props })}
<Button {...props} icon="i-mdi-plus" size="xs">
{$t`Variant`}
</Button>
{/snippet}
</EntryOrSensePicker>
{/snippet}
</EntryOrSenseItemList>
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import {MultiSelect, MultiWsInput, RichMultiWsInput} from '$lib/components/field-editors';
import ComplexFormComponents from '../field-editors/ComplexFormComponents.svelte';
import ComplexForms from '../field-editors/ComplexForms.svelte';
import VariantOf from '../field-editors/VariantOf.svelte';
import Variants from '../field-editors/Variants.svelte';
import type {EditorSubGridProps} from '$lib/components/editor/editor-sub-grid.svelte';
import {mergeProps} from 'bits-ui';
import {initSubjectContext} from '$lib/entry-editor/object-editors/subject-context';
Expand Down Expand Up @@ -94,6 +96,26 @@
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root fieldId="variantOf" class={cn(fields.variantOf?.show || 'hidden')}>
<Editor.Field.Title name={$tvt(entityConfig.entry.variantOf.label)} helpId={entityConfig.entry.variantOf.helpId} />
<Editor.Field.Body>
<VariantOf onchange={() => onFieldChanged('variantOf')}
bind:value={entry.variantOf}
{readonly}
{entry} />
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root fieldId="variants" class={cn(fields.variants?.show || 'hidden')}>
<Editor.Field.Title name={$tvt(entityConfig.entry.variants.label)} helpId={entityConfig.entry.variants.helpId} />
<Editor.Field.Body>
<Variants onchange={() => onFieldChanged('variants')}
bind:value={entry.variants}
{readonly}
{entry} />
</Editor.Field.Body>
</Editor.Field.Root>

<Editor.Field.Root fieldId="complexFormTypes" class={cn(fields.complexFormTypes?.show || 'hidden')}>
<Editor.Field.Title name={$tvt(entityConfig.entry.complexFormTypes.label)} helpId={entityConfig.entry.complexFormTypes.helpId} />
<Editor.Field.Body>
Expand Down
9 changes: 9 additions & 0 deletions frontend/viewer/src/lib/views/entity-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ export const entityConfig = {
label: msg`Components`,
helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Components_field.htm',
},
variantOf: {
label: msg`Variant of`,
helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Variant_of_field.htm',
},
variants: {
label: msg`Variants`,
//there's no dedicated help topic for the variants back-reference; this one explains the relationship
helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Variant_of_field.htm',
},
literalMeaning: {
label: msg`Literal meaning`,
helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Literal_Meaning_field.htm',
Expand Down
8 changes: 5 additions & 3 deletions frontend/viewer/src/lib/views/view-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ export const FW_LITE_VIEW: RootView = {
complexForms: {show: true, order: 3},
complexFormTypes: {show: false, order: 4},
components: {show: true, order: 5},
literalMeaning: {show: false, order: 6},
note: {show: true, order: 7},
publishIn: {show: false, order: 8},
variantOf: {show: true, order: 6},
variants: {show: true, order: 7},
literalMeaning: {show: false, order: 8},
note: {show: true, order: 9},
publishIn: {show: false, order: 10},
},
sense: {
gloss: {show: true, order: 1},
Expand Down
47 changes: 42 additions & 5 deletions frontend/viewer/src/locales/en.po
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ msgstr "Add Sense"
msgid "Add to dictionary"
msgstr "Add to dictionary"

#. Dialog title: picker for adding an entry as a variant of the current entry (fills the "Variants" field)
#: src/lib/entry-editor/field-editors/Variants.svelte
msgid "Add variant"
msgstr "Add variant"

#. Dialog title: picker for marking the current entry as a variant of another entry/sense (fills the "Variant of" field)
#: src/lib/entry-editor/field-editors/VariantOf.svelte
msgid "Add variant of"
msgstr "Add variant of"

#. Dialog title
#: src/lib/entry-editor/NewEntryDialog.svelte
msgid "Add Word"
Expand Down Expand Up @@ -528,6 +538,8 @@ msgstr "Create View"
#. Label in picker: marks the entry that is currently being edited
#: src/lib/entry-editor/field-editors/ComplexFormComponents.svelte
#: src/lib/entry-editor/field-editors/ComplexForms.svelte
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/Variants.svelte
msgid "Current Entry"
msgstr "Current Entry"

Expand All @@ -541,6 +553,8 @@ msgstr "Current version"
#. Label in picker: marks the word that is currently being edited
#: src/lib/entry-editor/field-editors/ComplexFormComponents.svelte
#: src/lib/entry-editor/field-editors/ComplexForms.svelte
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/Variants.svelte
msgid "Current Word"
msgstr "Current Word"

Expand Down Expand Up @@ -900,7 +914,7 @@ msgstr "FieldWorks Lite version"

#. Alt text for FieldWorks logo image
#: src/home/HomeView.svelte
#: src/lib/activity/ActivityFilter.svelte
#: src/lib/activity/AuthorLabel.svelte
#: src/lib/components/OpenInFieldWorksButton.svelte
#: src/project/ProjectDropdown.svelte
#: src/project/sync/FwLiteToFwMergeDetails.svelte
Expand Down Expand Up @@ -2142,10 +2156,7 @@ msgid "Unable to open in FieldWorks"
msgstr "Unable to open in FieldWorks"

#. Fallback value shown when author name or last-change date is unavailable (e.g., in activity history or the sync panel).
#: src/lib/activity/ActivityFilter.svelte
#: src/lib/activity/ActivityFilter.svelte
#: src/lib/activity/ActivityItem.svelte
#: src/lib/activity/ActivityView.svelte
#: src/lib/activity/AuthorLabel.svelte
#: src/lib/activity/utils.ts
#: src/project/sync/FwLiteToFwMergeDetails.svelte
#: src/project/sync/FwLiteToFwMergeDetails.svelte
Expand Down Expand Up @@ -2245,6 +2256,32 @@ msgstr "Updates"
msgid "Uses components as"
msgstr "Uses components as"

#. Used two ways: (1) button label for adding an entry to the "Variants" field, and (2) disable-reason shown in the entry picker when an entry is already linked as a variant, so it can't be picked again
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/Variants.svelte
#: src/lib/entry-editor/field-editors/Variants.svelte
msgid "Variant"
msgstr "Variant"

#. Used three ways: (1) field label for the "Variant of" relationship (this entry is a variant of another entry/sense), (2) button label in that field's entry picker, and (3) disable-reason shown when an entry is already linked, so it can't be picked again
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/VariantOf.svelte
#: src/lib/entry-editor/field-editors/Variants.svelte
#: src/lib/views/entity-config.ts
msgid "Variant of"
msgstr "Variant of"

#. Dropdown submenu label in a variant's context menu, for assigning its variant type (a user-configurable classification, e.g. dialectal or spelling variant)
#: src/lib/entry-editor/field-editors/VariantTypesMenuItems.svelte
msgid "Variant type"
msgstr "Variant type"

#. Field label: other entries that are variants of this entry (the reverse of "Variant of")
#: src/lib/views/entity-config.ts
msgid "Variants"
msgstr "Variants"

#. Section heading in custom view writing system picker.
#. Labels the vernacular language writing systems (the language being documented).
#: src/lib/views/custom/CustomViewWritingSystems.svelte
Expand Down
Loading
Loading