Files
nebula.js/apis/nucleus/src/components/selections/SelectedFields.jsx
2025-12-11 14:03:17 +01:00

209 lines
6.8 KiB
JavaScript

import React, { useEffect, useState, useContext } from 'react';
import { Grid } from '@mui/material';
import { useTheme } from '@nebula.js/ui/theme';
import useCurrentSelectionsModel from '../../hooks/useCurrentSelectionsModel';
import useLayout from '../../hooks/useLayout';
import useRect from '../../hooks/useRect';
import InstanceContext from '../../contexts/InstanceContext';
import { getSinglePublicObjectProps, SINGLE_OBJECT_ID } from './single-public';
import { sortAllFields, sortSelections } from './utils';
import OneField from './OneField';
import MultiState from './MultiState';
import More from './More';
const MIN_WIDTH = 120;
const MIN_WIDTH_MORE = 72;
function getItems(layout) {
if (!layout) {
return [];
}
const fields = {};
// There is one qSelectionObject for the default state $,
// and an array of one qSelectionObject for each alternate state.
function collectFields(qSelectionObject, state) {
qSelectionObject.qSelections.forEach((selection) => {
const name = selection.qField;
let currentField = fields[name];
if (currentField === undefined) {
currentField = {
name,
label:
selection.qDimensionReferences?.find((element) => element.qLabel)?.qLabel ||
selection.qReadableName ||
name,
states: [],
selections: [],
};
fields[name] = currentField;
}
currentField.states.push(state);
currentField.selections.push(selection);
});
}
if (layout.qSelectionObject) {
collectFields(layout.qSelectionObject, '$');
}
if (layout.alternateStates) {
layout.alternateStates.forEach((s) => collectFields(s.qSelectionObject, s.stateName));
}
return Object.keys(fields)
.map((key) => fields[key])
.filter((f) => !f.selections.some((s) => s.qIsHidden));
}
export default function SelectedFields({ api, app, halo }) {
const theme = useTheme();
const [currentSelectionsModel] = useCurrentSelectionsModel(app);
const [layout] = useLayout(currentSelectionsModel);
const [state, setState] = useState({ items: [], more: [] });
const { modalObjectStore } = useContext(InstanceContext).selectionStore;
const [containerRef, containerRect] = useRect();
const [maxItems, setMaxItems] = useState(0);
const flags = halo.public.galaxy?.flags;
const isPinFieldEnabled = flags?.isEnabled('TLV_1394_PIN_FIELD_TO_TOOLBAR');
const [pinnedItems, setPinnedItems] = useState([]);
const [masterDimList, setMasterDimList] = useState([]);
const [fieldList, setFieldList] = useState([]);
const isInListboxPopover = () => {
const { model } = modalObjectStore.get(app.id) || {};
return model?.genericType === 'njsListbox';
};
useEffect(() => {
if (!containerRect) return;
const { width } = containerRect;
const maxWidth = Math.floor(width) - MIN_WIDTH_MORE;
const items = Math.floor(maxWidth / MIN_WIDTH);
setMaxItems(items);
}, [containerRect]);
useEffect(() => {
if (isPinFieldEnabled) {
const getPinnedItems = async () => {
const singlePublicProps = await getSinglePublicObjectProps(app, SINGLE_OBJECT_ID);
return singlePublicProps?.pinnedItems || [];
};
const masterDimList = async () => {
const dimensionListObject = await app.getDimensionListObject();
return dimensionListObject.getLayout();
};
const getFieldList = async () => {
const fieldListObject = await app.getFieldListObject();
return fieldListObject.expand();
};
Promise.all([getPinnedItems(), masterDimList(), getFieldList()]).then(([pinnedItems, dimList, fieldList]) => {
setPinnedItems(pinnedItems);
setMasterDimList(dimList);
setFieldList(fieldList);
});
}
}, [app]);
useEffect(() => {
if (!app || !currentSelectionsModel || !layout || !maxItems) {
return;
}
let items = isPinFieldEnabled ? getItems(layout).sort(sortSelections) : getItems(layout);
if (isPinFieldEnabled) {
items = sortAllFields(fieldList, pinnedItems, items, masterDimList);
}
setState((currState) => {
const newItems = items;
// Maintain modal state in app selections
if (isInListboxPopover() && newItems.length + 1 === currState.items.length) {
const lastDeselectedField = currState.items.filter(
(f1) => newItems.some((f2) => f1.name === f2.name) === false
)[0];
const { qField } = lastDeselectedField.selections[0];
lastDeselectedField.selections = [{ qField }];
const wasIx = currState.items.indexOf(lastDeselectedField);
newItems.splice(wasIx, 0, lastDeselectedField);
}
let newMoreItems = [];
if (maxItems < newItems.length) {
if (isPinFieldEnabled) {
newMoreItems = newItems.splice(0, newItems.length - maxItems);
} else {
newMoreItems = newItems.splice(maxItems - newItems.length);
}
}
return {
items: newItems,
more: newMoreItems,
};
});
}, [
app,
currentSelectionsModel,
layout,
api.isInModal(),
maxItems,
isPinFieldEnabled,
pinnedItems,
masterDimList,
fieldList,
]);
return (
<Grid ref={containerRef} container gap={0} wrap="nowrap" style={{ height: '100%' }}>
{isPinFieldEnabled && state.more.length > 0 && (
<Grid
item
style={{
position: 'relative',
maxWidth: '98px',
minWidth: `${MIN_WIDTH_MORE}px`,
background: theme.palette.background.paper,
borderRight: `1px solid ${theme.palette.divider}`,
}}
>
<More items={state.more} api={api} isPinFieldEnabled={isPinFieldEnabled} />
</Grid>
)}
{state.items.map((s) => (
<Grid
item
key={`${s.states.join('::')}::${s.id ?? s.qField ?? s.name}`}
style={{
position: 'relative',
maxWidth: '240px',
minWidth: `${MIN_WIDTH}px`,
background: theme.palette.background.paper,
borderRight: `1px solid ${theme.palette.divider}`,
}}
>
{s.states.length > 1 ? (
<MultiState field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
) : (
<OneField field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
)}
</Grid>
))}
{!isPinFieldEnabled && state.more.length > 0 && (
<Grid
item
style={{
position: 'relative',
maxWidth: '98px',
minWidth: `${MIN_WIDTH_MORE}px`,
background: theme.palette.background.paper,
borderRight: `1px solid ${theme.palette.divider}`,
}}
>
<More items={state.more} api={api} />
</Grid>
)}
</Grid>
);
}