diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index f358d943..0e9b63c9 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -1693,6 +1693,105 @@ body.objectKanbanColumnResizing * {
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-mask,
.electron-body .ant-drawer {
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 1c1f0824..47dc2eee 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -35,6 +35,7 @@ import {
import ObjectProperty from './ObjectProperty'
import ObjectCard from './ObjectCard'
import ObjectKanban from './ObjectKanban'
+import ObjectTimeline from './ObjectTimeline'
import FilterSidebar from './FilterSidebar'
import SortSidebar from './SortSidebar'
import CheckIcon from '../../Icons/CheckIcon'
@@ -59,7 +60,12 @@ import { hasActionPermission } from '../../../database/permissions'
import Tooltip from './Tooltip'
import { ObjectTableFilterContext } from './ObjectTableFilterContext'
import ObjectListViewContext from '../context/ObjectListViewContext'
-import { isCardsView, isKanbanView, normalizeViewMode } from './viewModeUtils'
+import {
+ isCardsView,
+ isKanbanView,
+ isTimelineView,
+ normalizeViewMode
+} from './viewModeUtils'
import { areValuesEqual } from '../utils/Utils'
import MissingPlaceholder from './MissingPlaceholder'
import LoadingPlaceholder from './LoadingPlaceholder'
@@ -319,6 +325,7 @@ const ObjectTable = forwardRef(
)
const isCards = isCardsView(viewMode)
const isKanban = isKanbanView(viewMode)
+ const isTimeline = isTimelineView(viewMode)
const kanbanRef = useRef(null)
const prevViewModeKeyRef = useRef(null)
const { token, userProfile } = useContext(AuthContext)
@@ -379,7 +386,7 @@ const ObjectTable = forwardRef(
if (isMobile) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 298px)'
}
- if (isCards || isKanban) {
+ if (isCards || isKanban || isTimeline) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 210px)'
}
if (isElectron) {
@@ -388,7 +395,7 @@ const ObjectTable = forwardRef(
if (isMobile && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 282px)'
}
- if ((isCards || isKanban) && isElectron) {
+ if ((isCards || isKanban || isTimeline) && isElectron) {
adjustedScrollHeight = 'calc(var(--unit-100vh) - 258px)'
}
const tableRef = useRef(null)
@@ -1514,7 +1521,13 @@ const ObjectTable = forwardRef(
}, [token, initialized, connected])
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) {
prevViewModeKeyRef.current = viewModeKey
@@ -1525,6 +1538,7 @@ const ObjectTable = forwardRef(
prevViewModeKeyRef.current = viewModeKey
if (isKanban && !viewMode?.settings?.categoryProperty) return
+ if (isTimeline && !viewMode?.settings?.startDate) return
loadPage(
initialPage,
@@ -1533,13 +1547,17 @@ const ObjectTable = forwardRef(
)
}, [
isKanban,
+ isTimeline,
connected,
token,
initialized,
loadPage,
initialPage,
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
@@ -2135,6 +2153,38 @@ const ObjectTable = forwardRef(
/>
+ ) : isTimeline ? (
+
+
+ (
+
+ {row}
+
+ )}
+ />
+
+
) : isCards ? (
model?.properties?.filter(isTimelineDateProperty) || [],
+ [model]
+ )
+
+ const colorProperties = useMemo(
+ () => model?.properties?.filter(isTimelineColorProperty) || [],
+ [model]
+ )
const hasKanbanOption = categoryProperties.length > 0
+ const hasTimelineOption = dateProperties.length > 0
const defaultCategoryProperty =
categoryProperties.find((property) => property.type === 'state')?.name ||
categoryProperties[0]?.name
+ const defaultStartDate = dateProperties[0]?.name
+ const defaultColorProperty =
+ colorProperties[0]?.color || colorProperties[0]?.state
const availableViewModes = useMemo(
() =>
VIEW_MODE_OPTIONS.filter(
- (option) => option.type !== 'kanban' || hasKanbanOption
+ (option) =>
+ (option.type !== 'kanban' || hasKanbanOption) &&
+ (option.type !== 'timeline' || hasTimelineOption)
).map((option) => option.type),
- [hasKanbanOption]
+ [hasKanbanOption, hasTimelineOption]
)
const handleTypeChange = (nextType) => {
@@ -87,6 +106,23 @@ const ObjectTableViewButton = ({
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 })
}
@@ -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 handleCycleView = (event) => {
@@ -115,6 +161,8 @@ const ObjectTableViewButton = ({
) : normalizedViewMode.type === 'kanban' ? (
+ ) : normalizedViewMode.type === 'timeline' ? (
+
) : (
)
@@ -130,7 +178,9 @@ const ObjectTableViewButton = ({
>
{VIEW_MODE_OPTIONS.filter(
- (option) => option.type !== 'kanban' || hasKanbanOption
+ (option) =>
+ (option.type !== 'kanban' || hasKanbanOption) &&
+ (option.type !== 'timeline' || hasTimelineOption)
).map((option) => (
{option.label}
- {option.type === 'kanban' && (
+ {(option.type === 'kanban' ||
+ option.type === 'timeline') && (
}
- disabled={normalizedViewMode.type !== 'kanban'}
+ disabled={normalizedViewMode.type !== option.type}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
@@ -185,27 +236,86 @@ const ObjectTableViewButton = ({
getContainer={() => document.body}
width={520}
>
- event.stopPropagation()}>
+ event.stopPropagation()}
+ >
- Kanban settings
+
+ {normalizedViewMode.type === 'timeline'
+ ? 'Timeline settings'
+ : 'Kanban settings'}
+
-
- Select the property used to group cards into columns:
-
-