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') && (