init
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, useModal } from '@ohif/ui';
|
||||
import { Types } from '@ohif/core';
|
||||
import DataSourceConfigurationModalComponent from './DataSourceConfigurationModalComponent';
|
||||
|
||||
function DataSourceConfigurationComponent({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}: withAppTypes): ReactElement {
|
||||
const { t } = useTranslation('DataSourceConfiguration');
|
||||
const { show, hide } = useModal();
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const [configurationAPI, setConfigurationAPI] = useState<Types.BaseDataSourceConfigurationAPI>();
|
||||
|
||||
const [configuredItems, setConfiguredItems] =
|
||||
useState<Array<Types.BaseDataSourceConfigurationAPIItem>>();
|
||||
|
||||
useEffect(() => {
|
||||
let shouldUpdate = true;
|
||||
|
||||
const dataSourceChangedCallback = async () => {
|
||||
const activeDataSourceDef = extensionManager.getActiveDataSourceDefinition();
|
||||
|
||||
if (!activeDataSourceDef.configuration.configurationAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { factory: configurationAPIFactory } =
|
||||
customizationService.get(activeDataSourceDef.configuration.configurationAPI) ?? {};
|
||||
|
||||
if (!configurationAPIFactory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configAPI = configurationAPIFactory(activeDataSourceDef.sourceName);
|
||||
setConfigurationAPI(configAPI);
|
||||
|
||||
// New configuration API means that the existing configured items must be cleared.
|
||||
setConfiguredItems(null);
|
||||
|
||||
configAPI.getConfiguredItems().then(list => {
|
||||
if (shouldUpdate) {
|
||||
setConfiguredItems(list);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const sub = extensionManager.subscribe(
|
||||
extensionManager.EVENTS.ACTIVE_DATA_SOURCE_CHANGED,
|
||||
dataSourceChangedCallback
|
||||
);
|
||||
|
||||
dataSourceChangedCallback();
|
||||
|
||||
return () => {
|
||||
shouldUpdate = false;
|
||||
sub.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const showConfigurationModal = useCallback(() => {
|
||||
show({
|
||||
content: DataSourceConfigurationModalComponent,
|
||||
title: t('Configure Data Source'),
|
||||
contentProps: {
|
||||
configurationAPI,
|
||||
configuredItems,
|
||||
onHide: hide,
|
||||
},
|
||||
});
|
||||
}, [configurationAPI, configuredItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!configurationAPI || !configuredItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuredItems.length !== configurationAPI.getItemLabels().length) {
|
||||
// Not the correct number of configured items, so show the modal to configure the data source.
|
||||
showConfigurationModal();
|
||||
}
|
||||
}, [configurationAPI, configuredItems, showConfigurationModal]);
|
||||
|
||||
return configuredItems ? (
|
||||
<div className="text-aqua-pale flex items-center overflow-hidden">
|
||||
<Icon
|
||||
name="settings"
|
||||
className="mr-2.5 h-3.5 w-3.5 shrink-0 cursor-pointer"
|
||||
onClick={showConfigurationModal}
|
||||
></Icon>
|
||||
{configuredItems.map((item, itemIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={itemIndex}
|
||||
className="flex overflow-hidden"
|
||||
>
|
||||
<div
|
||||
key={itemIndex}
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
{itemIndex !== configuredItems.length - 1 && <div className="px-2.5">|</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceConfigurationComponent;
|
||||
@@ -0,0 +1,195 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
import { Types } from '@ohif/core';
|
||||
import ItemListComponent from './ItemListComponent';
|
||||
|
||||
const NO_WRAP_ELLIPSIS_CLASS_NAMES = 'text-ellipsis whitespace-nowrap overflow-hidden';
|
||||
|
||||
type DataSourceConfigurationModalComponentProps = {
|
||||
configurationAPI: Types.BaseDataSourceConfigurationAPI;
|
||||
configuredItems: Array<Types.BaseDataSourceConfigurationAPIItem>;
|
||||
onHide: () => void;
|
||||
};
|
||||
|
||||
function DataSourceConfigurationModalComponent({
|
||||
configurationAPI,
|
||||
configuredItems,
|
||||
onHide,
|
||||
}: DataSourceConfigurationModalComponentProps) {
|
||||
const { t } = useTranslation('DataSourceConfiguration');
|
||||
|
||||
const [itemList, setItemList] = useState<Array<Types.BaseDataSourceConfigurationAPIItem>>();
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState(configuredItems);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const [itemLabels] = useState(configurationAPI.getItemLabels());
|
||||
|
||||
// Determines whether to show the full/existing configuration for the data source.
|
||||
// A full or complete configuration is one where the data source (path) has the
|
||||
// maximum/required number of path items. Anything less is considered not complete and
|
||||
// the configuration starts from scratch (i.e. as if no items are configured at all).
|
||||
// TODO: consider configuration starting from a partial (i.e. non-empty) configuration
|
||||
const [showFullConfig, setShowFullConfig] = useState(
|
||||
itemLabels.length === configuredItems.length
|
||||
);
|
||||
|
||||
/**
|
||||
* The index of the selected item that is considered current and for which
|
||||
* its sub-items should be displayed in the items list component. When the
|
||||
* full/existing configuration for a data source is to be shown, the current
|
||||
* selected item is the second to last in the `selectedItems` list.
|
||||
*/
|
||||
const currentSelectedItemIndex = showFullConfig
|
||||
? selectedItems.length - 2
|
||||
: selectedItems.length - 1;
|
||||
|
||||
useEffect(() => {
|
||||
let shouldUpdate = true;
|
||||
|
||||
setErrorMessage(null);
|
||||
|
||||
// Clear out the former/old list while we fetch the next sub item list.
|
||||
setItemList(null);
|
||||
|
||||
if (selectedItems.length === 0) {
|
||||
configurationAPI
|
||||
.initialize()
|
||||
.then(items => {
|
||||
if (shouldUpdate) {
|
||||
setItemList(items);
|
||||
}
|
||||
})
|
||||
.catch(error => setErrorMessage(error.message));
|
||||
} else if (!showFullConfig && selectedItems.length === itemLabels.length) {
|
||||
// The last item to configure the data source (path) has been selected.
|
||||
configurationAPI.setCurrentItem(selectedItems[selectedItems.length - 1]);
|
||||
// We can hide the modal dialog now.
|
||||
onHide();
|
||||
} else {
|
||||
configurationAPI
|
||||
.setCurrentItem(selectedItems[currentSelectedItemIndex])
|
||||
.then(items => {
|
||||
if (shouldUpdate) {
|
||||
setItemList(items);
|
||||
}
|
||||
})
|
||||
.catch(error => setErrorMessage(error.message));
|
||||
}
|
||||
|
||||
return () => {
|
||||
shouldUpdate = false;
|
||||
};
|
||||
}, [
|
||||
selectedItems,
|
||||
configurationAPI,
|
||||
onHide,
|
||||
itemLabels,
|
||||
showFullConfig,
|
||||
currentSelectedItemIndex,
|
||||
]);
|
||||
|
||||
const getSelectedItemCursorClasses = itemIndex =>
|
||||
itemIndex !== itemLabels.length - 1 && itemIndex < selectedItems.length
|
||||
? 'cursor-pointer'
|
||||
: 'cursor-auto';
|
||||
|
||||
const getSelectedItemBackgroundClasses = itemIndex =>
|
||||
itemIndex < selectedItems.length
|
||||
? classNames(
|
||||
'bg-black/[.4]',
|
||||
itemIndex !== itemLabels.length - 1 ? 'hover:bg-transparent active:bg-secondary-dark' : ''
|
||||
)
|
||||
: 'bg-transparent';
|
||||
|
||||
const getSelectedItemBorderClasses = itemIndex =>
|
||||
itemIndex === currentSelectedItemIndex + 1
|
||||
? classNames('border-2', 'border-solid', 'border-primary-light')
|
||||
: itemIndex < selectedItems.length
|
||||
? 'border border-solid border-primary-active hover:border-primary-light active:border-white'
|
||||
: 'border border-dashed border-secondary-light';
|
||||
|
||||
const getSelectedItemTextClasses = itemIndex =>
|
||||
itemIndex <= selectedItems.length ? 'text-primary-light' : 'text-primary-active';
|
||||
|
||||
const getErrorComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex min-h-[1px] grow flex-col gap-4">
|
||||
<div className="text-primary-light text-[20px]">
|
||||
{t(`Error fetching ${itemLabels[selectedItems.length]} list`)}
|
||||
</div>
|
||||
<div className="grow bg-black p-4 text-[14px]">{errorMessage}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getSelectedItemsComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{itemLabels.map((itemLabel, itemLabelIndex) => {
|
||||
return (
|
||||
<div
|
||||
key={itemLabel}
|
||||
className={classNames(
|
||||
'flex min-w-[1px] shrink basis-[200px] flex-col gap-1 rounded-md p-3.5',
|
||||
getSelectedItemCursorClasses(itemLabelIndex),
|
||||
getSelectedItemBackgroundClasses(itemLabelIndex),
|
||||
getSelectedItemBorderClasses(itemLabelIndex),
|
||||
getSelectedItemTextClasses(itemLabelIndex)
|
||||
)}
|
||||
onClick={
|
||||
(showFullConfig && itemLabelIndex < currentSelectedItemIndex) ||
|
||||
itemLabelIndex <= currentSelectedItemIndex
|
||||
? () => {
|
||||
setShowFullConfig(false);
|
||||
setSelectedItems(theList => theList.slice(0, itemLabelIndex));
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="text- flex items-center gap-2">
|
||||
{itemLabelIndex < selectedItems.length ? (
|
||||
<Icon name="status-tracked" />
|
||||
) : (
|
||||
<Icon name="status-untracked" />
|
||||
)}
|
||||
<div className={classNames(NO_WRAP_ELLIPSIS_CLASS_NAMES)}>{t(itemLabel)}</div>
|
||||
</div>
|
||||
{itemLabelIndex < selectedItems.length ? (
|
||||
<div className={classNames('text-[14px] text-white', NO_WRAP_ELLIPSIS_CLASS_NAMES)}>
|
||||
{selectedItems[itemLabelIndex].name}
|
||||
</div>
|
||||
) : (
|
||||
<br></br>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-300px)] select-none flex-col gap-4 pt-0.5">
|
||||
{getSelectedItemsComponent()}
|
||||
<div className="h-0.5 w-full shrink-0 bg-black"></div>
|
||||
{errorMessage ? (
|
||||
getErrorComponent()
|
||||
) : (
|
||||
<ItemListComponent
|
||||
itemLabel={itemLabels[currentSelectedItemIndex + 1]}
|
||||
itemList={itemList}
|
||||
onItemClicked={item => {
|
||||
setShowFullConfig(false);
|
||||
setSelectedItems(theList => [...theList.slice(0, currentSelectedItemIndex + 1), item]);
|
||||
}}
|
||||
></ItemListComponent>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceConfigurationModalComponent;
|
||||
86
extensions/default/src/Components/ItemListComponent.tsx
Normal file
86
extensions/default/src/Components/ItemListComponent.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Icon, InputFilterText, LoadingIndicatorProgress } from '@ohif/ui';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
type ItemListComponentProps = {
|
||||
itemLabel: string;
|
||||
itemList: Array<Types.BaseDataSourceConfigurationAPIItem>;
|
||||
onItemClicked: (item: Types.BaseDataSourceConfigurationAPIItem) => void;
|
||||
};
|
||||
|
||||
function ItemListComponent({
|
||||
itemLabel,
|
||||
itemList,
|
||||
onItemClicked,
|
||||
}: ItemListComponentProps): ReactElement {
|
||||
const { t } = useTranslation('DataSourceConfiguration');
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setFilterValue('');
|
||||
}, [itemList]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[1px] grow flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-primary-light text-[20px]">{t(`Select ${itemLabel}`)}</div>
|
||||
<InputFilterText
|
||||
className="max-w-[40%] grow"
|
||||
value={filterValue}
|
||||
onDebounceChange={setFilterValue}
|
||||
placeholder={t(`Search ${itemLabel} list`)}
|
||||
></InputFilterText>
|
||||
</div>
|
||||
<div className="relative flex min-h-[1px] grow flex-col bg-black text-[14px]">
|
||||
{itemList == null ? (
|
||||
<LoadingIndicatorProgress className={'h-full w-full'} />
|
||||
) : itemList.length === 0 ? (
|
||||
<div className="text-primary-light flex h-full flex-col items-center justify-center px-6 py-4">
|
||||
<Icon
|
||||
name="magnifier"
|
||||
className="mb-4"
|
||||
/>
|
||||
<span>{t(`No ${itemLabel} available`)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-secondary-dark px-3 py-1.5 text-white">{t(itemLabel)}</div>
|
||||
<div className="ohif-scrollbar overflow-auto">
|
||||
{itemList
|
||||
.filter(
|
||||
item =>
|
||||
!filterValue || item.name.toLowerCase().includes(filterValue.toLowerCase())
|
||||
)
|
||||
.map(item => {
|
||||
const border =
|
||||
'rounded border-transparent border-b-secondary-light border-[1px] hover:border-primary-light';
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'hover:text-primary-light hover:bg-primary-dark group mx-2 flex items-center justify-between px-6 py-2',
|
||||
border
|
||||
)}
|
||||
key={item.id}
|
||||
>
|
||||
<div>{item.name}</div>
|
||||
<Button
|
||||
onClick={() => onItemClicked(item)}
|
||||
className="invisible group-hover:visible"
|
||||
endIcon={<Icon name="arrow-left" />}
|
||||
>
|
||||
{t('Select')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ItemListComponent;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { LineChart } from '@ohif/ui';
|
||||
|
||||
const LineChartViewport = ({ displaySets }) => {
|
||||
const displaySet = displaySets[0];
|
||||
const { axis: chartAxis, series: chartSeries } = displaySet.instance.chartData;
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
showLegend={true}
|
||||
legendWidth={150}
|
||||
axis={{
|
||||
x: {
|
||||
label: chartAxis.x.label,
|
||||
indexRef: 0,
|
||||
type: 'x',
|
||||
range: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
label: chartAxis.y.label,
|
||||
indexRef: 1,
|
||||
type: 'y',
|
||||
},
|
||||
}}
|
||||
series={chartSeries}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { LineChartViewport as default };
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './LineChartViewport';
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useEffect, useState, useCallback, ReactElement } from 'react';
|
||||
import { ProgressDropdown } from '@ohif/ui';
|
||||
|
||||
const workflowStepsToDropdownOptions = (steps = []) =>
|
||||
steps.map(step => ({
|
||||
label: step.name,
|
||||
value: step.id,
|
||||
info: step.info,
|
||||
activated: false,
|
||||
completed: false,
|
||||
}));
|
||||
|
||||
export function ProgressDropdownWithService({ servicesManager }: withAppTypes): ReactElement {
|
||||
const { workflowStepsService } = servicesManager.services;
|
||||
const [activeStepId, setActiveStepId] = useState(workflowStepsService.activeWorkflowStep?.id);
|
||||
|
||||
const [dropdownOptions, setDropdownOptions] = useState(
|
||||
workflowStepsToDropdownOptions(workflowStepsService.workflowSteps)
|
||||
);
|
||||
|
||||
const setCurrentAndPreviousOptionsAsCompleted = useCallback(currentOption => {
|
||||
if (currentOption.completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDropdownOptions(prevOptions => {
|
||||
const newOptionsState = [...prevOptions];
|
||||
const startIndex = newOptionsState.findIndex(option => option.value === currentOption.value);
|
||||
|
||||
for (let i = startIndex; i >= 0; i--) {
|
||||
const option = newOptionsState[i];
|
||||
|
||||
if (option.completed) {
|
||||
break;
|
||||
}
|
||||
|
||||
newOptionsState[i] = {
|
||||
...option,
|
||||
completed: true,
|
||||
};
|
||||
}
|
||||
|
||||
return newOptionsState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDropdownChange = useCallback(
|
||||
({ selectedOption }) => {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Steps should be marked as completed after user has
|
||||
// completed some action when required (not implemented)
|
||||
setCurrentAndPreviousOptionsAsCompleted(selectedOption);
|
||||
setActiveStepId(selectedOption.value);
|
||||
},
|
||||
[setCurrentAndPreviousOptionsAsCompleted]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId;
|
||||
|
||||
if (activeStepId) {
|
||||
// We've used setTimeout to give it more time to update the UI since
|
||||
// create3DFilterableFromDataArray from Texture.js may take 600+ ms to run
|
||||
// when there is a new series to load in the next step but that resulted
|
||||
// in the followed React error when updating the content from left/right panels
|
||||
// and all component states were being lost:
|
||||
// Error: Can't perform a React state update on an unmounted component
|
||||
workflowStepsService.setActiveWorkflowStep(activeStepId);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [activeStepId, workflowStepsService]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe: unsubStepsChanged } = workflowStepsService.subscribe(
|
||||
workflowStepsService.EVENTS.STEPS_CHANGED,
|
||||
() => setDropdownOptions(workflowStepsToDropdownOptions(workflowStepsService.workflowSteps))
|
||||
);
|
||||
|
||||
const { unsubscribe: unsubActiveStepChanged } = workflowStepsService.subscribe(
|
||||
workflowStepsService.EVENTS.ACTIVE_STEP_CHANGED,
|
||||
|
||||
() => setActiveStepId(workflowStepsService.activeWorkflowStep.id)
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubStepsChanged();
|
||||
unsubActiveStepChanged();
|
||||
};
|
||||
}, [servicesManager, workflowStepsService]);
|
||||
|
||||
return (
|
||||
<ProgressDropdown
|
||||
options={dropdownOptions}
|
||||
value={activeStepId}
|
||||
onChange={handleDropdownChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
102
extensions/default/src/Components/SidePanelWithServices.tsx
Normal file
102
extensions/default/src/Components/SidePanelWithServices.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { SidePanel } from '@ohif/ui-next';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
export type SidePanelWithServicesProps = {
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
side: 'left' | 'right';
|
||||
className?: string;
|
||||
activeTabIndex: number;
|
||||
tabs: any;
|
||||
expandedWidth?: number;
|
||||
};
|
||||
|
||||
const SidePanelWithServices = ({
|
||||
servicesManager,
|
||||
side,
|
||||
activeTabIndex: activeTabIndexProp,
|
||||
tabs: tabsProp,
|
||||
expandedWidth,
|
||||
...props
|
||||
}: SidePanelWithServicesProps) => {
|
||||
const panelService = servicesManager?.services?.panelService;
|
||||
|
||||
// Tracks whether this SidePanel has been opened at least once since this SidePanel was inserted into the DOM.
|
||||
// Thus going to the Study List page and back to the viewer resets this flag for a SidePanel.
|
||||
const [sidePanelOpen, setSidePanelOpen] = useState(activeTabIndexProp !== null);
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(activeTabIndexProp);
|
||||
const [tabs, setTabs] = useState(tabsProp ?? panelService.getPanels(side));
|
||||
|
||||
const handleActiveTabIndexChange = useCallback(({ activeTabIndex }) => {
|
||||
setActiveTabIndex(activeTabIndex);
|
||||
setSidePanelOpen(activeTabIndex !== null);
|
||||
}, []);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setSidePanelOpen(true);
|
||||
// If panel is being opened but no tab is active, set first tab as active
|
||||
if (activeTabIndex === null && tabs.length > 0) {
|
||||
setActiveTabIndex(0);
|
||||
}
|
||||
}, [activeTabIndex, tabs]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSidePanelOpen(false);
|
||||
setActiveTabIndex(null);
|
||||
}, []);
|
||||
|
||||
/** update the active tab index from outside */
|
||||
useEffect(() => {
|
||||
setActiveTabIndex(activeTabIndexProp);
|
||||
}, [activeTabIndexProp]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = panelService.subscribe(
|
||||
panelService.EVENTS.PANELS_CHANGED,
|
||||
panelChangedEvent => {
|
||||
if (panelChangedEvent.position !== side) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTabs(panelService.getPanels(side));
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [panelService, side]);
|
||||
|
||||
useEffect(() => {
|
||||
const activatePanelSubscription = panelService.subscribe(
|
||||
panelService.EVENTS.ACTIVATE_PANEL,
|
||||
(activatePanelEvent: Types.ActivatePanelEvent) => {
|
||||
if (sidePanelOpen || activatePanelEvent.forceActive) {
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === activatePanelEvent.panelId);
|
||||
if (tabIndex !== -1) {
|
||||
setActiveTabIndex(tabIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
activatePanelSubscription.unsubscribe();
|
||||
};
|
||||
}, [tabs, sidePanelOpen, panelService]);
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
{...props}
|
||||
side={side}
|
||||
tabs={tabs}
|
||||
activeTabIndex={activeTabIndex}
|
||||
onOpen={handleOpen}
|
||||
onClose={handleClose}
|
||||
onActiveTabIndexChange={handleActiveTabIndexChange}
|
||||
expandedWidth={expandedWidth}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidePanelWithServices;
|
||||
Reference in New Issue
Block a user