Add Timeline View Support to ObjectTable and Enhance CSS Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a new timeline view in the ObjectTable component, allowing users to visualize data over time. - Updated view mode utilities to include timeline-specific settings for start and end dates, and color properties. - Enhanced ObjectTableViewButton to manage timeline settings and integrate timeline icon. - Added CSS styles for timeline components to improve layout and visual feedback. - Refactored StateTag component to utilize a utility function for state representation, improving maintainability.
This commit is contained in:
parent
789e66b0f3
commit
d02895b78d
@ -1693,6 +1693,105 @@ body.objectKanbanColumnResizing * {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.objectTimelineContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineContainer .simplebar-mask {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineItemLabelContainer {
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRow {
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRowHeaderLabelContainer {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRowHeaderLabel {
|
||||||
|
height: 100%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding-left: 44px;
|
||||||
|
background: transparent;
|
||||||
|
transition:
|
||||||
|
background 0.2s ease,
|
||||||
|
box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRowHeaderLabel:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
opacity: 1;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--color-table-header-bg) 0%,
|
||||||
|
var(--color-table-header-bg) 70%,
|
||||||
|
transparent calc(100% - 40px)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineShadow .objectTimelineRowHeaderLabel {
|
||||||
|
background: var(--color-table-header-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRowLabelContainer {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRowLabel {
|
||||||
|
height: 100%;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
box-shadow 0.3s;
|
||||||
|
background: var(--layout-header-bg, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .objectTimelineShadow .objectTimelineRowLabel,
|
||||||
|
.dark-mode .objectTimelineShadow .objectTimelineRowHeaderLabel {
|
||||||
|
box-shadow: -2px -1px 8px 4px rgba(253, 253, 253, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineRow:hover,
|
||||||
|
.objectTimelineRow:hover .objectTimelineRowLabel {
|
||||||
|
background-color: var(--color-table-header-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineItemLabel {
|
||||||
|
display: block;
|
||||||
|
margin-top: -1.5px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineItemLabel-reference {
|
||||||
|
font-family: 'DM Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.objectTimelineItemWithEndDate {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.electron-body .ant-modal-wrap,
|
.electron-body .ant-modal-wrap,
|
||||||
.electron-body .ant-modal-mask,
|
.electron-body .ant-modal-mask,
|
||||||
.electron-body .ant-drawer {
|
.electron-body .ant-drawer {
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
import ObjectProperty from './ObjectProperty'
|
import ObjectProperty from './ObjectProperty'
|
||||||
import ObjectCard from './ObjectCard'
|
import ObjectCard from './ObjectCard'
|
||||||
import ObjectKanban from './ObjectKanban'
|
import ObjectKanban from './ObjectKanban'
|
||||||
|
import ObjectTimeline from './ObjectTimeline'
|
||||||
import FilterSidebar from './FilterSidebar'
|
import FilterSidebar from './FilterSidebar'
|
||||||
import SortSidebar from './SortSidebar'
|
import SortSidebar from './SortSidebar'
|
||||||
import CheckIcon from '../../Icons/CheckIcon'
|
import CheckIcon from '../../Icons/CheckIcon'
|
||||||
@ -59,7 +60,12 @@ import { hasActionPermission } from '../../../database/permissions'
|
|||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { ObjectTableFilterContext } from './ObjectTableFilterContext'
|
import { ObjectTableFilterContext } from './ObjectTableFilterContext'
|
||||||
import ObjectListViewContext from '../context/ObjectListViewContext'
|
import ObjectListViewContext from '../context/ObjectListViewContext'
|
||||||
import { isCardsView, isKanbanView, normalizeViewMode } from './viewModeUtils'
|
import {
|
||||||
|
isCardsView,
|
||||||
|
isKanbanView,
|
||||||
|
isTimelineView,
|
||||||
|
normalizeViewMode
|
||||||
|
} from './viewModeUtils'
|
||||||
import { areValuesEqual } from '../utils/Utils'
|
import { areValuesEqual } from '../utils/Utils'
|
||||||
import MissingPlaceholder from './MissingPlaceholder'
|
import MissingPlaceholder from './MissingPlaceholder'
|
||||||
import LoadingPlaceholder from './LoadingPlaceholder'
|
import LoadingPlaceholder from './LoadingPlaceholder'
|
||||||
@ -319,6 +325,7 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
const isCards = isCardsView(viewMode)
|
const isCards = isCardsView(viewMode)
|
||||||
const isKanban = isKanbanView(viewMode)
|
const isKanban = isKanbanView(viewMode)
|
||||||
|
const isTimeline = isTimelineView(viewMode)
|
||||||
const kanbanRef = useRef(null)
|
const kanbanRef = useRef(null)
|
||||||
const prevViewModeKeyRef = useRef(null)
|
const prevViewModeKeyRef = useRef(null)
|
||||||
const { token, userProfile } = useContext(AuthContext)
|
const { token, userProfile } = useContext(AuthContext)
|
||||||
@ -379,7 +386,7 @@ const ObjectTable = forwardRef(
|
|||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
|
||||||
}
|
}
|
||||||
if (isCards || isKanban) {
|
if (isCards || isKanban || isTimeline) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
|
||||||
}
|
}
|
||||||
if (isElectron) {
|
if (isElectron) {
|
||||||
@ -388,7 +395,7 @@ const ObjectTable = forwardRef(
|
|||||||
if (isMobile && isElectron) {
|
if (isMobile && isElectron) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
|
||||||
}
|
}
|
||||||
if ((isCards || isKanban) && isElectron) {
|
if ((isCards || isKanban || isTimeline) && isElectron) {
|
||||||
adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)'
|
adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)'
|
||||||
}
|
}
|
||||||
const tableRef = useRef(null)
|
const tableRef = useRef(null)
|
||||||
@ -1514,7 +1521,13 @@ const ObjectTable = forwardRef(
|
|||||||
}, [token, initialized, connected])
|
}, [token, initialized, connected])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const viewModeKey = `${viewMode.type}:${viewMode?.settings?.categoryProperty || ''}`
|
const viewModeKey = [
|
||||||
|
viewMode.type,
|
||||||
|
viewMode?.settings?.categoryProperty || '',
|
||||||
|
viewMode?.settings?.startDate || '',
|
||||||
|
viewMode?.settings?.endDate || '',
|
||||||
|
viewMode?.settings?.colorProperty || ''
|
||||||
|
].join(':')
|
||||||
|
|
||||||
if (connected !== true || token == null || !initialized) {
|
if (connected !== true || token == null || !initialized) {
|
||||||
prevViewModeKeyRef.current = viewModeKey
|
prevViewModeKeyRef.current = viewModeKey
|
||||||
@ -1525,6 +1538,7 @@ const ObjectTable = forwardRef(
|
|||||||
prevViewModeKeyRef.current = viewModeKey
|
prevViewModeKeyRef.current = viewModeKey
|
||||||
|
|
||||||
if (isKanban && !viewMode?.settings?.categoryProperty) return
|
if (isKanban && !viewMode?.settings?.categoryProperty) return
|
||||||
|
if (isTimeline && !viewMode?.settings?.startDate) return
|
||||||
|
|
||||||
loadPage(
|
loadPage(
|
||||||
initialPage,
|
initialPage,
|
||||||
@ -1533,13 +1547,17 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
isKanban,
|
isKanban,
|
||||||
|
isTimeline,
|
||||||
connected,
|
connected,
|
||||||
token,
|
token,
|
||||||
initialized,
|
initialized,
|
||||||
loadPage,
|
loadPage,
|
||||||
initialPage,
|
initialPage,
|
||||||
viewMode.type,
|
viewMode.type,
|
||||||
viewMode?.settings?.categoryProperty
|
viewMode?.settings?.categoryProperty,
|
||||||
|
viewMode?.settings?.startDate,
|
||||||
|
viewMode?.settings?.endDate,
|
||||||
|
viewMode?.settings?.colorProperty
|
||||||
])
|
])
|
||||||
|
|
||||||
// Watch for changes in type and masterFilter, reset component state when they change
|
// Watch for changes in type and masterFilter, reset component state when they change
|
||||||
@ -2135,6 +2153,38 @@ const ObjectTable = forwardRef(
|
|||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
) : isTimeline ? (
|
||||||
|
<div style={{ width: '100%', height: '100%', minHeight: 0 }}>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<ObjectTimeline
|
||||||
|
type={type}
|
||||||
|
records={tableData}
|
||||||
|
model={model}
|
||||||
|
startDate={viewMode.settings?.startDate}
|
||||||
|
endDate={viewMode.settings?.endDate}
|
||||||
|
colorProperty={viewMode.settings?.colorProperty}
|
||||||
|
filter={effectiveFilter}
|
||||||
|
masterFilter={resolvedMasterFilter}
|
||||||
|
isEditing={isEditing}
|
||||||
|
rowActions={rowActions}
|
||||||
|
renderActions={renderActions}
|
||||||
|
lazyLoading={lazyLoading}
|
||||||
|
loading={loading}
|
||||||
|
skeletonBoundaryIds={skeletonBoundaryIds}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
rowWrapper={(record, row) => (
|
||||||
|
<RowForm
|
||||||
|
key={record._id}
|
||||||
|
record={record}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onRegister={registerForm}
|
||||||
|
>
|
||||||
|
{row}
|
||||||
|
</RowForm>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
</div>
|
||||||
) : isCards ? (
|
) : isCards ? (
|
||||||
<div
|
<div
|
||||||
className='objectTableCardsContainer'
|
className='objectTableCardsContainer'
|
||||||
|
|||||||
@ -14,19 +14,23 @@ import {
|
|||||||
import GridIcon from '../../Icons/GridIcon'
|
import GridIcon from '../../Icons/GridIcon'
|
||||||
import ListIcon from '../../Icons/ListIcon'
|
import ListIcon from '../../Icons/ListIcon'
|
||||||
import KanbanIcon from '../../Icons/KanbanIcon'
|
import KanbanIcon from '../../Icons/KanbanIcon'
|
||||||
|
import TimelineIcon from '../../Icons/TimelineIcon'
|
||||||
import SettingsIcon from '../../Icons/SettingsIcon'
|
import SettingsIcon from '../../Icons/SettingsIcon'
|
||||||
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import {
|
import {
|
||||||
getViewModeType,
|
getViewModeType,
|
||||||
isKanbanCategoryProperty,
|
isKanbanCategoryProperty,
|
||||||
|
isTimelineDateProperty,
|
||||||
|
isTimelineColorProperty,
|
||||||
normalizeViewMode
|
normalizeViewMode
|
||||||
} from './viewModeUtils'
|
} from './viewModeUtils'
|
||||||
|
|
||||||
const VIEW_MODE_OPTIONS = [
|
const VIEW_MODE_OPTIONS = [
|
||||||
{ type: 'list', label: 'List' },
|
{ type: 'list', label: 'List' },
|
||||||
{ type: 'cards', label: 'Cards' },
|
{ type: 'cards', label: 'Cards' },
|
||||||
{ type: 'kanban', label: 'Kanban' }
|
{ type: 'kanban', label: 'Kanban' },
|
||||||
|
{ type: 'timeline', label: 'Timeline' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
@ -52,18 +56,33 @@ const ObjectTableViewButton = ({
|
|||||||
) || [],
|
) || [],
|
||||||
[model]
|
[model]
|
||||||
)
|
)
|
||||||
|
const dateProperties = useMemo(
|
||||||
|
() => model?.properties?.filter(isTimelineDateProperty) || [],
|
||||||
|
[model]
|
||||||
|
)
|
||||||
|
|
||||||
|
const colorProperties = useMemo(
|
||||||
|
() => model?.properties?.filter(isTimelineColorProperty) || [],
|
||||||
|
[model]
|
||||||
|
)
|
||||||
|
|
||||||
const hasKanbanOption = categoryProperties.length > 0
|
const hasKanbanOption = categoryProperties.length > 0
|
||||||
|
const hasTimelineOption = dateProperties.length > 0
|
||||||
const defaultCategoryProperty =
|
const defaultCategoryProperty =
|
||||||
categoryProperties.find((property) => property.type === 'state')?.name ||
|
categoryProperties.find((property) => property.type === 'state')?.name ||
|
||||||
categoryProperties[0]?.name
|
categoryProperties[0]?.name
|
||||||
|
const defaultStartDate = dateProperties[0]?.name
|
||||||
|
const defaultColorProperty =
|
||||||
|
colorProperties[0]?.color || colorProperties[0]?.state
|
||||||
|
|
||||||
const availableViewModes = useMemo(
|
const availableViewModes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
VIEW_MODE_OPTIONS.filter(
|
VIEW_MODE_OPTIONS.filter(
|
||||||
(option) => option.type !== 'kanban' || hasKanbanOption
|
(option) =>
|
||||||
|
(option.type !== 'kanban' || hasKanbanOption) &&
|
||||||
|
(option.type !== 'timeline' || hasTimelineOption)
|
||||||
).map((option) => option.type),
|
).map((option) => option.type),
|
||||||
[hasKanbanOption]
|
[hasKanbanOption, hasTimelineOption]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTypeChange = (nextType) => {
|
const handleTypeChange = (nextType) => {
|
||||||
@ -87,6 +106,23 @@ const ObjectTableViewButton = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextType === 'timeline') {
|
||||||
|
const existingSettings =
|
||||||
|
normalizedViewMode.type === 'timeline'
|
||||||
|
? normalizedViewMode.settings
|
||||||
|
: undefined
|
||||||
|
setViewMode({
|
||||||
|
type: 'timeline',
|
||||||
|
settings: {
|
||||||
|
startDate: existingSettings?.startDate || defaultStartDate,
|
||||||
|
...(existingSettings?.endDate
|
||||||
|
? { endDate: existingSettings.endDate }
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setViewMode({ type: nextType })
|
setViewMode({ type: nextType })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +133,16 @@ const ObjectTableViewButton = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTimelineSettingChange = (name, value) => {
|
||||||
|
const settings = {
|
||||||
|
...normalizedViewMode.settings,
|
||||||
|
[name]: value
|
||||||
|
}
|
||||||
|
if (!settings.endDate) delete settings.endDate
|
||||||
|
if (!settings.colorProperty) delete settings.colorProperty
|
||||||
|
setViewMode({ type: 'timeline', settings })
|
||||||
|
}
|
||||||
|
|
||||||
const { onClick: buttonOnClick, ...restButtonProps } = buttonProps
|
const { onClick: buttonOnClick, ...restButtonProps } = buttonProps
|
||||||
|
|
||||||
const handleCycleView = (event) => {
|
const handleCycleView = (event) => {
|
||||||
@ -115,6 +161,8 @@ const ObjectTableViewButton = ({
|
|||||||
<GridIcon />
|
<GridIcon />
|
||||||
) : normalizedViewMode.type === 'kanban' ? (
|
) : normalizedViewMode.type === 'kanban' ? (
|
||||||
<KanbanIcon />
|
<KanbanIcon />
|
||||||
|
) : normalizedViewMode.type === 'timeline' ? (
|
||||||
|
<TimelineIcon />
|
||||||
) : (
|
) : (
|
||||||
<ListIcon />
|
<ListIcon />
|
||||||
)
|
)
|
||||||
@ -130,7 +178,9 @@ const ObjectTableViewButton = ({
|
|||||||
>
|
>
|
||||||
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
<Flex vertical gap='middle' style={{ margin: '4px 8px' }}>
|
||||||
{VIEW_MODE_OPTIONS.filter(
|
{VIEW_MODE_OPTIONS.filter(
|
||||||
(option) => option.type !== 'kanban' || hasKanbanOption
|
(option) =>
|
||||||
|
(option.type !== 'kanban' || hasKanbanOption) &&
|
||||||
|
(option.type !== 'timeline' || hasTimelineOption)
|
||||||
).map((option) => (
|
).map((option) => (
|
||||||
<Radio key={option.type} value={option.type}>
|
<Radio key={option.type} value={option.type}>
|
||||||
<Flex
|
<Flex
|
||||||
@ -139,7 +189,8 @@ const ObjectTableViewButton = ({
|
|||||||
style={{ marginLeft: '4px' }}
|
style={{ marginLeft: '4px' }}
|
||||||
>
|
>
|
||||||
<Text>{option.label}</Text>
|
<Text>{option.label}</Text>
|
||||||
{option.type === 'kanban' && (
|
{(option.type === 'kanban' ||
|
||||||
|
option.type === 'timeline') && (
|
||||||
<Button
|
<Button
|
||||||
type='text'
|
type='text'
|
||||||
size='small'
|
size='small'
|
||||||
@ -148,7 +199,7 @@ const ObjectTableViewButton = ({
|
|||||||
style={{ fontSize: '14px', marginTop: '2.5px' }}
|
style={{ fontSize: '14px', marginTop: '2.5px' }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
disabled={normalizedViewMode.type !== 'kanban'}
|
disabled={normalizedViewMode.type !== option.type}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@ -185,27 +236,86 @@ const ObjectTableViewButton = ({
|
|||||||
getContainer={() => document.body}
|
getContainer={() => document.body}
|
||||||
width={520}
|
width={520}
|
||||||
>
|
>
|
||||||
<Flex vertical gap='middle' onMouseDown={(event) => event.stopPropagation()}>
|
<Flex
|
||||||
|
vertical
|
||||||
|
gap='middle'
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
<Flex gap='middle'>
|
<Flex gap='middle'>
|
||||||
<InfoCircleIcon />
|
<InfoCircleIcon />
|
||||||
<Text strong>Kanban settings</Text>
|
<Text strong>
|
||||||
|
{normalizedViewMode.type === 'timeline'
|
||||||
|
? 'Timeline settings'
|
||||||
|
: 'Kanban settings'}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text>
|
{normalizedViewMode.type === 'timeline' ? (
|
||||||
Select the property used to group cards into columns:
|
<>
|
||||||
</Text>
|
<Text>
|
||||||
<Select
|
Select the required start property and an optional end property:
|
||||||
style={{ width: '100%' }}
|
</Text>
|
||||||
placeholder='Select category property'
|
<Select
|
||||||
value={
|
style={{ width: '100%' }}
|
||||||
normalizedViewMode.settings?.categoryProperty ||
|
placeholder='Select start date property'
|
||||||
defaultCategoryProperty
|
value={
|
||||||
}
|
normalizedViewMode.settings?.startDate || defaultStartDate
|
||||||
onChange={handleCategoryPropertyChange}
|
}
|
||||||
options={categoryProperties.map((property) => ({
|
onChange={(value) =>
|
||||||
value: property.name,
|
handleTimelineSettingChange('startDate', value)
|
||||||
label: property.label || property.name
|
}
|
||||||
}))}
|
options={dateProperties.map((property) => ({
|
||||||
/>
|
value: property.name,
|
||||||
|
label: property.label || property.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder='Select optional end date property'
|
||||||
|
value={normalizedViewMode.settings?.endDate}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleTimelineSettingChange('endDate', value)
|
||||||
|
}
|
||||||
|
options={dateProperties.map((property) => ({
|
||||||
|
value: property.name,
|
||||||
|
label: property.label || property.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder='Select optional color property'
|
||||||
|
value={
|
||||||
|
normalizedViewMode.settings?.colorProperty ||
|
||||||
|
defaultColorProperty
|
||||||
|
}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleTimelineSettingChange('colorProperty', value)
|
||||||
|
}
|
||||||
|
options={colorProperties.map((property) => ({
|
||||||
|
value: property.name,
|
||||||
|
label: property.label || property.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>Select the property used to group cards into columns:</Text>
|
||||||
|
<Select
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder='Select category property'
|
||||||
|
value={
|
||||||
|
normalizedViewMode.settings?.categoryProperty ||
|
||||||
|
defaultCategoryProperty
|
||||||
|
}
|
||||||
|
onChange={handleCategoryPropertyChange}
|
||||||
|
options={categoryProperties.map((property) => ({
|
||||||
|
value: property.name,
|
||||||
|
label: property.label || property.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Flex justify='end' gap='small'>
|
<Flex justify='end' gap='small'>
|
||||||
<Button type='default' onClick={() => setSettingsOpen(false)}>
|
<Button type='default' onClick={() => setSettingsOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
350
src/components/Dashboard/common/ObjectTimeline.jsx
Normal file
350
src/components/Dashboard/common/ObjectTimeline.jsx
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
import {
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useCallback
|
||||||
|
} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Flex, Typography, Card, Divider } from 'antd'
|
||||||
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
|
import ScrollBox from './ScrollBox'
|
||||||
|
import MissingPlaceholder from './MissingPlaceholder'
|
||||||
|
import LoadingPlaceholder from './LoadingPlaceholder'
|
||||||
|
import ObjectTimelineRow from './ObjectTimelineRow'
|
||||||
|
import {
|
||||||
|
getTimelineRange,
|
||||||
|
getTimelineRangeFromValues,
|
||||||
|
getTimelineTicks
|
||||||
|
} from './timelineUtils'
|
||||||
|
import cn from 'classnames'
|
||||||
|
import { getStateTagInfo } from '../utils/Utils'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
const LABEL_WIDTH = 226
|
||||||
|
const SHADOW_PADDING = 12
|
||||||
|
const MIN_TICK_SECTION_WIDTH = 140
|
||||||
|
|
||||||
|
const ObjectTimeline = ({
|
||||||
|
type,
|
||||||
|
records,
|
||||||
|
model,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
colorProperty,
|
||||||
|
filter = {},
|
||||||
|
masterFilter = {},
|
||||||
|
isEditing = false,
|
||||||
|
rowActions = [],
|
||||||
|
renderActions,
|
||||||
|
lazyLoading = false,
|
||||||
|
loading = false,
|
||||||
|
skeletonBoundaryIds = {},
|
||||||
|
onScroll,
|
||||||
|
rowWrapper
|
||||||
|
}) => {
|
||||||
|
const { getModelPropertyValues } = useContext(ApiServerContext)
|
||||||
|
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
|
||||||
|
getModelPropertyValuesRef.current = getModelPropertyValues
|
||||||
|
|
||||||
|
const [startValues, setStartValues] = useState([])
|
||||||
|
const [endValues, setEndValues] = useState([])
|
||||||
|
|
||||||
|
const rangeQueryKey = useMemo(
|
||||||
|
() =>
|
||||||
|
JSON.stringify({
|
||||||
|
type,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
filter,
|
||||||
|
masterFilter
|
||||||
|
}),
|
||||||
|
[type, startDate, endDate, filter, masterFilter]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!type || !startDate) {
|
||||||
|
setStartValues([])
|
||||||
|
setEndValues([])
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false
|
||||||
|
const { filter: activeFilter, masterFilter: activeMasterFilter } =
|
||||||
|
JSON.parse(rangeQueryKey)
|
||||||
|
|
||||||
|
const loadRangeValues = async () => {
|
||||||
|
setStartValues([])
|
||||||
|
setEndValues([])
|
||||||
|
try {
|
||||||
|
const startPromise = getModelPropertyValuesRef.current(
|
||||||
|
type,
|
||||||
|
startDate,
|
||||||
|
{
|
||||||
|
filter: activeFilter,
|
||||||
|
masterFilter: activeMasterFilter
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const endPromise = endDate
|
||||||
|
? getModelPropertyValuesRef.current(type, endDate, {
|
||||||
|
filter: activeFilter,
|
||||||
|
masterFilter: activeMasterFilter
|
||||||
|
})
|
||||||
|
: Promise.resolve([])
|
||||||
|
const [nextStartValues, nextEndValues] = await Promise.all([
|
||||||
|
startPromise,
|
||||||
|
endPromise
|
||||||
|
])
|
||||||
|
if (cancelled) return
|
||||||
|
setStartValues(Array.isArray(nextStartValues) ? nextStartValues : [])
|
||||||
|
setEndValues(Array.isArray(nextEndValues) ? nextEndValues : [])
|
||||||
|
} catch (error) {
|
||||||
|
if (cancelled) return
|
||||||
|
console.error('Error fetching timeline date values:', error)
|
||||||
|
setStartValues([])
|
||||||
|
setEndValues([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRangeValues()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [endDate, rangeQueryKey, startDate, type])
|
||||||
|
|
||||||
|
const [showShadow, setShowShadow] = useState(false)
|
||||||
|
|
||||||
|
const handleScroll = useCallback(
|
||||||
|
(event) => {
|
||||||
|
const { scrollLeft } = event.target
|
||||||
|
setShowShadow(scrollLeft > 0)
|
||||||
|
onScroll?.(event)
|
||||||
|
},
|
||||||
|
[onScroll]
|
||||||
|
)
|
||||||
|
|
||||||
|
const timelineRange = useMemo(() => {
|
||||||
|
const fromValues = getTimelineRangeFromValues(startValues, endValues)
|
||||||
|
if (fromValues) return fromValues
|
||||||
|
return getTimelineRange(records, startDate, endDate)
|
||||||
|
}, [endDate, endValues, records, startDate, startValues])
|
||||||
|
|
||||||
|
const range = useMemo(
|
||||||
|
() => timelineRange || getTimelineRangeFromValues([new Date()]),
|
||||||
|
[timelineRange]
|
||||||
|
)
|
||||||
|
|
||||||
|
const calculatedLabelWidth = useMemo(() => {
|
||||||
|
return LABEL_WIDTH + rowActions.length * 32
|
||||||
|
}, [rowActions.length])
|
||||||
|
|
||||||
|
const ticks = useMemo(() => getTimelineTicks(range.start, range.end), [range])
|
||||||
|
const tickCount = Math.max(1, ticks.length - 1)
|
||||||
|
const minTrackWidth = MIN_TICK_SECTION_WIDTH * tickCount
|
||||||
|
const minContentWidth = calculatedLabelWidth + minTrackWidth
|
||||||
|
|
||||||
|
const nameProperty = model.properties?.find(
|
||||||
|
(property) => property.name === 'name'
|
||||||
|
)
|
||||||
|
|
||||||
|
const referenceProperty = model.properties?.find(
|
||||||
|
(property) => property.name === '_reference'
|
||||||
|
)
|
||||||
|
|
||||||
|
const colorPropertyObject = model.properties?.find(
|
||||||
|
(property) => property.name === colorProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!startDate) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message='Select a start date property in timeline settings.'
|
||||||
|
hasBackground
|
||||||
|
hasBorder={false}
|
||||||
|
height='260px'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading && !lazyLoading && records.length === 0) {
|
||||||
|
return (
|
||||||
|
<MissingPlaceholder
|
||||||
|
message='No data.'
|
||||||
|
hasBackground
|
||||||
|
hasBorder={false}
|
||||||
|
height='260px'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length === 0) {
|
||||||
|
return (
|
||||||
|
<LoadingPlaceholder
|
||||||
|
message='Loading, please wait...'
|
||||||
|
hasBackground
|
||||||
|
hasBorder={false}
|
||||||
|
height='260px'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
variant='borderless'
|
||||||
|
styles={{ body: { height: '100%', padding: 0 } }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn('objectTimelineContainer', {
|
||||||
|
objectTimelineShadow: showShadow
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ScrollBox
|
||||||
|
style={{ width: '100%', height: '100%', zIndex: 2 }}
|
||||||
|
scrollableNodeProps={{ onScroll: handleScroll }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
minWidth: minContentWidth,
|
||||||
|
minHeight: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 3,
|
||||||
|
width: '100%',
|
||||||
|
minWidth: minContentWidth,
|
||||||
|
height: 47,
|
||||||
|
background: 'var(--color-table-header-bg, #fff)',
|
||||||
|
borderBottom:
|
||||||
|
'1px solid color-mix(in srgb, var(--color-text) 12%, transparent)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='objectTimelineRowHeaderLabelContainer'
|
||||||
|
style={{ minWidth: calculatedLabelWidth + SHADOW_PADDING }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
className='objectTimelineRowHeaderLabel'
|
||||||
|
style={{
|
||||||
|
width: calculatedLabelWidth,
|
||||||
|
minWidth: calculatedLabelWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Divider
|
||||||
|
type='vertical'
|
||||||
|
style={{ margin: '0 8px 0 0', height: 22.7 }}
|
||||||
|
/>
|
||||||
|
<Text strong>
|
||||||
|
{nameProperty?.label
|
||||||
|
? nameProperty?.label
|
||||||
|
: referenceProperty?.label}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
marginLeft: SHADOW_PADDING * -1,
|
||||||
|
minWidth: minTrackWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ticks.map((tick, index) => {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={tick.value}
|
||||||
|
type='secondary'
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 13,
|
||||||
|
left: `${(index / tickCount) * 100}%`,
|
||||||
|
padding: '0 4px',
|
||||||
|
fontSize: 11,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
transform:
|
||||||
|
index === tickCount
|
||||||
|
? 'translateX(-100%)'
|
||||||
|
: 'translateX(-50%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tick.label}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
{records.map((record) => {
|
||||||
|
const skeletonBoundary =
|
||||||
|
record._id === skeletonBoundaryIds.next
|
||||||
|
? 'next'
|
||||||
|
: record._id === skeletonBoundaryIds.previous
|
||||||
|
? 'previous'
|
||||||
|
: undefined
|
||||||
|
var color = 'var(--color-primary, #1677ff)'
|
||||||
|
if (colorPropertyObject) {
|
||||||
|
if (
|
||||||
|
colorPropertyObject.type === 'state' &&
|
||||||
|
record[colorPropertyObject.name]?.type
|
||||||
|
) {
|
||||||
|
console.log('GOT STATE')
|
||||||
|
color = `var(--color-${getStateTagInfo(record[colorPropertyObject.name].type).status}, #1677ff)`
|
||||||
|
} else {
|
||||||
|
color = record[colorPropertyObject.name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const row = (
|
||||||
|
<ObjectTimelineRow
|
||||||
|
key={record._id}
|
||||||
|
model={model}
|
||||||
|
record={record}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
rangeStart={range.start}
|
||||||
|
rangeEnd={range.end}
|
||||||
|
labelWidth={calculatedLabelWidth}
|
||||||
|
minTrackWidth={minTrackWidth}
|
||||||
|
tickCount={tickCount}
|
||||||
|
isEditing={isEditing}
|
||||||
|
rowActions={rowActions}
|
||||||
|
renderActions={renderActions}
|
||||||
|
lazyLoading={lazyLoading}
|
||||||
|
skeletonBoundary={skeletonBoundary}
|
||||||
|
shadowPadding={SHADOW_PADDING}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return rowWrapper ? rowWrapper(record, row) : row
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectTimeline.propTypes = {
|
||||||
|
type: PropTypes.string,
|
||||||
|
records: PropTypes.array.isRequired,
|
||||||
|
model: PropTypes.object.isRequired,
|
||||||
|
startDate: PropTypes.string,
|
||||||
|
endDate: PropTypes.string,
|
||||||
|
colorProperty: PropTypes.string,
|
||||||
|
filter: PropTypes.object,
|
||||||
|
masterFilter: PropTypes.object,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
|
rowActions: PropTypes.array,
|
||||||
|
renderActions: PropTypes.func.isRequired,
|
||||||
|
lazyLoading: PropTypes.bool,
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
skeletonBoundaryIds: PropTypes.object,
|
||||||
|
onScroll: PropTypes.func,
|
||||||
|
rowWrapper: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectTimeline
|
||||||
121
src/components/Dashboard/common/ObjectTimelineItem.jsx
Normal file
121
src/components/Dashboard/common/ObjectTimelineItem.jsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Flex, Typography } from 'antd'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { getTimelineItemPosition } from './timelineUtils'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const ObjectTimelineItem = ({
|
||||||
|
label,
|
||||||
|
startValue,
|
||||||
|
endValue,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
labelIsReference = false,
|
||||||
|
isSkeleton = false,
|
||||||
|
color = 'var(--color-primary, #1677ff)'
|
||||||
|
}) => {
|
||||||
|
if (isSkeleton) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '5%',
|
||||||
|
width: '35%',
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: 'color-mix(in srgb, var(--color-text) 8%, transparent)'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = getTimelineItemPosition(
|
||||||
|
startValue,
|
||||||
|
endValue,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd
|
||||||
|
)
|
||||||
|
if (!position) {
|
||||||
|
return <Text type='secondary'>No valid start date</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
const startLabel = dayjs(startValue).format('YYYY-MM-DD HH:mm')
|
||||||
|
const endLabel =
|
||||||
|
!position.isPoint && endValue && dayjs(endValue).isValid()
|
||||||
|
? dayjs(endValue).format('YYYY-MM-DD HH:mm')
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
title={endLabel ? `${startLabel} – ${endLabel}` : startLabel}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${position.left}%`,
|
||||||
|
width: position.isPoint ? 'max-content' : `${position.width}%`,
|
||||||
|
minWidth: position.isPoint ? undefined : 28,
|
||||||
|
maxWidth: `calc(${100 - position.left}% - 4px)`,
|
||||||
|
height: 30,
|
||||||
|
padding: '0 10px',
|
||||||
|
overflow: 'visible',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
borderRadius: 8,
|
||||||
|
color: 'white',
|
||||||
|
background: color,
|
||||||
|
boxShadow:
|
||||||
|
'0 1px 3px color-mix(in srgb, var(--color-primary) 35%, transparent)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='objectTimelineItemLabelContainer'
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(
|
||||||
|
to right,
|
||||||
|
${color} 20%,
|
||||||
|
color-mix(in srgb, ${color} 35%, transparent) 100%
|
||||||
|
)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={classNames('objectTimelineItemLabel', {
|
||||||
|
'objectTimelineItemLabel-reference': labelIsReference,
|
||||||
|
objectTimelineItemWithEndDate: endValue
|
||||||
|
})}
|
||||||
|
style={{
|
||||||
|
flex: position.isPoint ? '0 0 auto' : '1 1 auto',
|
||||||
|
color: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectTimelineItem.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
startValue: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.instanceOf(Date)
|
||||||
|
]),
|
||||||
|
endValue: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]),
|
||||||
|
rangeStart: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.instanceOf(Date)
|
||||||
|
]).isRequired,
|
||||||
|
rangeEnd: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.instanceOf(Date)
|
||||||
|
]).isRequired,
|
||||||
|
isSkeleton: PropTypes.bool,
|
||||||
|
labelIsReference: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectTimelineItem
|
||||||
160
src/components/Dashboard/common/ObjectTimelineRow.jsx
Normal file
160
src/components/Dashboard/common/ObjectTimelineRow.jsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { createElement } from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Flex, Skeleton } from 'antd'
|
||||||
|
import { getPropertyValue } from '../../../database/ObjectModels'
|
||||||
|
import ObjectProperty from './ObjectProperty'
|
||||||
|
import ObjectTimelineItem from './ObjectTimelineItem'
|
||||||
|
|
||||||
|
const ObjectTimelineRow = ({
|
||||||
|
model,
|
||||||
|
record,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd,
|
||||||
|
labelWidth,
|
||||||
|
minTrackWidth,
|
||||||
|
tickCount,
|
||||||
|
isEditing = false,
|
||||||
|
rowActions = [],
|
||||||
|
renderActions,
|
||||||
|
lazyLoading = false,
|
||||||
|
skeletonBoundary,
|
||||||
|
color = 'var(--color-primary, #1677ff)',
|
||||||
|
shadowPadding = 12
|
||||||
|
}) => {
|
||||||
|
const isSkeleton = record?.isSkeleton === true
|
||||||
|
const nameProperty = model.properties?.find(
|
||||||
|
(property) => property.name === 'name'
|
||||||
|
)
|
||||||
|
const referenceProperty = model.properties?.find(
|
||||||
|
(property) => property.name === '_reference'
|
||||||
|
)
|
||||||
|
const nameValue = getPropertyValue(record, 'name')
|
||||||
|
const modelPrefix = model.prefix
|
||||||
|
const label =
|
||||||
|
typeof nameValue === 'string'
|
||||||
|
? nameValue
|
||||||
|
: modelPrefix + ':' + String(record?._reference || '')
|
||||||
|
const startValue = getPropertyValue(record, startDate)
|
||||||
|
const endValue = endDate ? getPropertyValue(record, endDate) : undefined
|
||||||
|
const actions =
|
||||||
|
!isSkeleton && rowActions.length > 0
|
||||||
|
? renderActions(record, lazyLoading)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
data-skeleton-boundary={skeletonBoundary}
|
||||||
|
className='objectTimelineRow'
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
minWidth: labelWidth + minTrackWidth,
|
||||||
|
minHeight: 49.5,
|
||||||
|
borderBottom: '1px solid var(--color-table-row-border)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='objectTimelineRowLabelContainer'
|
||||||
|
style={{ minWidth: labelWidth + shadowPadding }}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
gap='small'
|
||||||
|
className='objectTimelineRowLabel'
|
||||||
|
style={{
|
||||||
|
width: labelWidth,
|
||||||
|
minWidth: labelWidth,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
padding: '4px 12px 4px 8px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSkeleton ? (
|
||||||
|
<Skeleton.Input active size='small' style={{ width: '75%' }} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Flex style={{ width: 29 }} justify='center'>
|
||||||
|
{createElement(model.icon, {
|
||||||
|
style: { fontSize: 14, flex: '0 0 auto' }
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
paddingLeft: '8px',
|
||||||
|
minWidth: 168
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nameProperty ? (
|
||||||
|
<ObjectProperty
|
||||||
|
{...nameProperty}
|
||||||
|
objectData={record}
|
||||||
|
isEditing={isEditing}
|
||||||
|
style={{ minWidth: 0 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ObjectProperty
|
||||||
|
{...referenceProperty}
|
||||||
|
objectData={record}
|
||||||
|
isEditing={isEditing}
|
||||||
|
style={{ fontWeight: 600, minWidth: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{actions}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
<Flex
|
||||||
|
align='center'
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
minWidth: minTrackWidth,
|
||||||
|
padding: '0 4px',
|
||||||
|
marginLeft: shadowPadding * -1,
|
||||||
|
backgroundImage:
|
||||||
|
'linear-gradient(to right, color-mix(in srgb, var(--color-text) 7%, transparent) 1px, transparent 1px)',
|
||||||
|
backgroundSize: `${100 / tickCount}% 100%`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ObjectTimelineItem
|
||||||
|
label={label}
|
||||||
|
labelIsReference={nameProperty == undefined}
|
||||||
|
startValue={startValue}
|
||||||
|
endValue={endValue}
|
||||||
|
rangeStart={rangeStart}
|
||||||
|
rangeEnd={rangeEnd}
|
||||||
|
isSkeleton={isSkeleton}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectTimelineRow.propTypes = {
|
||||||
|
model: PropTypes.object.isRequired,
|
||||||
|
record: PropTypes.object.isRequired,
|
||||||
|
color: PropTypes.string,
|
||||||
|
startDate: PropTypes.string.isRequired,
|
||||||
|
endDate: PropTypes.string,
|
||||||
|
rangeStart: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
.isRequired,
|
||||||
|
rangeEnd: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||||
|
.isRequired,
|
||||||
|
labelWidth: PropTypes.number.isRequired,
|
||||||
|
minTrackWidth: PropTypes.number.isRequired,
|
||||||
|
tickCount: PropTypes.number.isRequired,
|
||||||
|
isEditing: PropTypes.bool,
|
||||||
|
rowActions: PropTypes.array,
|
||||||
|
renderActions: PropTypes.func.isRequired,
|
||||||
|
lazyLoading: PropTypes.bool,
|
||||||
|
skeletonBoundary: PropTypes.string,
|
||||||
|
shadowPadding: PropTypes.number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectTimelineRow
|
||||||
@ -2,6 +2,7 @@ import PropTypes from 'prop-types'
|
|||||||
import { Badge, Flex, Tag } from 'antd'
|
import { Badge, Flex, Tag } from 'antd'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import LoadingIcon from '../../Icons/LoadingIcon'
|
import LoadingIcon from '../../Icons/LoadingIcon'
|
||||||
|
import { getStateTagInfo } from '../utils/Utils'
|
||||||
|
|
||||||
const StateTag = ({
|
const StateTag = ({
|
||||||
state,
|
state,
|
||||||
@ -11,198 +12,7 @@ const StateTag = ({
|
|||||||
style = {}
|
style = {}
|
||||||
}) => {
|
}) => {
|
||||||
const { badgeStatus, badgeText } = useMemo(() => {
|
const { badgeStatus, badgeText } = useMemo(() => {
|
||||||
let status = 'default'
|
const { status, text } = getStateTagInfo(state)
|
||||||
let text = 'Unknown'
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case 'online':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Online'
|
|
||||||
break
|
|
||||||
case 'standby':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Standby'
|
|
||||||
break
|
|
||||||
case 'complete':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Complete'
|
|
||||||
break
|
|
||||||
case 'offline':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Offline'
|
|
||||||
break
|
|
||||||
case 'shutdown':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Shutdown'
|
|
||||||
break
|
|
||||||
case 'initializing':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Initializing'
|
|
||||||
break
|
|
||||||
case 'connecting':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Connecting'
|
|
||||||
break
|
|
||||||
case 'deploying':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Deploying'
|
|
||||||
break
|
|
||||||
case 'printing':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Printing'
|
|
||||||
break
|
|
||||||
case 'paused':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Paused'
|
|
||||||
break
|
|
||||||
case 'cancelled':
|
|
||||||
status = 'error'
|
|
||||||
text = 'Cancelled'
|
|
||||||
break
|
|
||||||
case 'loading':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Uploading'
|
|
||||||
break
|
|
||||||
case 'processing':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Processing'
|
|
||||||
break
|
|
||||||
case 'ready':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Ready'
|
|
||||||
break
|
|
||||||
case 'new':
|
|
||||||
status = 'success'
|
|
||||||
text = 'New'
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
status = 'error'
|
|
||||||
text = 'Error'
|
|
||||||
break
|
|
||||||
case 'startup':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Startup'
|
|
||||||
break
|
|
||||||
case 'draft':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Draft'
|
|
||||||
break
|
|
||||||
case 'active':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Active'
|
|
||||||
break
|
|
||||||
case 'inactive':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Inactive'
|
|
||||||
break
|
|
||||||
case 'deleted':
|
|
||||||
status = 'error'
|
|
||||||
text = 'Deleted'
|
|
||||||
break
|
|
||||||
case 'suspended':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Suspended'
|
|
||||||
break
|
|
||||||
case 'syncing':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Syncing'
|
|
||||||
break
|
|
||||||
case 'publishing':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Publishing'
|
|
||||||
break
|
|
||||||
case 'unpublishing':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Unpublishing'
|
|
||||||
break
|
|
||||||
case 'disconnected':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Disconnected'
|
|
||||||
break
|
|
||||||
case 'failed':
|
|
||||||
status = 'error'
|
|
||||||
text = 'Failed'
|
|
||||||
break
|
|
||||||
case 'queued':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Queued'
|
|
||||||
break
|
|
||||||
case 'pending':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Pending'
|
|
||||||
break
|
|
||||||
case 'used':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Used'
|
|
||||||
break
|
|
||||||
case 'consumed':
|
|
||||||
status = 'default'
|
|
||||||
text = 'Consumed'
|
|
||||||
break
|
|
||||||
case 'unconsumed':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Unconsumed'
|
|
||||||
break
|
|
||||||
case 'sent':
|
|
||||||
status = 'cyan'
|
|
||||||
text = 'Sent'
|
|
||||||
break
|
|
||||||
case 'acknowledged':
|
|
||||||
status = 'purple'
|
|
||||||
text = 'Acknowledged'
|
|
||||||
break
|
|
||||||
case 'confirmed':
|
|
||||||
status = 'purple'
|
|
||||||
text = 'Confirmed'
|
|
||||||
break
|
|
||||||
case 'ordered':
|
|
||||||
status = 'cyan'
|
|
||||||
text = 'Ordered'
|
|
||||||
break
|
|
||||||
case 'posted':
|
|
||||||
status = 'magenta'
|
|
||||||
text = 'Posted'
|
|
||||||
break
|
|
||||||
case 'authorised':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Authorised'
|
|
||||||
break
|
|
||||||
case 'declined':
|
|
||||||
status = 'error'
|
|
||||||
text = 'Declined'
|
|
||||||
break
|
|
||||||
case 'received':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Received'
|
|
||||||
break
|
|
||||||
case 'invoiced':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Invoiced'
|
|
||||||
break
|
|
||||||
case 'planned':
|
|
||||||
status = 'warning'
|
|
||||||
text = 'Planned'
|
|
||||||
break
|
|
||||||
case 'partiallyShipped':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Partially Shipped'
|
|
||||||
break
|
|
||||||
case 'shipped':
|
|
||||||
status = 'processing'
|
|
||||||
text = 'Shipped'
|
|
||||||
break
|
|
||||||
case 'delivered':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Delivered'
|
|
||||||
break
|
|
||||||
case 'paid':
|
|
||||||
status = 'success'
|
|
||||||
text = 'Paid'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
status = 'default'
|
|
||||||
text = state || 'Unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
return { badgeStatus: status, badgeText: text }
|
return { badgeStatus: status, badgeText: text }
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|||||||
95
src/components/Dashboard/common/timelineUtils.js
Normal file
95
src/components/Dashboard/common/timelineUtils.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { getPropertyValue } from '../../../database/ObjectModels'
|
||||||
|
|
||||||
|
const toTimestamp = (value) => {
|
||||||
|
if (value == null || value === '') return null
|
||||||
|
const date = dayjs(value)
|
||||||
|
return date.isValid() ? date.valueOf() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimelineDates = (values = []) => {
|
||||||
|
const timestamps = []
|
||||||
|
for (const value of values) {
|
||||||
|
const timestamp = toTimestamp(value)
|
||||||
|
if (timestamp != null) timestamps.push(timestamp)
|
||||||
|
}
|
||||||
|
return timestamps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimelineRangeFromValues = (startValues = [], endValues = []) => {
|
||||||
|
const timestamps = [
|
||||||
|
...getTimelineDates(startValues),
|
||||||
|
...getTimelineDates(endValues)
|
||||||
|
]
|
||||||
|
if (timestamps.length === 0) return null
|
||||||
|
|
||||||
|
const minimum = Math.min(...timestamps)
|
||||||
|
const maximum = Math.max(...timestamps)
|
||||||
|
const start = dayjs(minimum).startOf('day').subtract(1, 'day')
|
||||||
|
const end = dayjs(maximum).startOf('day').add(2, 'day')
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: start.valueOf(),
|
||||||
|
end: end.valueOf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimelineRange = (records, startDate, endDate) => {
|
||||||
|
const startValues = []
|
||||||
|
const endValues = []
|
||||||
|
|
||||||
|
records.forEach((record) => {
|
||||||
|
if (record?.isSkeleton) return
|
||||||
|
if (startDate) startValues.push(getPropertyValue(record, startDate))
|
||||||
|
if (endDate) endValues.push(getPropertyValue(record, endDate))
|
||||||
|
})
|
||||||
|
|
||||||
|
return getTimelineRangeFromValues(startValues, endValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimelineTicks = (rangeStart, rangeEnd) => {
|
||||||
|
if (rangeStart == null || rangeEnd == null) return []
|
||||||
|
|
||||||
|
const start = dayjs(rangeStart).startOf('day')
|
||||||
|
const end = dayjs(rangeEnd).startOf('day')
|
||||||
|
const dayCount = Math.max(1, end.diff(start, 'day'))
|
||||||
|
const lastDay = end.subtract(1, 'day')
|
||||||
|
const includeYear = start.year() !== lastDay.year() || start.year() !== end.year()
|
||||||
|
const format = includeYear ? 'MMM D YYYY' : 'MMM D'
|
||||||
|
|
||||||
|
return Array.from({ length: dayCount + 1 }, (_, index) => {
|
||||||
|
const value = start.add(index, 'day')
|
||||||
|
return {
|
||||||
|
value: value.valueOf(),
|
||||||
|
label: value.format(format)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTimelineItemPosition = (
|
||||||
|
startValue,
|
||||||
|
endValue,
|
||||||
|
rangeStart,
|
||||||
|
rangeEnd
|
||||||
|
) => {
|
||||||
|
if (startValue == null || startValue === '') return null
|
||||||
|
const start = dayjs(startValue)
|
||||||
|
const rangeStartMs = dayjs(rangeStart).valueOf()
|
||||||
|
const rangeEndMs = dayjs(rangeEnd).valueOf()
|
||||||
|
const duration = rangeEndMs - rangeStartMs
|
||||||
|
|
||||||
|
if (!start.isValid() || duration <= 0) return null
|
||||||
|
|
||||||
|
const startMs = Math.min(rangeEndMs, Math.max(rangeStartMs, start.valueOf()))
|
||||||
|
const end = endValue == null || endValue === '' ? null : dayjs(endValue)
|
||||||
|
const hasValidEnd = end?.isValid() && end.valueOf() > startMs
|
||||||
|
const endMs = hasValidEnd
|
||||||
|
? Math.min(rangeEndMs, Math.max(startMs, end.valueOf()))
|
||||||
|
: startMs
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: ((startMs - rangeStartMs) / duration) * 100,
|
||||||
|
width: ((endMs - startMs) / duration) * 100,
|
||||||
|
isPoint: !hasValidEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,8 @@ export const DEFAULT_VIEW_MODE = { type: 'list' }
|
|||||||
|
|
||||||
/** Property types that can be used as kanban column categories. */
|
/** Property types that can be used as kanban column categories. */
|
||||||
export const KANBAN_CATEGORY_PROPERTY_TYPES = ['state', 'tags']
|
export const KANBAN_CATEGORY_PROPERTY_TYPES = ['state', 'tags']
|
||||||
|
export const TIMELINE_COLOR_PROPERTY_TYPES = ['color', 'state']
|
||||||
|
export const TIMELINE_DATE_PROPERTY_TYPES = ['dateTime']
|
||||||
|
|
||||||
export const KANBAN_DEFAULT_COLUMN_WIDTH = 360
|
export const KANBAN_DEFAULT_COLUMN_WIDTH = 360
|
||||||
export const KANBAN_MIN_COLUMN_WIDTH = 200
|
export const KANBAN_MIN_COLUMN_WIDTH = 200
|
||||||
@ -9,11 +11,17 @@ export const KANBAN_MIN_COLUMN_WIDTH = 200
|
|||||||
export const isKanbanCategoryProperty = (property) =>
|
export const isKanbanCategoryProperty = (property) =>
|
||||||
KANBAN_CATEGORY_PROPERTY_TYPES.includes(property?.type)
|
KANBAN_CATEGORY_PROPERTY_TYPES.includes(property?.type)
|
||||||
|
|
||||||
|
export const isTimelineColorProperty = (property) =>
|
||||||
|
TIMELINE_COLOR_PROPERTY_TYPES.includes(property?.type)
|
||||||
|
|
||||||
|
export const isTimelineDateProperty = (property) =>
|
||||||
|
TIMELINE_DATE_PROPERTY_TYPES.includes(property?.type)
|
||||||
|
|
||||||
export const normalizeViewMode = (value) => {
|
export const normalizeViewMode = (value) => {
|
||||||
if (!value) return DEFAULT_VIEW_MODE
|
if (!value) return DEFAULT_VIEW_MODE
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return {
|
return {
|
||||||
type: value === 'cards' ? 'cards' : value === 'kanban' ? 'kanban' : 'list'
|
type: ['cards', 'kanban', 'timeline'].includes(value) ? value : 'list'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (value?.type) return value
|
if (value?.type) return value
|
||||||
@ -22,6 +30,7 @@ export const normalizeViewMode = (value) => {
|
|||||||
|
|
||||||
export const isCardsView = (vm) => normalizeViewMode(vm).type === 'cards'
|
export const isCardsView = (vm) => normalizeViewMode(vm).type === 'cards'
|
||||||
export const isKanbanView = (vm) => normalizeViewMode(vm).type === 'kanban'
|
export const isKanbanView = (vm) => normalizeViewMode(vm).type === 'kanban'
|
||||||
|
export const isTimelineView = (vm) => normalizeViewMode(vm).type === 'timeline'
|
||||||
export const getViewModeType = (vm) => normalizeViewMode(vm).type
|
export const getViewModeType = (vm) => normalizeViewMode(vm).type
|
||||||
|
|
||||||
export const toCategoryFilterValue = (value) => {
|
export const toCategoryFilterValue = (value) => {
|
||||||
|
|||||||
@ -192,6 +192,14 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
'--color-button-border',
|
'--color-button-border',
|
||||||
isDarkMode ? '#424242' : '#d9d9d9'
|
isDarkMode ? '#424242' : '#d9d9d9'
|
||||||
)
|
)
|
||||||
|
root.style.setProperty(
|
||||||
|
'--color-table-header-bg',
|
||||||
|
isDarkMode ? '#1d1d1d' : '#fafafa'
|
||||||
|
)
|
||||||
|
root.style.setProperty(
|
||||||
|
'--color-table-row-border',
|
||||||
|
isDarkMode ? '#303030' : '#f0f0f0'
|
||||||
|
)
|
||||||
}, [isDarkMode, primaryColorOverride])
|
}, [isDarkMode, primaryColorOverride])
|
||||||
|
|
||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
|
|||||||
@ -96,6 +96,202 @@ export function timeStringToMinutes(timeString) {
|
|||||||
return Math.floor(totalMinutes)
|
return Math.floor(totalMinutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStateTagInfo(state) {
|
||||||
|
let status = 'default'
|
||||||
|
let text = 'Unknown'
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'online':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Online'
|
||||||
|
break
|
||||||
|
case 'standby':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Standby'
|
||||||
|
break
|
||||||
|
case 'complete':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Complete'
|
||||||
|
break
|
||||||
|
case 'offline':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Offline'
|
||||||
|
break
|
||||||
|
case 'shutdown':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Shutdown'
|
||||||
|
break
|
||||||
|
case 'initializing':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Initializing'
|
||||||
|
break
|
||||||
|
case 'connecting':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Connecting'
|
||||||
|
break
|
||||||
|
case 'deploying':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Deploying'
|
||||||
|
break
|
||||||
|
case 'printing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Printing'
|
||||||
|
break
|
||||||
|
case 'paused':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Paused'
|
||||||
|
break
|
||||||
|
case 'cancelled':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Cancelled'
|
||||||
|
break
|
||||||
|
case 'loading':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Uploading'
|
||||||
|
break
|
||||||
|
case 'processing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Processing'
|
||||||
|
break
|
||||||
|
case 'ready':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Ready'
|
||||||
|
break
|
||||||
|
case 'new':
|
||||||
|
status = 'success'
|
||||||
|
text = 'New'
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Error'
|
||||||
|
break
|
||||||
|
case 'startup':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Startup'
|
||||||
|
break
|
||||||
|
case 'draft':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Draft'
|
||||||
|
break
|
||||||
|
case 'active':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Active'
|
||||||
|
break
|
||||||
|
case 'inactive':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Inactive'
|
||||||
|
break
|
||||||
|
case 'deleted':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Deleted'
|
||||||
|
break
|
||||||
|
case 'suspended':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Suspended'
|
||||||
|
break
|
||||||
|
case 'syncing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Syncing'
|
||||||
|
break
|
||||||
|
case 'publishing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Publishing'
|
||||||
|
break
|
||||||
|
case 'unpublishing':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Unpublishing'
|
||||||
|
break
|
||||||
|
case 'disconnected':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Disconnected'
|
||||||
|
break
|
||||||
|
case 'failed':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Failed'
|
||||||
|
break
|
||||||
|
case 'queued':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Queued'
|
||||||
|
break
|
||||||
|
case 'pending':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Pending'
|
||||||
|
break
|
||||||
|
case 'used':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Used'
|
||||||
|
break
|
||||||
|
case 'consumed':
|
||||||
|
status = 'default'
|
||||||
|
text = 'Consumed'
|
||||||
|
break
|
||||||
|
case 'unconsumed':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Unconsumed'
|
||||||
|
break
|
||||||
|
case 'sent':
|
||||||
|
status = 'cyan'
|
||||||
|
text = 'Sent'
|
||||||
|
break
|
||||||
|
case 'acknowledged':
|
||||||
|
status = 'purple'
|
||||||
|
text = 'Acknowledged'
|
||||||
|
break
|
||||||
|
case 'confirmed':
|
||||||
|
status = 'purple'
|
||||||
|
text = 'Confirmed'
|
||||||
|
break
|
||||||
|
case 'ordered':
|
||||||
|
status = 'cyan'
|
||||||
|
text = 'Ordered'
|
||||||
|
break
|
||||||
|
case 'posted':
|
||||||
|
status = 'magenta'
|
||||||
|
text = 'Posted'
|
||||||
|
break
|
||||||
|
case 'authorised':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Authorised'
|
||||||
|
break
|
||||||
|
case 'declined':
|
||||||
|
status = 'error'
|
||||||
|
text = 'Declined'
|
||||||
|
break
|
||||||
|
case 'received':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Received'
|
||||||
|
break
|
||||||
|
case 'invoiced':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Invoiced'
|
||||||
|
break
|
||||||
|
case 'planned':
|
||||||
|
status = 'warning'
|
||||||
|
text = 'Planned'
|
||||||
|
break
|
||||||
|
case 'partiallyShipped':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Partially Shipped'
|
||||||
|
break
|
||||||
|
case 'shipped':
|
||||||
|
status = 'processing'
|
||||||
|
text = 'Shipped'
|
||||||
|
break
|
||||||
|
case 'delivered':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Delivered'
|
||||||
|
break
|
||||||
|
case 'paid':
|
||||||
|
status = 'success'
|
||||||
|
text = 'Paid'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
status = 'default'
|
||||||
|
text = state || 'Unknown'
|
||||||
|
}
|
||||||
|
return { status, text }
|
||||||
|
}
|
||||||
|
|
||||||
export function round(num, decimals) {
|
export function round(num, decimals) {
|
||||||
return Math.round(num * 10 ** decimals) / 10 ** decimals
|
return Math.round(num * 10 ** decimals) / 10 ** decimals
|
||||||
}
|
}
|
||||||
@ -177,7 +373,10 @@ const stripProperties = (data, properties) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(property.properties) || property.properties.length === 0) {
|
if (
|
||||||
|
!Array.isArray(property.properties) ||
|
||||||
|
property.properties.length === 0
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +412,10 @@ const collectModelPropertyPaths = (modelDefinition) => {
|
|||||||
if (property?.name) {
|
if (property?.name) {
|
||||||
paths.push(property.name)
|
paths.push(property.name)
|
||||||
}
|
}
|
||||||
if (Array.isArray(property?.properties) && property.properties.length > 0) {
|
if (
|
||||||
|
Array.isArray(property?.properties) &&
|
||||||
|
property.properties.length > 0
|
||||||
|
) {
|
||||||
visit(property.properties)
|
visit(property.properties)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -311,10 +513,7 @@ export function calculateModelComputedEntries(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (Array.isArray(property.properties) && property.properties.length > 0) {
|
||||||
Array.isArray(property.properties) &&
|
|
||||||
property.properties.length > 0
|
|
||||||
) {
|
|
||||||
if (property.type === 'objectChildren') {
|
if (property.type === 'objectChildren') {
|
||||||
const childValues = getValueAtPath(workingData, propertyPath)
|
const childValues = getValueAtPath(workingData, propertyPath)
|
||||||
if (Array.isArray(childValues)) {
|
if (Array.isArray(childValues)) {
|
||||||
|
|||||||
6
src/components/Icons/TimelineIcon.jsx
Normal file
6
src/components/Icons/TimelineIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import CustomIconSvg from '../../../assets/icons/timelineicon.svg?react'
|
||||||
|
|
||||||
|
const TimelineIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default TimelineIcon
|
||||||
Loading…
x
Reference in New Issue
Block a user