Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
b382a99
test(schema-config): add store and redirect tests
grantfitzsimmons Aug 14, 2026
31e14c9
fix(schema-config): handle trailing slash in table route
grantfitzsimmons Aug 14, 2026
2e2869c
fix(schema-config): use non-splat table route
grantfitzsimmons Aug 14, 2026
b91b711
fix(db-viewer): use caption terminology over labels
grantfitzsimmons Aug 14, 2026
010f695
feat(schema-config): replace fields select with sortable tables
grantfitzsimmons Aug 14, 2026
15d8f1d
fix(db-viewer): remove unused reportsText imports
grantfitzsimmons Aug 14, 2026
b73c755
refactor(schema-config): show visibility icons and sort by visibility
grantfitzsimmons Aug 14, 2026
ab5991e
fix(schema-config): keep alphabetical secondary sort and italicize hi…
grantfitzsimmons Aug 14, 2026
3b0fb01
fix(schema-config): italicize entire hidden field row
grantfitzsimmons Aug 14, 2026
7dacda3
fix(schema-config): align field and relationship column widths
grantfitzsimmons Aug 14, 2026
c3ffd14
feat(schema-config): widen fields column
grantfitzsimmons Aug 14, 2026
e5a69dd
fix(schema-config): force-wrap field name and caption cells
grantfitzsimmons Aug 14, 2026
5bd752d
fix(schema-config): prevent scrollbar flicker in columns
grantfitzsimmons Aug 14, 2026
49220a9
fix(schema-config): reserve width for visible column header
grantfitzsimmons Aug 14, 2026
4081af2
fix(schema-config): use persistent scrollbars in remaining containers
grantfitzsimmons Aug 14, 2026
9faeee9
fix(schema-config): narrow visible column and wrap sortable headers
grantfitzsimmons Aug 14, 2026
3ee9f85
fix(schema-config): wrap text gracefully with overflow-wrap anywhere
grantfitzsimmons Aug 14, 2026
117e08f
feat(schema-config): add collapsible tables sidebar
grantfitzsimmons Aug 14, 2026
3565481
fix(schema-config): clarify collapse and expand tooltips
grantfitzsimmons Aug 14, 2026
98c3157
fix(schema-config): size collapsed sidebar rail to icon
grantfitzsimmons Aug 14, 2026
b421238
refactor(schema-config): use scrollbar gutter instead of forced scrol…
grantfitzsimmons Aug 14, 2026
91861c0
fix(schema-config): guard table lookup with optional chaining
grantfitzsimmons Aug 14, 2026
508d8c6
perf(schema-config): precompute field index lookups
grantfitzsimmons Aug 14, 2026
c3690c7
refactor(schema-config): remove stray JSX expression
grantfitzsimmons Aug 14, 2026
7950e8e
fix(schema-config): keep unload protection until save succeeds
grantfitzsimmons Aug 14, 2026
a4ead22
fix(schema-config): handle table load errors
grantfitzsimmons Aug 14, 2026
7d874d6
fix(schema-config): only mark auto-scroll after successful scroll
grantfitzsimmons Aug 14, 2026
a7f1010
fix(schema-config): reset field index synchronously on table change
grantfitzsimmons Aug 14, 2026
fd21d0e
fix(schema-config): fix button sizing
grantfitzsimmons Aug 16, 2026
6260c4a
fix(schema-config): keep selected field name in normal color
grantfitzsimmons Aug 16, 2026
1f4e5b7
test(schema-config): add bulk-save store tests
grantfitzsimmons Aug 16, 2026
abf2f4d
fix(schema-config): refresh formatters, aggregators, and pick lists a…
grantfitzsimmons Aug 16, 2026
b450fce
style(schema-config): put fields list in outline
grantfitzsimmons Aug 16, 2026
8aecf1f
feat(schema-config): add shared store for unsaved table edits
grantfitzsimmons Aug 14, 2026
8955a50
feat(schema-config): add table sidebar with bulk save
grantfitzsimmons Aug 14, 2026
d8b71d7
style(schema-config): make fields list denser
grantfitzsimmons Aug 16, 2026
37dd72f
fix(schema-config): reconcile partial save failures
grantfitzsimmons Aug 17, 2026
c595e20
test(schema-config): assert redirect target table name
grantfitzsimmons Aug 17, 2026
43ea209
fix(schema-config): guard against stale schema refresh
grantfitzsimmons Aug 17, 2026
dc97c49
refactor(schema-config): deduplicate change detection and reuse exist…
grantfitzsimmons Aug 17, 2026
8456587
feat(schema-config): navigate fields list with arrow keys
grantfitzsimmons Aug 17, 2026
c087911
fix(schema-config): shrink sort indicator so header labels don't wrap
grantfitzsimmons Aug 17, 2026
677233e
fix(schema-config): widen visible column so header fits on one line
grantfitzsimmons Aug 17, 2026
6639d82
refactor(schema-config): use icon for visible column header
grantfitzsimmons Aug 17, 2026
b0a00f5
fix(schema-config): show tooltip on sortable headers
grantfitzsimmons Aug 17, 2026
6caebc6
feat(schema-config): show related table icons on relationship fields
grantfitzsimmons Aug 17, 2026
cb12ac7
feat(schema-config): jump to field by typing its name
grantfitzsimmons Aug 17, 2026
bcdba22
fix(schema-config): reconcile saves against latest editor state
grantfitzsimmons Aug 17, 2026
68090e5
fix(schema-config): clarify captions and unsaved-change badge
grantfitzsimmons Aug 17, 2026
5da1b21
fix(schema-config): harden field list and formatter refresh
grantfitzsimmons Aug 17, 2026
0b570b2
Merge branch 'main' into issue-8420
grantfitzsimmons Aug 17, 2026
47a4954
fix(schema-config): respond to code review comments
grantfitzsimmons Aug 17, 2026
b4e1116
fix(schema-config): prevent stale updates
grantfitzsimmons Aug 18, 2026
bb4e171
fix(schema-config): remove unused sortByHiddenFields string
grantfitzsimmons Aug 18, 2026
d5d9dd4
Merge branch 'main' into issue-8420
grantfitzsimmons Aug 18, 2026
ed86391
Merge branch 'main' into issue-8420
grantfitzsimmons Aug 19, 2026
307b3dc
refactor(schema-config): extract store types and save helpers
grantfitzsimmons Aug 20, 2026
2a36472
refactor(schema-config): rename fieldLength to characterLimit
grantfitzsimmons Aug 20, 2026
67e7170
feat(schema-config): add human friendly labels for field types
grantfitzsimmons Aug 20, 2026
3afd103
fix(schema-config): show timestamp
grantfitzsimmons Aug 20, 2026
212e235
fix(schema-config): make edits during save reconcile
grantfitzsimmons Aug 20, 2026
9fcf7c2
Merge branch 'main' into issue-8420
grantfitzsimmons Aug 25, 2026
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 @@ -99,7 +99,7 @@ export const saveResource = async <TABLE_NAME extends keyof Tables>(
tableName: TABLE_NAME,
id: number,
data: DeepPartial<SerializedResource<Tables[TABLE_NAME]>>,
handleConflict: (() => void) | void
handleConflict?: () => void
): Promise<SerializedResource<Tables[TABLE_NAME]>> =>
ajax<SerializedRecord<Tables[TABLE_NAME]>>(
`/api/specify/${tableName.toLowerCase()}/${id}/`,
Expand Down
62 changes: 45 additions & 17 deletions specifyweb/frontend/js_src/lib/components/FieldFormatters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,52 @@ import type { FieldFormatter, FieldFormatterPart } from './spec';
import { fieldFormattersSpec, trimRegexString } from './spec';

let uiFormatters: IR<UiFormatter>;
export const fetchContext = Promise.all([
load<Element>(getAppResourceUrl('UIFormatters'), 'text/xml'),
import('../DataModel/tables').then(async ({ fetchContext }) => fetchContext),
]).then(([formatters]) => {
uiFormatters = Object.fromEntries(
filterArray(
xmlToSpec(formatters, fieldFormattersSpec()).fieldFormatters.map(
(formatter, index) => {
const resolvedFormatter = resolveFieldFormatter(formatter, index);
return resolvedFormatter === undefined
? undefined
: [formatter.name, resolvedFormatter];
}
let uiFormattersGeneration = 0;

const loadUiFormatters = (refresh = false): Promise<IR<UiFormatter>> => {
const generation = ++uiFormattersGeneration;
return Promise.all([
load<Element>(getAppResourceUrl('UIFormatters'), 'text/xml', refresh),
import('../DataModel/tables').then(
async ({ fetchContext }) => fetchContext
),
]).then(([formatters]) => {
const result = Object.fromEntries(
filterArray(
xmlToSpec(formatters, fieldFormattersSpec()).fieldFormatters.map(
(formatter, index) => {
const resolvedFormatter = resolveFieldFormatter(formatter, index);
return resolvedFormatter === undefined
? undefined
: [formatter.name, resolvedFormatter];
}
)
)
)
);
return uiFormatters;
});
);
if (generation === uiFormattersGeneration) uiFormatters = result;
return result;
});
};

export let fetchContext = loadUiFormatters();

// Re-fetch UI formatters after the app resource is edited
export const refreshUiFormatters = async (): Promise<
Awaited<typeof fetchContext>
> => {
const previous = fetchContext;
const refreshed = loadUiFormatters(true);
const generation = uiFormattersGeneration;
try {
const result = await refreshed;
if (generation === uiFormattersGeneration) fetchContext = refreshed;
return result;
} catch (error) {
// Keep the previous UI formatters on failure rather than a rejected promise
if (generation === uiFormattersGeneration) fetchContext = previous;
throw error;
Comment thread
grantfitzsimmons marked this conversation as resolved.
}
};
export const getUiFormatters = (): typeof uiFormatters =>
uiFormatters ?? error('Tried to access UI formatters before fetching them');

Expand Down
47 changes: 35 additions & 12 deletions specifyweb/frontend/js_src/lib/components/Formatters/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,43 @@ import { fieldFormat } from './fieldFormat';
import type { Aggregator, Formatter } from './spec';
import { formattersSpec } from './spec';

export const fetchFormatters: Promise<{
const loadFormatters = (
refresh = false
): Promise<{
readonly formatters: RA<Formatter>;
readonly aggregators: RA<Aggregator>;
}> = contextUnlockedPromise.then(async (entrypoint) =>
entrypoint === 'main'
? Promise.all([
ajax<Element>(cacheableUrl(getAppResourceUrl('DataObjFormatters')), {
headers: { Accept: 'text/xml' },
}).then(({ data }) => data),
fetchSchema,
fetchDomain,
]).then(([definitions]) => xmlToSpec(definitions, formattersSpec()))
: foreverFetch()
);
}> =>
contextUnlockedPromise.then(async (entrypoint) =>
entrypoint === 'main'
? Promise.all([
ajax<Element>(cacheableUrl(getAppResourceUrl('DataObjFormatters')), {
headers: { Accept: 'text/xml' },
cache: refresh ? 'no-cache' : undefined,
}).then(({ data }) => data),
fetchSchema,
fetchDomain,
]).then(([definitions]) => xmlToSpec(definitions, formattersSpec()))
: foreverFetch()
);

export let fetchFormatters = loadFormatters();

// Re-fetch formatters and aggregators after the app resource is edited
export const refreshFormatters = async (): Promise<
Awaited<typeof fetchFormatters>
> => {
const previous = fetchFormatters;
const refreshed = loadFormatters(true);
try {
const result = await refreshed;
fetchFormatters = refreshed;
return result;
} catch (error) {
// Keep the previous formatters on failure rather than a rejected promise
fetchFormatters = previous;
throw error;
}
};

export const naiveFormatter = (
tableLabel: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export const foreverFetch = async <T>(): Promise<T> => foreverPromise;
export const unlockInitialContext = (entrypoint: typeof entrypointName): void =>
unlock(entrypoint);

export const load = async <T>(path: string, mimeType: MimeType): Promise<T> =>
export const load = async <T>(
path: string,
mimeType: MimeType,
refresh = false
): Promise<T> =>
contextUnlockedPromise.then(async (entrypoint) => {
if (entrypoint !== 'main') return foreverFetch<T>();

Expand All @@ -60,6 +64,7 @@ export const load = async <T>(path: string, mimeType: MimeType): Promise<T> =>
const { data } = await ajax<T>(cacheableUrl(path), {
errorMode: 'visible',
headers: { Accept: mimeType },
cache: refresh ? 'no-cache' : undefined,
});

return data;
Expand Down
8 changes: 6 additions & 2 deletions specifyweb/frontend/js_src/lib/components/Router/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,17 @@ export const routes: RA<EnhancedRoute> = [
},
{
path: ':language',
element: () =>
import('../SchemaConfig/Layout').then(
({ SchemaConfigLayout }) => SchemaConfigLayout
),
children: [
{
index: true,
title: schemaText.tables(),
element: () =>
import('../SchemaConfig/Tables').then(
({ SchemaConfigTables }) => SchemaConfigTables
import('../SchemaConfig/Redirect').then(
({ SchemaConfigRedirect }) => SchemaConfigRedirect
),
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import type { LocalizedString } from 'typesafe-i18n';

import { commonText } from '../../localization/common';
Expand All @@ -11,18 +10,20 @@ import { H2 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { className } from '../Atoms/className';
import { Select } from '../Atoms/Form';
import { Link } from '../Atoms/Link';
import { formatUrl } from '../Router/queryString';
import type { SchemaData } from './schemaData';

export function SchemaConfigHeader({
languages,
language,
rawLanguage,
onSave: handleSave,
}: {
readonly languages: SchemaData['languages'];
readonly language: string;
readonly rawLanguage: string;
readonly onSave: (() => void) | undefined;
}): JSX.Element {
const navigate = useNavigate();
const [language] = rawLanguage.split('-');
return (
<header className="flex gap-2">
<H2 className="flex items-center">
Expand All @@ -32,11 +33,14 @@ export function SchemaConfigHeader({
})`
)}
</H2>
<Button.Small
onClick={(): void => navigate(`/specify/schema-config/${language}/`)}
<Link.Small
download={`schema_localization_${rawLanguage}.json`}
href={formatUrl('/context/schema_localization.json', {
lang: rawLanguage,
})}
>
{schemaText.changeBaseTable()}
</Button.Small>
{commonText.export()}
</Link.Small>
<span className="-ml-2 flex-1" />
<Button.Small variant={className.saveButton} onClick={handleSave}>
{commonText.save()}
Expand Down Expand Up @@ -100,7 +104,6 @@ export function PickList({
</optgroup>
))
)}
{}
</>
)}
</Select>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function SchemaConfigField({
/>
</Label.Block>
<Label.Block>
{schemaText.fieldLength()}
{schemaText.characterLimit()}
<Input.Integer isReadOnly value={field.length ?? ''} />
</Label.Block>
<Label.Block>
Expand Down
Loading
Loading