diff --git a/frontend/viewer/src/lib/components/editor/field/field-root.svelte b/frontend/viewer/src/lib/components/editor/field/field-root.svelte index 8312c63cc7..d16579b508 100644 --- a/frontend/viewer/src/lib/components/editor/field/field-root.svelte +++ b/frontend/viewer/src/lib/components/editor/field/field-root.svelte @@ -1,6 +1,6 @@ - - - + + + onFieldChanged('lexemeForm')} @@ -58,8 +60,8 @@ - - + + onFieldChanged('citationForm')} @@ -70,8 +72,8 @@ {#if !modalMode} - - + + onFieldChanged('complexForms')} bind:value={entry.complexForms} @@ -80,8 +82,8 @@ - - + + onFieldChanged('components')} @@ -91,8 +93,8 @@ - - + + onFieldChanged('complexFormTypes')} @@ -106,8 +108,8 @@ {/if} - - + + onFieldChanged('literalMeaning')} @@ -117,8 +119,8 @@ - - + + onFieldChanged('note')} @@ -128,8 +130,8 @@ - - + + onFieldChanged('publishIn')} diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte index bbae01813b..627e74f397 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/ExampleEditorPrimitive.svelte @@ -3,7 +3,7 @@ import {objectTemplateAreas, useCurrentView} from '$lib/views/view-service'; import * as Editor from '$lib/components/editor'; import {asString, useWritingSystemService} from '$project/data'; - import {fieldData, type FieldId} from '../field-data'; + import {fieldData, type ExampleFieldId} from '../../views/fields'; import {cn, draftTranslation, isDraft} from '$lib/utils'; import {vt} from '$lib/views/view-text'; import {t} from 'svelte-i18n-lingui'; @@ -15,7 +15,7 @@ interface Props extends Omit { example: IExampleSentence; readonly?: boolean; - onchange?: (sense: IExampleSentence, field: FieldId) => void; + onchange?: (sense: IExampleSentence, field: ExampleFieldId) => void; } const { @@ -29,14 +29,16 @@ const currentView = useCurrentView(); initSubjectContext(() => example); - function onFieldChanged(field: FieldId) { + function onFieldChanged(field: ExampleFieldId) { onchange?.(example, field); } + + const fields = $derived($currentView.fields.example); - - - + + + onFieldChanged('sentence')} @@ -46,11 +48,11 @@ - + {#each (example.translations.length ? example.translations : [draftTranslation(example)]) as translation, i (translation.id)} {@const title = example.translations.length > 1 ? vt($t`Translation ${i + 1}`) : vt($t`Translation`)} - + { @@ -68,8 +70,8 @@ {#if writingSystemService.defaultAnalysis} - - + + onFieldChanged('reference')} diff --git a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte index 8d9d54adcc..d2e20f0afb 100644 --- a/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte +++ b/frontend/viewer/src/lib/entry-editor/object-editors/SenseEditorPrimitive.svelte @@ -11,12 +11,12 @@ import {mergeProps} from 'bits-ui'; import type {Snippet} from 'svelte'; import {t} from 'svelte-i18n-lingui'; - import {fieldData, type FieldId} from '../field-data'; + import {fieldData, type SenseFieldId} from '../../views/fields'; interface Props extends Omit { sense: ISense; readonly?: boolean; - onchange?: (sense: ISense, field: FieldId) => void; + onchange?: (sense: ISense, field: SenseFieldId) => void; partOfSpeechDescription?: Snippet; semanticDomainsDescription?: Snippet; }; @@ -35,14 +35,16 @@ const semanticDomains = useSemanticDomains(); const currentView = useCurrentView(); initSubjectContext(() => sense); - function onFieldChanged(field: FieldId) { + function onFieldChanged(field: SenseFieldId) { onchange?.(sense, field); } + + const fields = $derived($currentView.fields.sense); - - - + + + onFieldChanged('gloss')} @@ -52,8 +54,8 @@ - - + + onFieldChanged('definition')} @@ -63,8 +65,8 @@ - - + + import {initView, useCurrentView} from '$lib/views/view-service'; - import type {FieldId} from '$lib/entry-editor/field-data'; - import type {FieldView, Overrides} from './view-data'; + import type {EntityViewFields, FieldView, Overrides} from './view-data'; import type {Snippet} from 'svelte'; import {watch} from 'runed'; + import type {FieldId} from './fields'; interface Props { shownFields?: FieldId[]; @@ -24,20 +24,27 @@ watch(() => [shownFields, respectOrder, $currentView, overrides] as const, ([shownFields, respectOrder, currentView, overrides]) => { $overrideView = { ...currentView, - fields: Object.fromEntries((Object.entries(currentView.fields) as Array<[FieldId, FieldView]>).map(([id, field]) => { - return [id, { - ...field, - show: shownFields.includes(id), - order: respectOrder ? shownFields.indexOf(id) : field.order - }]; - }) - ) as Record, + fields: { + entry: overrideEntityFields(currentView.fields.entry, shownFields, respectOrder), + sense: overrideEntityFields(currentView.fields.sense, shownFields, respectOrder), + example: overrideEntityFields(currentView.fields.example, shownFields, respectOrder), + }, overrides: { ...currentView.overrides, ...overrides } }; }); + + function overrideEntityFields(entityFields: T, shownFields: FieldId[], respectOrder: boolean): T { + return Object.fromEntries( + (Object.entries(entityFields) as [FieldId, FieldView][]).map(([id, field]) => [id, { + ...field, + show: shownFields.includes(id), + order: respectOrder ? shownFields.indexOf(id) : field.order + }]) + ) as T; + } {@render children?.()} diff --git a/frontend/viewer/src/lib/views/fields.ts b/frontend/viewer/src/lib/views/fields.ts new file mode 100644 index 0000000000..c66c609d50 --- /dev/null +++ b/frontend/viewer/src/lib/views/fields.ts @@ -0,0 +1,34 @@ +interface FieldData { + helpId: string; +} + +export const fieldData = { + entry: { + lexemeForm: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Lexeme_Form_field.htm' }, + citationForm: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Citation_Form_field.htm' }, + complexForms: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Complex_Forms.htm' }, + complexFormTypes: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Complex_Form_Type_field.htm' }, + components: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Components_field.htm' }, + literalMeaning: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Literal_Meaning_field.htm' }, + note: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Entry_level_fields/Note_field.htm' }, + publishIn: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Publication_Settings_flds/Publish_In_(Publication_Settings).htm' }, + }, + sense: { + gloss: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Gloss_field_Sense.htm' }, + definition: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/definition_field.htm' }, + partOfSpeechId: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Grammatical_Info_field.htm' }, + semanticDomains: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/semantic_domains_field.htm' }, + }, + example: { + sentence: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/example_field.htm' }, + translations: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Translation_field.htm' }, + reference: { helpId: 'User_Interface/Field_Descriptions/Lexicon/Lexicon_Edit_fields/Sense_level_fields/Reference_field.htm' }, + }, +} as const satisfies Record>; + +export type EntityType = keyof typeof fieldData; +export type EntityFields = keyof (typeof fieldData[T]); +export type EntryFieldId = EntityFields<'entry'>; +export type SenseFieldId = EntityFields<'sense'>; +export type ExampleFieldId = EntityFields<'example'>; +export type FieldId = EntryFieldId | SenseFieldId | ExampleFieldId; diff --git a/frontend/viewer/src/lib/views/view-data.ts b/frontend/viewer/src/lib/views/view-data.ts index 1aefcb2eca..a2cfa9aa35 100644 --- a/frontend/viewer/src/lib/views/view-data.ts +++ b/frontend/viewer/src/lib/views/view-data.ts @@ -1,35 +1,40 @@ -import type {FieldId} from '$lib/entry-editor/field-data'; +import type {EntityFields, EntityType, FieldId} from './fields'; + +import type {PartialDeep} from 'type-fest'; export interface FieldView { show: boolean; order: number; } -const defaultDef = Symbol('default spread values'); - -export const allFields: Record = { - //entry - lexemeForm: {show: true, order: 1}, - citationForm: {show: true, order: 2}, - complexForms: {show: true, order: 3}, - components: {show: true, order: 4}, - complexFormTypes: {show: false, order: 5}, - literalMeaning: {show: false, order: 6}, - note: {show: true, order: 7}, - publishIn: {show: false, order: 8}, - - //sense - gloss: {show: true, order: 1}, - definition: {show: true, order: 2}, - partOfSpeechId: {show: true, order: 3}, - semanticDomains: {show: true, order: 4}, - - //example sentence - sentence: {show: true, order: 1}, - translations: {show: true, order: 2}, - reference: {show: false, order: 3}, +export type ViewFields = {[T in EntityType]: {[F in EntityFields]: FieldView}}; + +export const allFields: ViewFields = { + entry: { + lexemeForm: {show: true, order: 1}, + citationForm: {show: true, order: 2}, + complexForms: {show: true, order: 3}, + components: {show: true, order: 4}, + complexFormTypes: {show: false, order: 5}, + literalMeaning: {show: false, order: 6}, + note: {show: true, order: 7}, + publishIn: {show: false, order: 8}, + }, + sense: { + gloss: {show: true, order: 1}, + definition: {show: true, order: 2}, + partOfSpeechId: {show: true, order: 3}, + semanticDomains: {show: true, order: 4}, + }, + example: { + sentence: {show: true, order: 1}, + translations: {show: true, order: 2}, + reference: {show: false, order: 3}, + }, }; +export type EntityViewFields = ViewFields[EntityType]; + export const FW_LITE_VIEW: RootView = { id: 'fwlite', type: 'fw-lite', @@ -42,10 +47,11 @@ export const FW_CLASSIC_VIEW: RootView = { id: 'fieldworks', type: 'fw-classic', label: 'FieldWorks Classic', - fields: recursiveSpread(allFields, { - complexFormTypes: {order: allFields.components.order - 0.1}, - [defaultDef]: {show: true} - }), + fields: { + entry: showAllFields(allFields.entry, {complexFormTypes: {order: allFields.entry.components.order - 0.1}}), + sense: showAllFields(allFields.sense), + example: showAllFields(allFields.example), + }, alternateView: FW_LITE_VIEW, }; @@ -56,37 +62,35 @@ const viewDefinitions: CustomViewDefinition[] = [ export const views: [RootView, RootView, ...CustomView[]] = [ FW_LITE_VIEW, FW_CLASSIC_VIEW, - ...viewDefinitions.map(view => { - const fields: Record = recursiveSpread(allFields, view.fieldOverrides); - return { - ...FW_LITE_VIEW, - ...view, - fields: fields - }; - }) + ...viewDefinitions.map(view => ({ + ...FW_LITE_VIEW, + ...view, + fields: mergeViewFields(allFields, view.fieldOverrides), + })) ]; -function recursiveSpread>(obj1: T, obj2: { [P in keyof T]?: Partial } & { [defaultDef]?: Partial }): T { - const result: Record = {...obj1}; - const defaultValues = obj2[defaultDef]; - if (defaultValues) { - for (const [key, value] of Object.entries(result)) { - if (typeof value === 'object' && value !== null) { - result[key] = {...value, ...defaultValues}; - } else { - result[key] = defaultValues; - } - } +function mergeFields(base: T, overrides?: Partial>>): T { + if (!overrides) return {...base}; + const result = {...base}; + for (const [id, override] of Object.entries(overrides) as [keyof T, Partial][]) { + result[id] = {...result[id], ...override}; } - for (const [key, value] of Object.entries(obj2)) { - const currentValue = result[key]; - if (typeof currentValue === 'object' && currentValue !== null && typeof value === 'object' && value !== null) { - result[key] = recursiveSpread(currentValue as Record, value as Record>); - } else { - result[key] = value; - } - } - return result as T; + return result; +} + +function mergeViewFields(base: ViewFields, overrides: CustomViewDefinition['fieldOverrides']): ViewFields { + return { + entry: mergeFields(base.entry, overrides.entry), + sense: mergeFields(base.sense, overrides.sense), + example: mergeFields(base.example, overrides.example), + }; +} + +function showAllFields(fields: T, overrides?: PartialDeep): T { + const allShown = Object.fromEntries( + Object.entries(fields).map(([id, field]) => [id, {...(field), show: true}]) + ) as T; + return mergeFields(allShown, overrides); } export type ViewType = 'fw-lite' | 'fw-classic'; @@ -103,12 +107,12 @@ export type Overrides = { }; interface CustomViewDefinition extends ViewDefinition { - fieldOverrides: Partial>>; + fieldOverrides: PartialDeep; parentView: RootView; } interface ViewBase extends ViewDefinition { - fields: Record; + fields: ViewFields; overrides?: Overrides } diff --git a/frontend/viewer/src/lib/views/view-service.ts b/frontend/viewer/src/lib/views/view-service.ts index 8e0068bc3b..d9f5e65ec4 100644 --- a/frontend/viewer/src/lib/views/view-service.ts +++ b/frontend/viewer/src/lib/views/view-service.ts @@ -1,5 +1,5 @@ import {type Writable, writable} from 'svelte/store'; -import {type View, views} from './view-data'; +import {type EntityViewFields, type View, views} from './view-data'; import {getContext, onDestroy, setContext} from 'svelte'; const currentViewContextName = 'currentView'; @@ -50,10 +50,9 @@ export function useViewSettings(): Writable { * looks like `"lexemeForm lexemeForm lexemeForm" "citationForm citationForm citationForm" "literalMeaning literalMeaning literalMeaning"` etc. each group of fields in quotes is a row. * there are 3 columns for each row and one field per row so the field name is repeated 3 times. */ -export function objectTemplateAreas(view: View, obj: object | string[]): string { - const fields = Array.isArray(obj) ? obj : Object.keys(obj); - return Object.entries(view.fields) - .filter(([id, field]) => fields.includes(id) && field.show) +export function objectTemplateAreas(fieldConfig: EntityViewFields): string { + return Object.entries(fieldConfig) + .filter(([, field]) => field.show) .sort((a, b) => a[1].order - b[1].order) - .map(([id, _field]) => `"${id} ${id} ${id}"`).join(' '); + .map(([id]) => `"${id} ${id} ${id}"`).join(' '); } diff --git a/frontend/viewer/src/project/tasks/tasks-service.ts b/frontend/viewer/src/project/tasks/tasks-service.ts index 2985ebc165..a66bd736cf 100644 --- a/frontend/viewer/src/project/tasks/tasks-service.ts +++ b/frontend/viewer/src/project/tasks/tasks-service.ts @@ -1,6 +1,6 @@ import {asString, useWritingSystemService, type WritingSystemService} from '$project/data'; import {useProjectContext} from '$project/project-context.svelte'; -import type {FieldId} from '$lib/entry-editor/field-data'; +import type {FieldId} from '$lib/views/fields'; import {gt} from 'svelte-i18n-lingui'; import type {IEntry, IExampleSentence, IRichString, ISense, IWritingSystem, WritingSystemType} from '$lib/dotnet-types'; import {defaultExampleSentence, defaultSense, firstTruthy, isEntry, isSense} from '$lib/utils'; diff --git a/frontend/viewer/src/stories/editor/fields/FieldDecorator.svelte b/frontend/viewer/src/stories/editor/fields/FieldDecorator.svelte index cba45f31c7..1dfcd9f368 100644 --- a/frontend/viewer/src/stories/editor/fields/FieldDecorator.svelte +++ b/frontend/viewer/src/stories/editor/fields/FieldDecorator.svelte @@ -1,7 +1,7 @@