export const DEFAULT_VIEW_MODE = { type: 'list' } /** Property types that can be used as kanban column categories. */ 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_MIN_COLUMN_WIDTH = 200 export const isKanbanCategoryProperty = (property) => 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) => { if (!value) return DEFAULT_VIEW_MODE if (typeof value === 'string') { return { type: ['cards', 'kanban', 'timeline'].includes(value) ? value : 'list' } } if (value?.type) return value return DEFAULT_VIEW_MODE } export const isCardsView = (vm) => normalizeViewMode(vm).type === 'cards' 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 toCategoryFilterValue = (value) => { if (Array.isArray(value)) { return value.map(toCategoryFilterValue) } if (value && typeof value === 'object') { return value.type ?? value._id ?? JSON.stringify(value) } return String(value) } export const getCategoryValueKey = (value) => { if ( value && typeof value === 'object' && !Array.isArray(value) && value.type != null ) { return String(value.type) } if (value != null) return String(value) return 'null' } /** * Column keys for a category property value. Multi-value properties (e.g. tags) * can map an item to multiple columns. */ export const getCategoryColumnKeys = (value) => { if (value === undefined) return [] if (Array.isArray(value)) { return [ ...new Set( value .filter((entry) => entry != null && entry !== '') .map(getCategoryValueKey) ) ] } return [getCategoryValueKey(value)] } /** * Merge fetched category values with persisted kanban column order/widths. * Unknown stored columns are dropped; new values are appended with the default width. */ export const resolveKanbanColumns = ( categoryValues = [], storedColumns = [] ) => { const byKey = new Map( categoryValues.map((value) => [getCategoryValueKey(value), value]) ) const seen = new Set() const ordered = [] for (const column of storedColumns || []) { if (column == null || column.value == null) continue const key = String(column.value) if (!byKey.has(key) || seen.has(key)) continue const width = typeof column.width === 'number' && column.width > 0 ? column.width : KANBAN_DEFAULT_COLUMN_WIDTH ordered.push({ key, value: byKey.get(key), width: Math.max(KANBAN_MIN_COLUMN_WIDTH, width) }) seen.add(key) } for (const value of categoryValues) { const key = getCategoryValueKey(value) if (seen.has(key)) continue ordered.push({ key, value, width: KANBAN_DEFAULT_COLUMN_WIDTH }) seen.add(key) } return ordered } export const toKanbanColumnsConfig = (columns) => (columns || []).map(({ key, width }) => ({ value: key, width: typeof width === 'number' && width > 0 ? Math.max(KANBAN_MIN_COLUMN_WIDTH, width) : KANBAN_DEFAULT_COLUMN_WIDTH }))