mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
feat: add pin item to selection bar
This commit is contained in:
@@ -10,7 +10,7 @@ import Nav from './Nav';
|
|||||||
import useAppSelections from '../../hooks/useAppSelections';
|
import useAppSelections from '../../hooks/useAppSelections';
|
||||||
import uid from '../../object/uid';
|
import uid from '../../object/uid';
|
||||||
|
|
||||||
function AppSelections({ app }) {
|
function AppSelections({ app, halo }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [appSelections] = useAppSelections(app);
|
const [appSelections] = useAppSelections(app);
|
||||||
if (!appSelections) return null;
|
if (!appSelections) return null;
|
||||||
@@ -33,7 +33,7 @@ function AppSelections({ app }) {
|
|||||||
<Nav api={appSelections} app={app} />
|
<Nav api={appSelections} app={app} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs style={{ backgroundColor: theme.palette.background.darker, overflow: 'hidden' }}>
|
<Grid item xs style={{ backgroundColor: theme.palette.background.darker, overflow: 'hidden' }}>
|
||||||
<SelectedFields api={appSelections} app={app} />
|
<SelectedFields api={appSelections} app={app} halo={halo} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
@@ -41,6 +41,6 @@ function AppSelections({ app }) {
|
|||||||
|
|
||||||
export { AppSelections };
|
export { AppSelections };
|
||||||
|
|
||||||
export default function mount({ element, app }) {
|
export default function mount({ element, app, halo }) {
|
||||||
return ReactDOM.createPortal(<AppSelections app={app} />, element, uid());
|
return ReactDOM.createPortal(<AppSelections app={app} halo={halo} />, element, uid());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const StyledGrid = styled(Grid)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function More({ items = [], api }) {
|
export default function More({ items = [], api, isPinFieldEnabled = false }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [showMoreItems, setShowMoreItems] = useState(false);
|
const [showMoreItems, setShowMoreItems] = useState(false);
|
||||||
const [showItemIx, setShowItemIx] = useState(-1);
|
const [showItemIx, setShowItemIx] = useState(-1);
|
||||||
@@ -66,7 +66,13 @@ export default function More({ items = [], api }) {
|
|||||||
if (showItemIx > -1) {
|
if (showItemIx > -1) {
|
||||||
CurrentItem =
|
CurrentItem =
|
||||||
items[showItemIx].states.length > 1 ? (
|
items[showItemIx].states.length > 1 ? (
|
||||||
<MultiState field={items[showItemIx]} api={api} moreAlignTo={alignTo} onClose={handleCloseShowItem} />
|
<MultiState
|
||||||
|
field={items[showItemIx]}
|
||||||
|
api={api}
|
||||||
|
moreAlignTo={alignTo}
|
||||||
|
onClose={handleCloseShowItem}
|
||||||
|
isPinFieldEnabled={isPinFieldEnabled}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<OneField
|
<OneField
|
||||||
field={items[showItemIx]}
|
field={items[showItemIx]}
|
||||||
@@ -74,6 +80,7 @@ export default function More({ items = [], api }) {
|
|||||||
skipHandleShowListBoxPopover
|
skipHandleShowListBoxPopover
|
||||||
moreAlignTo={alignTo}
|
moreAlignTo={alignTo}
|
||||||
onClose={handleCloseShowItem}
|
onClose={handleCloseShowItem}
|
||||||
|
isPinFieldEnabled={isPinFieldEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,7 +132,11 @@ export default function More({ items = [], api }) {
|
|||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<ListItem key={ix} title={s.name} onClick={(e) => handleShowItem(e, ix)}>
|
<ListItem key={ix} title={s.name} onClick={(e) => handleShowItem(e, ix)}>
|
||||||
<Box border={1} width="100%" borderRadius={1} borderColor="divider">
|
<Box border={1} width="100%" borderRadius={1} borderColor="divider">
|
||||||
{s.states.length > 1 ? <MultiState field={s} api={api} /> : <OneField field={s} api={api} />}
|
{s.states.length > 1 ? (
|
||||||
|
<MultiState field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
|
||||||
|
) : (
|
||||||
|
<OneField field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const StyledGrid = styled(Grid)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function MultiState({ field, api, moreAlignTo = null, onClose = () => {} }) {
|
export default function MultiState({ field, api, moreAlignTo = null, onClose = () => {}, isPinFieldEnabled = false }) {
|
||||||
// If originated from the `more` item show fields directly
|
// If originated from the `more` item show fields directly
|
||||||
const [showFields, setShowFields] = useState(!!moreAlignTo);
|
const [showFields, setShowFields] = useState(!!moreAlignTo);
|
||||||
const [showStateIx, setShowStateIx] = useState(-1);
|
const [showStateIx, setShowStateIx] = useState(-1);
|
||||||
@@ -100,7 +100,7 @@ export default function MultiState({ field, api, moreAlignTo = null, onClose = (
|
|||||||
// eslint-disable-next-line react/no-array-index-key
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
<ListItem key={ix} title={field.label} onClick={(e) => handleShowState(e, ix)}>
|
<ListItem key={ix} title={field.label} onClick={(e) => handleShowState(e, ix)}>
|
||||||
<Box border={1} width="100%" borderRadius={1} borderColor="divider">
|
<Box border={1} width="100%" borderRadius={1} borderColor="divider">
|
||||||
<OneField field={field} api={api} stateIx={ix} skipHandleShowListBoxPopover />
|
<OneField field={field} api={api} stateIx={ix} skipHandleShowListBoxPopover isPinFieldEnabled={isPinFieldEnabled}/>
|
||||||
</Box>
|
</Box>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTheme } from '@nebula.js/ui/theme';
|
|||||||
import ListBoxPopover from '../listbox/ListBoxPopover';
|
import ListBoxPopover from '../listbox/ListBoxPopover';
|
||||||
|
|
||||||
import InstanceContext from '../../contexts/InstanceContext';
|
import InstanceContext from '../../contexts/InstanceContext';
|
||||||
|
import PinItem from './PinItem';
|
||||||
|
|
||||||
export default function OneField({
|
export default function OneField({
|
||||||
field,
|
field,
|
||||||
@@ -17,11 +18,15 @@ export default function OneField({
|
|||||||
skipHandleShowListBoxPopover = false,
|
skipHandleShowListBoxPopover = false,
|
||||||
moreAlignTo = null,
|
moreAlignTo = null,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
|
isPinFieldEnabled = false,
|
||||||
}) {
|
}) {
|
||||||
const { translator } = useContext(InstanceContext);
|
const { translator } = useContext(InstanceContext);
|
||||||
const alignTo = moreAlignTo || useRef();
|
const alignTo = moreAlignTo || useRef();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [showListBoxPopover, setShowListBoxPopover] = useState(false);
|
const [showListBoxPopover, setShowListBoxPopover] = useState(false);
|
||||||
|
let Component = null;
|
||||||
|
let selection;
|
||||||
|
const isPinnedItem = !!field.isPinned && isPinFieldEnabled;
|
||||||
|
|
||||||
const handleShowListBoxPopover = (e) => {
|
const handleShowListBoxPopover = (e) => {
|
||||||
if (e.currentTarget.contains(e.target)) {
|
if (e.currentTarget.contains(e.target)) {
|
||||||
@@ -35,153 +40,167 @@ export default function OneField({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const selection = field.selections[stateIx];
|
if (isPinnedItem) {
|
||||||
if (typeof selection.qTotal === 'undefined') {
|
Component = (
|
||||||
selection.qTotal = 0;
|
<PinItem
|
||||||
}
|
field={field}
|
||||||
const counts = selection.qStateCounts || {
|
api={api}
|
||||||
qSelected: 0,
|
showListBoxPopover={showListBoxPopover}
|
||||||
qLocked: 0,
|
alignTo={alignTo}
|
||||||
qExcluded: 0,
|
skipHandleShowListBoxPopover={skipHandleShowListBoxPopover}
|
||||||
qLockedExcluded: 0,
|
handleShowListBoxPopover={handleShowListBoxPopover}
|
||||||
qSelectedExcluded: 0,
|
handleCloseShowListBoxPopover={handleCloseShowListBoxPopover}
|
||||||
qAlternative: 0,
|
/>
|
||||||
};
|
|
||||||
const green = (counts.qSelected + counts.qLocked) / selection.qTotal;
|
|
||||||
const white = counts.qAlternative / selection.qTotal;
|
|
||||||
const grey = (counts.qExcluded + counts.qLockedExcluded + counts.qSelectedExcluded) / selection.qTotal;
|
|
||||||
|
|
||||||
const numSelected = counts.qSelected + counts.qSelectedExcluded + counts.qLocked + counts.qLockedExcluded;
|
|
||||||
// Maintain modal state in app selections
|
|
||||||
const noSegments = numSelected === 0 && selection.qTotal === 0;
|
|
||||||
let label = '';
|
|
||||||
if (selection.qTotal === numSelected && selection.qTotal > 1) {
|
|
||||||
label = translator.get('CurrentSelections.All');
|
|
||||||
} else if (numSelected > 1 && selection.qTotal) {
|
|
||||||
label = translator.get('CurrentSelections.Of', [numSelected, selection.qTotal]);
|
|
||||||
} else if (selection.qSelectedFieldSelectionInfo) {
|
|
||||||
label = selection.qSelectedFieldSelectionInfo.map((v) => v.qName).join(', ');
|
|
||||||
}
|
|
||||||
if (field.states[stateIx] !== '$') {
|
|
||||||
label = `${field.states[stateIx]}: ${label}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const segments = [
|
|
||||||
{ color: theme.palette.selected.main, ratio: green },
|
|
||||||
{ color: theme.palette.selected.alternative, ratio: white },
|
|
||||||
{ color: theme.palette.selected.excluded, ratio: grey },
|
|
||||||
];
|
|
||||||
segments.forEach((s, i) => {
|
|
||||||
s.offset = i ? segments[i - 1].offset + segments[i - 1].ratio : 0; // eslint-disable-line
|
|
||||||
});
|
|
||||||
|
|
||||||
let Header = null;
|
|
||||||
let Icon = null;
|
|
||||||
let SegmentsIndicator = null;
|
|
||||||
let Component = null;
|
|
||||||
if (!moreAlignTo) {
|
|
||||||
Header = (
|
|
||||||
<Grid item xs style={{ minWidth: 0, flexGrow: 1, opacity: selection.qLocked ? '0.3' : '' }}>
|
|
||||||
<Typography noWrap style={{ fontSize: '12px', lineHeight: '16px', fontWeight: 600 }}>
|
|
||||||
{field.label}
|
|
||||||
</Typography>
|
|
||||||
<Typography noWrap style={{ fontSize: '12px', opacity: 0.55, lineHeight: '16px' }}>
|
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
selection = field.selections[stateIx];
|
||||||
|
if (typeof selection.qTotal === 'undefined') {
|
||||||
|
selection.qTotal = 0;
|
||||||
|
}
|
||||||
|
const counts = selection.qStateCounts || {
|
||||||
|
qSelected: 0,
|
||||||
|
qLocked: 0,
|
||||||
|
qExcluded: 0,
|
||||||
|
qLockedExcluded: 0,
|
||||||
|
qSelectedExcluded: 0,
|
||||||
|
qAlternative: 0,
|
||||||
|
};
|
||||||
|
const green = (counts.qSelected + counts.qLocked) / selection.qTotal;
|
||||||
|
const white = counts.qAlternative / selection.qTotal;
|
||||||
|
const grey = (counts.qExcluded + counts.qLockedExcluded + counts.qSelectedExcluded) / selection.qTotal;
|
||||||
|
|
||||||
if (selection.qLocked) {
|
const numSelected = counts.qSelected + counts.qSelectedExcluded + counts.qLocked + counts.qLockedExcluded;
|
||||||
Icon = (
|
// Maintain modal state in app selections
|
||||||
<Grid item>
|
const noSegments = numSelected === 0 && selection.qTotal === 0;
|
||||||
<IconButton size="large">
|
let label = '';
|
||||||
<Lock />
|
if (selection.qTotal === numSelected && selection.qTotal > 1) {
|
||||||
</IconButton>
|
label = translator.get('CurrentSelections.All');
|
||||||
|
} else if (numSelected > 1 && selection.qTotal) {
|
||||||
|
label = translator.get('CurrentSelections.Of', [numSelected, selection.qTotal]);
|
||||||
|
} else if (selection.qSelectedFieldSelectionInfo) {
|
||||||
|
label = selection.qSelectedFieldSelectionInfo.map((v) => v.qName).join(', ');
|
||||||
|
}
|
||||||
|
if (field.states[stateIx] !== '$') {
|
||||||
|
label = `${field.states[stateIx]}: ${label}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segments = [
|
||||||
|
{ color: theme.palette.selected.main, ratio: green },
|
||||||
|
{ color: theme.palette.selected.alternative, ratio: white },
|
||||||
|
{ color: theme.palette.selected.excluded, ratio: grey },
|
||||||
|
];
|
||||||
|
segments.forEach((s, i) => {
|
||||||
|
s.offset = i ? segments[i - 1].offset + segments[i - 1].ratio : 0; // eslint-disable-line
|
||||||
|
});
|
||||||
|
|
||||||
|
let Header = null;
|
||||||
|
let Icon = null;
|
||||||
|
let SegmentsIndicator = null;
|
||||||
|
if (!moreAlignTo) {
|
||||||
|
Header = (
|
||||||
|
<Grid item xs style={{ minWidth: 0, flexGrow: 1, opacity: selection.qLocked ? '0.3' : '' }}>
|
||||||
|
<Typography noWrap style={{ fontSize: '12px', lineHeight: '16px', fontWeight: 600 }}>
|
||||||
|
{field.label}
|
||||||
|
</Typography>
|
||||||
|
<Typography noWrap style={{ fontSize: '12px', opacity: 0.55, lineHeight: '16px' }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
} else if (!selection.qOneAndOnlyOne) {
|
|
||||||
Icon = (
|
if (selection.qLocked) {
|
||||||
<Grid item>
|
Icon = (
|
||||||
<IconButton
|
<Grid item>
|
||||||
title={translator.get('Selection.Clear')}
|
<IconButton size="large">
|
||||||
onClick={(e) => {
|
<Lock />
|
||||||
e.stopPropagation();
|
</IconButton>
|
||||||
api.clearField(selection.qField, field.states[stateIx]);
|
</Grid>
|
||||||
}}
|
);
|
||||||
size="large"
|
} else if (!selection.qOneAndOnlyOne) {
|
||||||
>
|
Icon = (
|
||||||
<Remove />
|
<Grid item>
|
||||||
</IconButton>
|
<IconButton
|
||||||
|
title={translator.get('Selection.Clear')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
api.clearField(selection.qField, field.states[stateIx]);
|
||||||
|
}}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<Remove />
|
||||||
|
</IconButton>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SegmentsIndicator = (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '4px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '0',
|
||||||
|
left: '0',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{noSegments === false &&
|
||||||
|
segments.map((s) => (
|
||||||
|
<div
|
||||||
|
key={s.color}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
background: s.color,
|
||||||
|
height: '100%',
|
||||||
|
top: 0,
|
||||||
|
width: `${s.ratio * 100}%`,
|
||||||
|
left: `${s.offset * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
Component = (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
gap={1}
|
||||||
|
ref={alignTo}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
|
||||||
|
>
|
||||||
|
{Header}
|
||||||
|
{Icon}
|
||||||
|
{SegmentsIndicator}
|
||||||
|
{showListBoxPopover && (
|
||||||
|
<ListBoxPopover
|
||||||
|
alignTo={alignTo}
|
||||||
|
show={showListBoxPopover}
|
||||||
|
close={handleCloseShowListBoxPopover}
|
||||||
|
app={api.model}
|
||||||
|
fieldName={selection.qField}
|
||||||
|
stateName={field.states[stateIx]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SegmentsIndicator = (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: '4px',
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{noSegments === false &&
|
|
||||||
segments.map((s) => (
|
|
||||||
<div
|
|
||||||
key={s.color}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
background: s.color,
|
|
||||||
height: '100%',
|
|
||||||
top: 0,
|
|
||||||
width: `${s.ratio * 100}%`,
|
|
||||||
left: `${s.offset * 100}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
Component = (
|
|
||||||
<Grid
|
|
||||||
container
|
|
||||||
gap={1}
|
|
||||||
ref={alignTo}
|
|
||||||
sx={{
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
position: 'relative',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '4px',
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
|
|
||||||
>
|
|
||||||
{Header}
|
|
||||||
{Icon}
|
|
||||||
{SegmentsIndicator}
|
|
||||||
{showListBoxPopover && (
|
|
||||||
<ListBoxPopover
|
|
||||||
alignTo={alignTo}
|
|
||||||
show={showListBoxPopover}
|
|
||||||
close={handleCloseShowListBoxPopover}
|
|
||||||
app={api.model}
|
|
||||||
fieldName={selection.qField}
|
|
||||||
stateName={field.states[stateIx]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return moreAlignTo ? (
|
return moreAlignTo ? (
|
||||||
<ListBoxPopover
|
<ListBoxPopover
|
||||||
alignTo={alignTo}
|
alignTo={alignTo}
|
||||||
show
|
show
|
||||||
close={handleCloseShowListBoxPopover}
|
close={handleCloseShowListBoxPopover}
|
||||||
app={api.model}
|
app={api.model}
|
||||||
fieldName={selection.qField}
|
fieldName={isPinnedItem ? field.qField : selection.qField}
|
||||||
stateName={field.states[stateIx]}
|
stateName={isPinnedItem ? undefined : field.states[stateIx]}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
Component
|
Component
|
||||||
|
|||||||
51
apis/nucleus/src/components/selections/PinItem.jsx
Normal file
51
apis/nucleus/src/components/selections/PinItem.jsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Grid2, Typography } from '@mui/material';
|
||||||
|
import { useTheme } from '@nebula.js/ui/theme';
|
||||||
|
import ListBoxPopover from '../listbox/ListBoxPopover';
|
||||||
|
|
||||||
|
const PinItem = ({
|
||||||
|
field,
|
||||||
|
api,
|
||||||
|
showListBoxPopover,
|
||||||
|
alignTo,
|
||||||
|
skipHandleShowListBoxPopover,
|
||||||
|
handleShowListBoxPopover,
|
||||||
|
handleCloseShowListBoxPopover,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const displayName = field.qName || field.qField;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid2
|
||||||
|
container
|
||||||
|
gap={1}
|
||||||
|
ref={alignTo}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
position: 'relative',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '4px',
|
||||||
|
height: '40px',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={(skipHandleShowListBoxPopover === false && handleShowListBoxPopover) || null}
|
||||||
|
>
|
||||||
|
<Typography noWrap style={{ fontSize: '12px', lineHeight: '16px', fontWeight: 600, marginTop: '8px' }}>
|
||||||
|
{displayName}
|
||||||
|
</Typography>
|
||||||
|
{showListBoxPopover && (
|
||||||
|
<ListBoxPopover
|
||||||
|
alignTo={alignTo}
|
||||||
|
show={showListBoxPopover}
|
||||||
|
close={handleCloseShowListBoxPopover}
|
||||||
|
app={api.model}
|
||||||
|
fieldName={field.qField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid2>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PinItem;
|
||||||
@@ -7,6 +7,8 @@ import useCurrentSelectionsModel from '../../hooks/useCurrentSelectionsModel';
|
|||||||
import useLayout from '../../hooks/useLayout';
|
import useLayout from '../../hooks/useLayout';
|
||||||
import useRect from '../../hooks/useRect';
|
import useRect from '../../hooks/useRect';
|
||||||
import InstanceContext from '../../contexts/InstanceContext';
|
import InstanceContext from '../../contexts/InstanceContext';
|
||||||
|
import { getSinglePublicObjectProps, SINGLE_OBJECT_ID } from './single-public';
|
||||||
|
import { sortAllFields, sortSelections } from './utils';
|
||||||
|
|
||||||
import OneField from './OneField';
|
import OneField from './OneField';
|
||||||
import MultiState from './MultiState';
|
import MultiState from './MultiState';
|
||||||
@@ -54,7 +56,7 @@ function getItems(layout) {
|
|||||||
.filter((f) => !f.selections.some((s) => s.qIsHidden));
|
.filter((f) => !f.selections.some((s) => s.qIsHidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SelectedFields({ api, app }) {
|
export default function SelectedFields({ api, app, halo }) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [currentSelectionsModel] = useCurrentSelectionsModel(app);
|
const [currentSelectionsModel] = useCurrentSelectionsModel(app);
|
||||||
const [layout] = useLayout(currentSelectionsModel);
|
const [layout] = useLayout(currentSelectionsModel);
|
||||||
@@ -63,6 +65,11 @@ export default function SelectedFields({ api, app }) {
|
|||||||
const { modalObjectStore } = useContext(InstanceContext).selectionStore;
|
const { modalObjectStore } = useContext(InstanceContext).selectionStore;
|
||||||
const [containerRef, containerRect] = useRect();
|
const [containerRef, containerRect] = useRect();
|
||||||
const [maxItems, setMaxItems] = useState(0);
|
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 isInListboxPopover = () => {
|
||||||
const { model } = modalObjectStore.get(app.id) || {};
|
const { model } = modalObjectStore.get(app.id) || {};
|
||||||
@@ -77,11 +84,39 @@ export default function SelectedFields({ api, app }) {
|
|||||||
setMaxItems(items);
|
setMaxItems(items);
|
||||||
}, [containerRect]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!app || !currentSelectionsModel || !layout || !maxItems) {
|
if (!app || !currentSelectionsModel || !layout || !maxItems) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const items = getItems(layout);
|
let items = isPinFieldEnabled ? getItems(layout).sort(sortSelections) : getItems(layout);
|
||||||
|
if (isPinFieldEnabled) {
|
||||||
|
items = sortAllFields(fieldList, pinnedItems, items, masterDimList);
|
||||||
|
}
|
||||||
setState((currState) => {
|
setState((currState) => {
|
||||||
const newItems = items;
|
const newItems = items;
|
||||||
// Maintain modal state in app selections
|
// Maintain modal state in app selections
|
||||||
@@ -96,21 +131,49 @@ export default function SelectedFields({ api, app }) {
|
|||||||
}
|
}
|
||||||
let newMoreItems = [];
|
let newMoreItems = [];
|
||||||
if (maxItems < newItems.length) {
|
if (maxItems < newItems.length) {
|
||||||
newMoreItems = newItems.splice(maxItems - newItems.length);
|
if (isPinFieldEnabled) {
|
||||||
|
newMoreItems = newItems.splice(0, newItems.length - maxItems);
|
||||||
|
} else {
|
||||||
|
newMoreItems = newItems.splice(maxItems - newItems.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
items: newItems,
|
items: newItems,
|
||||||
more: newMoreItems,
|
more: newMoreItems,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [app, currentSelectionsModel, layout, api.isInModal(), maxItems]);
|
}, [
|
||||||
|
app,
|
||||||
|
currentSelectionsModel,
|
||||||
|
layout,
|
||||||
|
api.isInModal(),
|
||||||
|
maxItems,
|
||||||
|
isPinFieldEnabled,
|
||||||
|
pinnedItems,
|
||||||
|
masterDimList,
|
||||||
|
fieldList,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid ref={containerRef} container gap={0} wrap="nowrap" style={{ height: '100%' }}>
|
<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) => (
|
{state.items.map((s) => (
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
key={`${s.states.join('::')}::${s.name}`}
|
key={`${s.states.join('::')}::${s.id ?? s.qField ?? s.name}`}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxWidth: '240px',
|
maxWidth: '240px',
|
||||||
@@ -119,10 +182,14 @@ export default function SelectedFields({ api, app }) {
|
|||||||
borderRight: `1px solid ${theme.palette.divider}`,
|
borderRight: `1px solid ${theme.palette.divider}`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{s.states.length > 1 ? <MultiState field={s} api={api} /> : <OneField field={s} api={api} />}
|
{s.states.length > 1 ? (
|
||||||
|
<MultiState field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
|
||||||
|
) : (
|
||||||
|
<OneField field={s} api={api} isPinFieldEnabled={isPinFieldEnabled} />
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
))}
|
||||||
{state.more.length > 0 && (
|
{!isPinFieldEnabled && state.more.length > 0 && (
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React, { act } from 'react';
|
||||||
|
import * as ReactTestRenderer from 'react-test-renderer';
|
||||||
|
import * as NebulaThemeModule from '@nebula.js/ui/theme';
|
||||||
|
import PinItem from '../PinItem';
|
||||||
|
|
||||||
|
jest.mock('@nebula.js/ui/theme', () => ({
|
||||||
|
...jest.requireActual('@nebula.js/ui/theme'),
|
||||||
|
useTheme: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../listbox/ListBoxPopover', () => {
|
||||||
|
return function MockListBoxPopover(props) {
|
||||||
|
return <div data-testid="listbox-popover">ListBoxPopover</div>;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<PinItem />', () => {
|
||||||
|
let renderer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest
|
||||||
|
.spyOn(NebulaThemeModule, 'useTheme')
|
||||||
|
.mockImplementation(() => ({
|
||||||
|
palette: {
|
||||||
|
background: { paper: '#ffffff' },
|
||||||
|
action: { hover: '#f5f5f5' },
|
||||||
|
divider: '#e0e0e0',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
if (renderer) {
|
||||||
|
renderer.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render field name from qName when available', () => {
|
||||||
|
const field = {
|
||||||
|
qName: 'Product',
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={false}
|
||||||
|
alignTo={React.createRef()}
|
||||||
|
skipHandleShowListBoxPopover={true}
|
||||||
|
handleShowListBoxPopover={jest.fn()}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = renderer.toJSON();
|
||||||
|
expect(JSON.stringify(output)).toContain('Product');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render field name from qField as fallback when qName is not available', () => {
|
||||||
|
const field = {
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={false}
|
||||||
|
alignTo={React.createRef()}
|
||||||
|
skipHandleShowListBoxPopover={true}
|
||||||
|
handleShowListBoxPopover={jest.fn()}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = renderer.toJSON();
|
||||||
|
expect(JSON.stringify(output)).toContain('product_field');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call handleShowListBoxPopover when clicked and skipHandleShowListBoxPopover is false', () => {
|
||||||
|
const handleShowListBoxPopover = jest.fn();
|
||||||
|
const field = {
|
||||||
|
qName: 'Product',
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={false}
|
||||||
|
alignTo={React.createRef()}
|
||||||
|
skipHandleShowListBoxPopover={false}
|
||||||
|
handleShowListBoxPopover={handleShowListBoxPopover}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the container div and trigger click
|
||||||
|
const root = renderer.root;
|
||||||
|
const containerInstance = root.findByType(PinItem);
|
||||||
|
expect(containerInstance).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not call handleShowListBoxPopover when skipHandleShowListBoxPopover is true', () => {
|
||||||
|
const handleShowListBoxPopover = jest.fn();
|
||||||
|
const field = {
|
||||||
|
qName: 'Product',
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={false}
|
||||||
|
alignTo={React.createRef()}
|
||||||
|
skipHandleShowListBoxPopover={true}
|
||||||
|
handleShowListBoxPopover={handleShowListBoxPopover}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(handleShowListBoxPopover).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render ListBoxPopover when showListBoxPopover is true', () => {
|
||||||
|
const field = {
|
||||||
|
qName: 'Product',
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
const alignToRef = React.createRef();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={true}
|
||||||
|
alignTo={alignToRef}
|
||||||
|
skipHandleShowListBoxPopover={true}
|
||||||
|
handleShowListBoxPopover={jest.fn()}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = renderer.toJSON();
|
||||||
|
const serialized = JSON.stringify(output);
|
||||||
|
expect(serialized).toContain('ListBoxPopover');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not render ListBoxPopover when showListBoxPopover is false', () => {
|
||||||
|
const field = {
|
||||||
|
qName: 'Product',
|
||||||
|
qField: 'product_field',
|
||||||
|
};
|
||||||
|
const api = {
|
||||||
|
model: {},
|
||||||
|
};
|
||||||
|
const alignToRef = React.createRef();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
renderer = ReactTestRenderer.create(
|
||||||
|
<PinItem
|
||||||
|
field={field}
|
||||||
|
api={api}
|
||||||
|
showListBoxPopover={false}
|
||||||
|
alignTo={alignToRef}
|
||||||
|
skipHandleShowListBoxPopover={true}
|
||||||
|
handleShowListBoxPopover={jest.fn()}
|
||||||
|
handleCloseShowListBoxPopover={jest.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = renderer.toJSON();
|
||||||
|
const serialized = JSON.stringify(output);
|
||||||
|
expect(serialized).not.toContain('ListBoxPopover');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -75,7 +75,9 @@ describe('<SelectedFields />', () => {
|
|||||||
selectionStore: selectionsStoreModule,
|
selectionStore: selectionsStoreModule,
|
||||||
};
|
};
|
||||||
|
|
||||||
render = async ({ api = {}, app = {}, rendererOptions } = {}) => {
|
const defaultHalo = { public: { galaxy: { flags: { isEnabled: jest.fn().mockReturnValue(false) } } } };
|
||||||
|
|
||||||
|
render = async ({ api = {}, app = {}, halo = {}, rendererOptions } = {}) => {
|
||||||
api = {
|
api = {
|
||||||
...defaultApi,
|
...defaultApi,
|
||||||
...api,
|
...api,
|
||||||
@@ -84,10 +86,14 @@ describe('<SelectedFields />', () => {
|
|||||||
...defaultApp,
|
...defaultApp,
|
||||||
...app,
|
...app,
|
||||||
};
|
};
|
||||||
|
halo = {
|
||||||
|
...defaultHalo,
|
||||||
|
...halo,
|
||||||
|
};
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
renderer = create(
|
renderer = create(
|
||||||
<InstanceContext.Provider value={context}>
|
<InstanceContext.Provider value={context}>
|
||||||
<SelectedFields api={api} app={app} />
|
<SelectedFields api={api} app={app} halo={halo} />
|
||||||
</InstanceContext.Provider>,
|
</InstanceContext.Provider>,
|
||||||
rendererOptions || null
|
rendererOptions || null
|
||||||
);
|
);
|
||||||
|
|||||||
32
apis/nucleus/src/components/selections/single-public.js
Normal file
32
apis/nucleus/src/components/selections/single-public.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
const SINGLE_OBJECT_ID = 'singlepublic-pinnedItems';
|
||||||
|
const TYPE = 'singlepublic';
|
||||||
|
|
||||||
|
const getSinglePublicObject = async (app, qId) => {
|
||||||
|
// TODO check create permission of single public object
|
||||||
|
const layout = await app.getLayout();
|
||||||
|
const canCreate = !!layout.permissions?.createMasterobject;
|
||||||
|
let singlePublicObject = null;
|
||||||
|
try {
|
||||||
|
if (canCreate) {
|
||||||
|
singlePublicObject = await app.getOrCreateObject({
|
||||||
|
qInfo: { qType: TYPE, qId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
singlePublicObject = await app.getObject(qId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return singlePublicObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSinglePublicObjectProps = async (app, qId) => {
|
||||||
|
const singlePublicObject = await getSinglePublicObject(app, qId);
|
||||||
|
if (!singlePublicObject) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const props = await singlePublicObject.getProperties();
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getSinglePublicObjectProps, SINGLE_OBJECT_ID };
|
||||||
87
apis/nucleus/src/components/selections/utils.js
Normal file
87
apis/nucleus/src/components/selections/utils.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
const FIELD_KEY = 'qField';
|
||||||
|
const MASTER_ITEM_KEY = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort items shown in current selections toolbar.
|
||||||
|
* The order is as follows:
|
||||||
|
* 1. qOneAndOnlyOne
|
||||||
|
* 2. qSortIndex
|
||||||
|
*
|
||||||
|
* @param {Item|Object} a
|
||||||
|
* @param {Item|Object} b
|
||||||
|
*/
|
||||||
|
const sortSelections = (a, b) => {
|
||||||
|
const aSelection = a.selections[0];
|
||||||
|
const bSelection = b.selections[0];
|
||||||
|
const aQ = !!aSelection.qOneAndOnlyOne;
|
||||||
|
const bQ = !!bSelection.qOneAndOnlyOne;
|
||||||
|
if (aQ === bQ) {
|
||||||
|
return aSelection.qSortIndex - bSelection.qSortIndex;
|
||||||
|
}
|
||||||
|
return aQ ? -1 : 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// fieldList only has qName properties can identify a field
|
||||||
|
const getValidPinnedItems = (fieldList, masterDimList, pinnedItems) =>
|
||||||
|
pinnedItems.filter((field) => {
|
||||||
|
// Validate field items (those with qField) against fieldList
|
||||||
|
if (FIELD_KEY in field && field[FIELD_KEY]) {
|
||||||
|
return fieldList.some((item) => item.qName === field[FIELD_KEY]);
|
||||||
|
}
|
||||||
|
// Validate master dimension items (those with id) against masterDimList
|
||||||
|
if (MASTER_ITEM_KEY in field && field[MASTER_ITEM_KEY]) {
|
||||||
|
return masterDimList.some((dimItem) => dimItem.qInfo?.qId === field[MASTER_ITEM_KEY]);
|
||||||
|
}
|
||||||
|
// Invalid field structure, filter it out
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sorts valid pinned items first, then selected fields.
|
||||||
|
// If pinned item is also selected, this pinned item will be replaced with selected field in this index.
|
||||||
|
const sortAllFields = (fieldList, pinnedItems, selectedFields, masterDimList) => {
|
||||||
|
if (fieldList.length === 0 || (pinnedItems.length === 0 && selectedFields.length === 0)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const validPinnedItems = getValidPinnedItems(fieldList, masterDimList, pinnedItems);
|
||||||
|
const sortedFields = [];
|
||||||
|
const remainingSelectedFields = [...selectedFields];
|
||||||
|
|
||||||
|
for (let i = 0; i < validPinnedItems.length; i++) {
|
||||||
|
const pinnedItem = validPinnedItems[i];
|
||||||
|
const isMasterDim = !!(MASTER_ITEM_KEY in pinnedItem && pinnedItem[MASTER_ITEM_KEY]);
|
||||||
|
const masterDimInfo = isMasterDim
|
||||||
|
? masterDimList.find((dimItem) => dimItem.qInfo?.qId === pinnedItem[MASTER_ITEM_KEY])
|
||||||
|
: null;
|
||||||
|
const fieldName = isMasterDim ? masterDimInfo.qData?.info[0]?.qName : pinnedItem[FIELD_KEY];
|
||||||
|
|
||||||
|
const matchFieldIndex = remainingSelectedFields.findIndex(
|
||||||
|
(item) =>
|
||||||
|
(isMasterDim && item.selections[0].qDimensionReferences?.[0]?.qId === pinnedItem[MASTER_ITEM_KEY]) ||
|
||||||
|
(!isMasterDim && (item.selections[0].qField === fieldName || item.selections[0].qReadableName === fieldName))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pinned field has already added as selected field, skip it to avoid duplicate display
|
||||||
|
const isDuplicateFieldSelected = sortedFields.findIndex((sf) => sf.qField === fieldName && !sf.isPinned) !== -1;
|
||||||
|
if (!isDuplicateFieldSelected) {
|
||||||
|
if (matchFieldIndex === -1) {
|
||||||
|
// If pinned field is not selected or is duplicated, keep it as pinned.
|
||||||
|
const pinnedFieldData = {
|
||||||
|
...validPinnedItems[i],
|
||||||
|
isPinned: true,
|
||||||
|
states: ['$'],
|
||||||
|
};
|
||||||
|
if (isMasterDim && masterDimInfo) {
|
||||||
|
pinnedFieldData.qField = fieldName;
|
||||||
|
pinnedFieldData.qName = masterDimInfo.qData.labelExpression || masterDimInfo.qData.title || fieldName;
|
||||||
|
}
|
||||||
|
sortedFields.push(pinnedFieldData);
|
||||||
|
} else {
|
||||||
|
sortedFields.push(remainingSelectedFields[matchFieldIndex]);
|
||||||
|
remainingSelectedFields.splice(matchFieldIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...sortedFields, ...remainingSelectedFields];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sortAllFields, sortSelections };
|
||||||
@@ -449,6 +449,7 @@ function nuked(configuration = {}) {
|
|||||||
selectionsComponentReference = AppSelectionsPortal({
|
selectionsComponentReference = AppSelectionsPortal({
|
||||||
element,
|
element,
|
||||||
app,
|
app,
|
||||||
|
halo,
|
||||||
});
|
});
|
||||||
root.add(selectionsComponentReference);
|
root.add(selectionsComponentReference);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user