* Add customization URL parameter
* fix: Preserve should be customizeable
* Update customizations docs
* fix: Overlay items on patient name
* Add customization test
* Fix resolve to absolute path
* fix: Warn on no data in load
* Remove unused customization stuff
* fix: PR comments
* Update stored parameters to only use an array for mulitples
* Remove requires ohif.* special call out
* Remove strict mode
* PR comments
* Document segmentation examples
* Add three examples as requested
* PR comments
* lock
* Remove old customizatoin export
* fix: Ordering issues on customization loads
* fix: Use correct default for dev builds app config
* Fixes for conflicts
* chore: restore pnpm-lock.yaml to match master
The lockfile diff was incidental peer-descriptor churn and carried no
functional dependency change. It tripped the CircleCI security-audit gate
(which only runs when pnpm-lock.yaml is in the PR diff), surfacing a
pre-existing critical `decompress` transitive vuln that also exists on
master. Restoring master's lockfile removes the audit trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ci): restore json5 lockfile entry; ignore unfixable decompress GHSA
The previous commit restored pnpm-lock.yaml from master, which dropped the
json5@2.2.3 entry that platform/core legitimately depends on (JSONC parsing
for the customization feature). That broke `--frozen-lockfile` install
(ERR_PNPM_OUTDATED_LOCKFILE). This restores the correct lockfile.
Because the lockfile must change (json5), the CircleCI security-audit gate
runs and previously failed on a critical `decompress` <=4.2.1 zip-slip
advisory. This is a pre-existing transitive vuln (present on master too) with
no published patch — decompress's latest release is 4.2.1, so no version
bump/override can resolve it. It reaches the tree only via @itk-wasm/dam, a
build/data-asset extraction tool under @cornerstonejs/labelmap-interpolation.
Add GHSA-mp2f-45pm-3cg9 to the existing pnpm-workspace.yaml auditConfig
ignoreGhsas accepted-risk list, matching how the repo already exempts other
build-tooling advisories. `pnpm audit --audit-level high` now passes locally
(1 critical ignored, 0 high).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(e2e): fix visitStudy URL encoding that broke mpr2 study load
The visitStudy rewrite (added for the ?customization= option) built the URL
with new URLSearchParams({ StudyInstanceUIDs: studyInstanceUID }), which
percent-encodes the value. mpr2.spec.ts embeds an extra param in the UID
string ('<uid>&hangingprotocolid=mpr'), so the & and = were encoded and the
whole thing collapsed into one invalid StudyInstanceUIDs value -> the study
could not be found ('studies are not available'), the viewer never rendered,
and the side-panel-header-right click timed out.
Restore master's raw concatenation for StudyInstanceUIDs (so embedded params
survive as separate query params) while still appending the customization
option separately. Only mpr2 embeds & in the UID, matching the single failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* PR comments
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
import React from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { Button, Header, Icons, useModal } from '@ohif/ui-next';
|
|
import { useSystem } from '@ohif/core';
|
|
import { Toolbar } from '../Toolbar/Toolbar';
|
|
import HeaderPatientInfo from './HeaderPatientInfo';
|
|
import { PatientInfoVisibility } from './HeaderPatientInfo/HeaderPatientInfo';
|
|
import { preserveQueryParameters } from '@ohif/app';
|
|
import { Types } from '@ohif/core';
|
|
|
|
function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }>) {
|
|
const { servicesManager, extensionManager, commandsManager } = useSystem();
|
|
const { customizationService } = servicesManager.services;
|
|
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const onClickReturnButton = () => {
|
|
const { pathname } = location;
|
|
const dataSourceIdx = pathname.indexOf('/', 1);
|
|
|
|
const dataSourceName = pathname.substring(dataSourceIdx + 1);
|
|
const existingDataSource = extensionManager.getDataSources(dataSourceName);
|
|
|
|
const searchQuery = new URLSearchParams();
|
|
if (dataSourceIdx !== -1 && existingDataSource) {
|
|
searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1));
|
|
}
|
|
preserveQueryParameters(searchQuery, customizationService);
|
|
|
|
navigate({
|
|
pathname: '/',
|
|
search: decodeURIComponent(searchQuery.toString()),
|
|
});
|
|
};
|
|
|
|
const { t } = useTranslation();
|
|
const { show } = useModal();
|
|
|
|
const AboutModal = customizationService.getCustomization(
|
|
'ohif.aboutModal'
|
|
) as Types.MenuComponentCustomization;
|
|
|
|
const AppearanceModal = customizationService.getCustomization(
|
|
'ohif.appearanceModal'
|
|
) as Types.MenuComponentCustomization;
|
|
|
|
const UserPreferencesModal = customizationService.getCustomization(
|
|
'ohif.userPreferencesModal'
|
|
) as Types.MenuComponentCustomization;
|
|
|
|
const menuOptions = [
|
|
{
|
|
title: AboutModal?.menuTitle ?? t('Header:About'),
|
|
icon: 'info',
|
|
onClick: () =>
|
|
show({
|
|
content: AboutModal,
|
|
title: AboutModal?.title ?? t('AboutModal:About OHIF Viewer'),
|
|
containerClassName: AboutModal?.containerClassName ?? 'max-w-md',
|
|
}),
|
|
},
|
|
{
|
|
title: UserPreferencesModal.menuTitle ?? t('Header:Preferences'),
|
|
icon: 'settings',
|
|
onClick: () =>
|
|
show({
|
|
content: UserPreferencesModal,
|
|
title: UserPreferencesModal.title ?? t('UserPreferencesModal:User preferences'),
|
|
containerClassName:
|
|
UserPreferencesModal?.containerClassName ?? 'flex max-w-4xl p-6 flex-col',
|
|
}),
|
|
},
|
|
];
|
|
|
|
if (AppearanceModal) {
|
|
menuOptions.splice(1, 0, {
|
|
title: AppearanceModal.menuTitle ?? t('Header:Appearance'),
|
|
icon: 'ColorChange',
|
|
onClick: () =>
|
|
show({
|
|
content: AppearanceModal,
|
|
title: AppearanceModal.title ?? t('AppearanceModal:Appearance'),
|
|
containerClassName: AppearanceModal.containerClassName ?? 'max-w-md',
|
|
}),
|
|
});
|
|
}
|
|
|
|
if (appConfig.oidc) {
|
|
menuOptions.push({
|
|
title: t('Header:Logout'),
|
|
icon: 'power-off',
|
|
onClick: async () => {
|
|
navigate(`/logout?redirect_uri=${encodeURIComponent(window.location.href)}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Header
|
|
menuOptions={menuOptions}
|
|
isReturnEnabled={!!appConfig.showStudyList}
|
|
onClickReturnButton={onClickReturnButton}
|
|
WhiteLabeling={appConfig.whiteLabeling}
|
|
Secondary={<Toolbar buttonSection="secondary" />}
|
|
PatientInfo={
|
|
appConfig.showPatientInfo !== PatientInfoVisibility.DISABLED && (
|
|
<HeaderPatientInfo
|
|
servicesManager={servicesManager}
|
|
appConfig={appConfig}
|
|
/>
|
|
)
|
|
}
|
|
UndoRedo={
|
|
<div className="text-primary flex cursor-pointer items-center">
|
|
<Button
|
|
variant="ghost"
|
|
className="hover:bg-muted"
|
|
data-cy="undo-btn"
|
|
onClick={() => {
|
|
commandsManager.run('undo');
|
|
}}
|
|
>
|
|
<Icons.Undo className="" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
className="hover:bg-muted"
|
|
data-cy="redo-btn"
|
|
onClick={() => {
|
|
commandsManager.run('redo');
|
|
}}
|
|
>
|
|
<Icons.Redo className="" />
|
|
</Button>
|
|
</div>
|
|
}
|
|
>
|
|
<div className="relative flex justify-center gap-[4px]">
|
|
<Toolbar buttonSection="primary" />
|
|
</div>
|
|
</Header>
|
|
);
|
|
}
|
|
|
|
export default ViewerHeader;
|