diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx index f09c0ee09da..41a2fcd560c 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx @@ -46,11 +46,23 @@ import { AppResourcesTab, useEditorTabs } from './Tabs'; import { getScope } from './tree'; import type { ScopedAppResourceDir } from './types'; import { appResourceSubTypes } from './types'; +import { replaceViewsetNameInXml } from './xmlUtils'; export const AppResourceContext = React.createContext< SpecifyResource >(undefined!); +const syncViewsetNameInXml = ( + data: string | null | undefined, + appResource: SpecifyResource +): string => { + const viewSet = toTable(appResource, 'SpViewSetObj'); + const name = viewSet?.get('name'); + if (typeof data !== 'string') return data ?? ''; + if (typeof name !== 'string' || name.length === 0) return data; + return replaceViewsetNameInXml(data, name); +}; + export function AppResourceEditor({ resource, directory, @@ -291,9 +303,13 @@ export function AppResourceEditor({ typeof lastDataRef.current === 'function' ? lastDataRef.current() : lastDataRef.current; + const syncedData = syncViewsetNameInXml( + data === undefined ? resourceData.data : data, + appResource + ); const appResourceData = deserializeResource({ ...resourceData, - data: data === undefined ? resourceData.data : data, + data: syncedData, spAppResource: toTable(appResource, 'SpAppResource')?.get( 'resource_uri' diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx index 32e9a169c07..a45b9ee21b7 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx @@ -32,6 +32,7 @@ import type { AppResourcesOutlet } from './index'; import { globalResourceKey } from './tree'; import type { ScopedAppResourceDir } from './types'; import { appResourceSubTypes } from './types'; +import { replaceViewsetNameInXml } from './xmlUtils'; export function AppResourceView(): JSX.Element { return ; @@ -197,13 +198,6 @@ function useAppResource( ); } -/* - * REFACTOR: - * Split this function up. - * Currently, the resource is not needed until subtype needs to be determined. - * All the functionality that does not depend on resource should be part of a different - * function. - */ function useInitialData( resource: SerializedResource, initialDataFrom: number | undefined, @@ -211,9 +205,14 @@ function useInitialData( ): string | false | undefined { return useAsyncState( React.useCallback(async () => { + const replaceViewsetName = (data: string | null | undefined): string => { + const resourceName = (resource as any)?.name ?? ''; + return replaceViewsetNameInXml(data, resourceName); + }; + if (typeof initialDataFrom === 'number') return fetchResource('SpAppResourceData', initialDataFrom).then( - ({ data }) => data ?? '' + ({ data }) => replaceViewsetName(data ?? '') ); else if (typeof templateFile === 'string') { if (templateFile.includes('..')) @@ -224,7 +223,7 @@ function useInitialData( return ajax(`/static/config/${templateFile}`, { headers: {}, }) - .then(({ data }) => data ?? '') + .then(({ data }) => replaceViewsetName(data)) .catch(() => ''); } const subType = f.maybe( @@ -239,11 +238,9 @@ function useInitialData( if (useTemplate) return ajax(getAppResourceUrl(type.name, 'quiet'), { headers: {}, - }).then(({ data }) => data); + }).then(({ data }) => replaceViewsetName(data)); } return false; - // Run this only once - // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialDataFrom, templateFile]), false )[0]; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts new file mode 100644 index 00000000000..a64dd02f6fa --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/AppResources/xmlUtils.ts @@ -0,0 +1,21 @@ +// Shared utilities for XML manipulation in AppResources +export const escapeXml = (value: string): string => + value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); + +// Replace the viewset name attribute in XML content +export const replaceViewsetNameInXml = ( + data: string | null | undefined, + resourceName: string +): string => { + const xml = data ?? ''; + if (typeof resourceName !== 'string' || resourceName.length === 0) return xml; + return xml.replace( + /(]*\bname=)(")(.*?)\2/, + (_match, prefix, quote) => + `${prefix}${quote}${escapeXml(resourceName)}${quote}` + ); +};