Compare commits
2 Commits
6a09f6a6a2
...
0823947e33
| Author | SHA1 | Date | |
|---|---|---|---|
| 0823947e33 | |||
| b5abc4e553 |
@@ -19,7 +19,6 @@ type defaultBoardFieldValue struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultBoardFieldValues = []defaultBoardFieldValue{
|
var defaultBoardFieldValues = []defaultBoardFieldValue{
|
||||||
{Field: "status", Value: "", Label: "Backlog", Color: "#9e9e9e"},
|
|
||||||
{Field: "status", Value: "todo", Label: "To Do", Color: "#1976d2"},
|
{Field: "status", Value: "todo", Label: "To Do", Color: "#1976d2"},
|
||||||
{Field: "status", Value: "in_progress", Label: "In Progress", Color: "#f59e0b"},
|
{Field: "status", Value: "in_progress", Label: "In Progress", Color: "#f59e0b"},
|
||||||
{Field: "status", Value: "done", Label: "Done", Color: "#16a34a"},
|
{Field: "status", Value: "done", Label: "Done", Color: "#16a34a"},
|
||||||
|
|||||||
@@ -20,13 +20,11 @@ CREATE INDEX idx_board_field_values_board_field ON board_field_values(board_id,
|
|||||||
|
|
||||||
-- Seed default status values for every existing board
|
-- Seed default status values for every existing board
|
||||||
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
||||||
SELECT lower(hex(randomblob(8))), b.id, 'status', '', 'Backlog', '#9e9e9e', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
SELECT lower(hex(randomblob(8))), b.id, 'status', 'todo', 'To Do', '#1976d2', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
||||||
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
||||||
SELECT lower(hex(randomblob(8))), b.id, 'status', 'todo', 'To Do', '#1976d2', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
SELECT lower(hex(randomblob(8))), b.id, 'status', 'in_progress','In Progress', '#f59e0b', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
||||||
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
||||||
SELECT lower(hex(randomblob(8))), b.id, 'status', 'in_progress','In Progress', '#f59e0b', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
SELECT lower(hex(randomblob(8))), b.id, 'status', 'done', 'Done', '#16a34a', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
||||||
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
|
||||||
SELECT lower(hex(randomblob(8))), b.id, 'status', 'done', 'Done', '#16a34a', 3, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
|
|
||||||
|
|
||||||
-- Seed default type values
|
-- Seed default type values
|
||||||
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import FormDialogContent from './FormDialogContent'
|
|||||||
import ModalDialog from './ModalDialog'
|
import ModalDialog from './ModalDialog'
|
||||||
|
|
||||||
export const CARD_STATUSES = [
|
export const CARD_STATUSES = [
|
||||||
{ value: '', label: 'Backlog' },
|
{ value: '', label: 'Unassigned' },
|
||||||
{ value: 'todo', label: 'To Do' },
|
{ value: 'todo', label: 'To Do' },
|
||||||
{ value: 'in_progress', label: 'In Progress' },
|
{ value: 'in_progress', label: 'In Progress' },
|
||||||
{ value: 'done', label: 'Done' },
|
{ value: 'done', label: 'Done' },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from '@dnd-kit/sortable'
|
} from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { Box, Button, Chip, Paper, Typography } from '@mui/material'
|
import { Box, Button, Chip, Paper, Typography } from '@mui/material'
|
||||||
|
import { Theme } from '@mui/material/styles'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { api, Block, BlockProperties, BlockPropertiesPayload, BoardFieldValue, GroupBy } from '../api'
|
import { api, Block, BlockProperties, BlockPropertiesPayload, BoardFieldValue, GroupBy } from '../api'
|
||||||
|
|
||||||
@@ -297,10 +298,18 @@ function KanbanColumn({
|
|||||||
style={{ transform: CSS.Transform.toString(transform), transition }}
|
style={{ transform: CSS.Transform.toString(transform), transition }}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 280, flex: '0 0 280px', display: 'flex', flexDirection: 'column', gap: 1,
|
minWidth: 280, flex: '0 0 280px', display: 'flex', flexDirection: 'column', gap: 1,
|
||||||
p: 0.5, borderRadius: 1,
|
p: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: showCardDropHighlight ? 'primary.main' : 'divider',
|
||||||
opacity: isDragging ? 0 : 1,
|
opacity: isDragging ? 0 : 1,
|
||||||
bgcolor: showCardDropHighlight ? 'action.hover' : 'transparent',
|
bgcolor: showCardDropHighlight
|
||||||
transition: 'background-color 0.15s',
|
? 'action.hover'
|
||||||
|
: (theme: Theme) => theme.palette.mode === 'dark'
|
||||||
|
? 'rgba(255,255,255,0.045)'
|
||||||
|
: 'rgba(15,23,42,0.065)',
|
||||||
|
boxShadow: showCardDropHighlight ? 2 : 0,
|
||||||
|
transition: 'background-color 0.15s, border-color 0.15s, box-shadow 0.15s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Column header — drag handle for column reorder */}
|
{/* Column header — drag handle for column reorder */}
|
||||||
@@ -640,7 +649,7 @@ export default function BoardKanbanView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel}>
|
<DndContext sensors={sensors} onDragStart={handleDragStart} onDragOver={handleDragOver} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel}>
|
||||||
<Box sx={{ overflowX: 'auto', pb: 2, display: 'flex' }}>
|
<Box sx={{ overflowX: 'auto', display: 'flex', flex: 1, minHeight: 0 }}>
|
||||||
<SortableContext items={colSortableIds} strategy={horizontalListSortingStrategy}>
|
<SortableContext items={colSortableIds} strategy={horizontalListSortingStrategy}>
|
||||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start', mx: 'auto' }}>
|
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start', mx: 'auto' }}>
|
||||||
{orderedColumns.map((col) => {
|
{orderedColumns.map((col) => {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function getGroupKey(p: BlockProperties, groupBy: GroupBy): string {
|
|||||||
function getGroupLabel(key: string, groupBy: GroupBy, valueByKey: Record<string, BoardFieldValue>): string {
|
function getGroupLabel(key: string, groupBy: GroupBy, valueByKey: Record<string, BoardFieldValue>): string {
|
||||||
if (!key) {
|
if (!key) {
|
||||||
switch (groupBy) {
|
switch (groupBy) {
|
||||||
case 'status': return 'Backlog'
|
case 'status': return 'Unassigned'
|
||||||
case 'sprint': return 'No Sprint'
|
case 'sprint': return 'No Sprint'
|
||||||
default: return 'None'
|
default: return 'None'
|
||||||
}
|
}
|
||||||
@@ -121,9 +121,9 @@ function buildColumns(
|
|||||||
label: 'Status',
|
label: 'Status',
|
||||||
defaultWidth: 120,
|
defaultWidth: 120,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
getValue: (r) => resolveLabel(r.props.status, statusValues, 'Backlog'),
|
getValue: (r) => resolveLabel(r.props.status, statusValues, 'Unassigned'),
|
||||||
renderCell: (r) => {
|
renderCell: (r) => {
|
||||||
const label = resolveLabel(r.props.status, statusValues, 'Backlog')
|
const label = resolveLabel(r.props.status, statusValues, 'Unassigned')
|
||||||
const color = resolveColor(r.props.status, statusValues)
|
const color = resolveColor(r.props.status, statusValues)
|
||||||
return <Chip label={label} size="small" sx={color ? { bgcolor: color, color: '#fff' } : undefined} />
|
return <Chip label={label} size="small" sx={color ? { bgcolor: color, color: '#fff' } : undefined} />
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ export default function CardDetailDialog(props: CardDetailDialogProps) {
|
|||||||
const sprintValues = fieldValues['sprint'] ?? []
|
const sprintValues = fieldValues['sprint'] ?? []
|
||||||
|
|
||||||
const typeOptions = fieldOptions(typeValues, 'None')
|
const typeOptions = fieldOptions(typeValues, 'None')
|
||||||
const statusOptions = fieldOptions(statusValues, 'Backlog')
|
const statusOptions = fieldOptions(statusValues, 'Unassigned')
|
||||||
const priorityOptions = fieldOptions(priorityValues, 'None')
|
const priorityOptions = fieldOptions(priorityValues, 'None')
|
||||||
|
|
||||||
const currentTypeColor = typeValues.find((v) => v.value === props_.card_type)?.color
|
const currentTypeColor = typeValues.find((v) => v.value === props_.card_type)?.color
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Box } from '@mui/material'
|
import { Box } from '@mui/material'
|
||||||
|
import { SxProps, Theme } from '@mui/material/styles'
|
||||||
import { ReactNode, useEffect, useState } from 'react'
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
import { api, Project } from '../api'
|
import { api, Project } from '../api'
|
||||||
import ContextToolbar from './ContextToolbar'
|
import ContextToolbar from './ContextToolbar'
|
||||||
@@ -17,14 +18,23 @@ type ProjectPageFrameProps = {
|
|||||||
showContext?: boolean
|
showContext?: boolean
|
||||||
contextToolbar?: ReactNode
|
contextToolbar?: ReactNode
|
||||||
repoSubNav?: ReactNode
|
repoSubNav?: ReactNode
|
||||||
|
fillHeight?: boolean
|
||||||
|
contentSx?: SxProps<Theme>
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProjectPageFrame(props: ProjectPageFrameProps) {
|
export default function ProjectPageFrame(props: ProjectPageFrameProps) {
|
||||||
const { projectId, project, parentItems, currentLabel, showContext, contextToolbar, repoSubNav, children } = props
|
const { projectId, project, parentItems, currentLabel, showContext, contextToolbar, repoSubNav, fillHeight, contentSx, children } = props
|
||||||
const [loadedProject, setLoadedProject] = useState<Project | null>(null)
|
const [loadedProject, setLoadedProject] = useState<Project | null>(null)
|
||||||
const projectItem: Project | null = project === undefined ? loadedProject : project
|
const projectItem: Project | null = project === undefined ? loadedProject : project
|
||||||
const items: ProjectPageFrameItem[] = parentItems ?? []
|
const items: ProjectPageFrameItem[] = parentItems ?? []
|
||||||
|
const rootSx: SxProps<Theme> = fillHeight ? {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: 'calc(100dvh - 76px)',
|
||||||
|
minHeight: 0,
|
||||||
|
minWidth: 0,
|
||||||
|
} : {}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (project !== undefined || !projectId) return
|
if (project !== undefined || !projectId) return
|
||||||
@@ -35,12 +45,14 @@ export default function ProjectPageFrame(props: ProjectPageFrameProps) {
|
|||||||
}, [project, projectId])
|
}, [project, projectId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box sx={rootSx}>
|
||||||
<ProjectNavBar projectId={projectId} projectName={projectItem?.name} sx={{ mb: 0.75 }} />
|
<ProjectNavBar projectId={projectId} projectName={projectItem?.name} sx={{ mb: 0.75 }} />
|
||||||
{contextToolbar ?? (showContext !== false ? (
|
{contextToolbar ?? (showContext !== false ? (
|
||||||
<ContextToolbar items={items} label={currentLabel} nav={repoSubNav} />
|
<ContextToolbar items={items} label={currentLabel} nav={repoSubNav} />
|
||||||
) : repoSubNav)}
|
) : repoSubNav)}
|
||||||
|
<Box sx={contentSx}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ViewKanbanIcon from '@mui/icons-material/ViewKanban'
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Link,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
ToggleButtonGroup,
|
ToggleButtonGroup,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { MouseEvent, useEffect, useState } from 'react'
|
import { MouseEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { api, Block, BlockProperties, BlockPropertiesPayload, Board, BoardFieldValue, GroupBy } from '../api'
|
import { api, Block, BlockProperties, BlockPropertiesPayload, Board, BoardFieldValue, GroupBy } from '../api'
|
||||||
import BoardFormDialog from '../components/BoardFormDialog'
|
import BoardFormDialog from '../components/BoardFormDialog'
|
||||||
@@ -40,13 +41,8 @@ const FIELD_LABELS: Record<GroupBy, string> = {
|
|||||||
|
|
||||||
const FALLBACK_DOT = '#9e9e9e'
|
const FALLBACK_DOT = '#9e9e9e'
|
||||||
|
|
||||||
function fieldValuesToCols(values: BoardFieldValue[], noneLabel: string): ColDef[] {
|
function fieldValuesToCols(values: BoardFieldValue[]): ColDef[] {
|
||||||
const cols = values.map((v) => ({ value: v.value, label: v.label, dotColor: v.color || FALLBACK_DOT }))
|
return values.map((v: BoardFieldValue) => ({ value: v.value, label: v.label, dotColor: v.color || FALLBACK_DOT }))
|
||||||
// Only add the empty-string bucket if no defined value already covers it (status seeds '' as Backlog)
|
|
||||||
if (!values.some((v) => v.value === '')) {
|
|
||||||
cols.push({ value: '', label: noneLabel, dotColor: FALLBACK_DOT })
|
|
||||||
}
|
|
||||||
return cols
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardGroupValue(block: Block, groupBy: GroupBy, p: BlockProperties | undefined): string {
|
function getCardGroupValue(block: Block, groupBy: GroupBy, p: BlockProperties | undefined): string {
|
||||||
@@ -68,6 +64,32 @@ const GROUP_BY_OPTIONS: { value: GroupBy; label: string }[] = [
|
|||||||
{ value: 'priority', label: 'Priority' },
|
{ value: 'priority', label: 'Priority' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
type StandardFieldValueSeed = {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const STANDARD_FIELD_VALUES: Partial<Record<GroupBy, StandardFieldValueSeed[]>> = {
|
||||||
|
status: [
|
||||||
|
{ value: 'todo', label: 'To Do', color: '#1976d2' },
|
||||||
|
{ value: 'in_progress', label: 'In Progress', color: '#f59e0b' },
|
||||||
|
{ value: 'done', label: 'Done', color: '#16a34a' },
|
||||||
|
],
|
||||||
|
type: [
|
||||||
|
{ value: 'task', label: 'Task', color: '#1976d2' },
|
||||||
|
{ value: 'bug', label: 'Bug', color: '#d32f2f' },
|
||||||
|
{ value: 'story', label: 'Story', color: '#388e3c' },
|
||||||
|
{ value: 'epic', label: 'Epic', color: '#7b1fa2' },
|
||||||
|
],
|
||||||
|
priority: [
|
||||||
|
{ value: 'low', label: 'Low', color: '#4caf50' },
|
||||||
|
{ value: 'medium', label: 'Medium', color: '#2196f3' },
|
||||||
|
{ value: 'high', label: 'High', color: '#ff9800' },
|
||||||
|
{ value: 'urgent', label: 'Urgent', color: '#f44336' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
// All field values keyed by field name
|
// All field values keyed by field name
|
||||||
type FieldValuesMap = Record<string, BoardFieldValue[]>
|
type FieldValuesMap = Record<string, BoardFieldValue[]>
|
||||||
|
|
||||||
@@ -95,6 +117,8 @@ export default function BoardPage() {
|
|||||||
const [fieldManagerField, setFieldManagerField] = useState<GroupBy | null>(null)
|
const [fieldManagerField, setFieldManagerField] = useState<GroupBy | null>(null)
|
||||||
const [fieldsMenuAnchor, setFieldsMenuAnchor] = useState<HTMLElement | null>(null)
|
const [fieldsMenuAnchor, setFieldsMenuAnchor] = useState<HTMLElement | null>(null)
|
||||||
const [boardActionsAnchor, setBoardActionsAnchor] = useState<HTMLElement | null>(null)
|
const [boardActionsAnchor, setBoardActionsAnchor] = useState<HTMLElement | null>(null)
|
||||||
|
const [creatingStandardFieldValues, setCreatingStandardFieldValues] = useState(false)
|
||||||
|
const creatingStandardFieldValuesRef = useRef(false)
|
||||||
|
|
||||||
const [selectedCard, setSelectedCard] = useState<Block | null>(null)
|
const [selectedCard, setSelectedCard] = useState<Block | null>(null)
|
||||||
|
|
||||||
@@ -195,6 +219,57 @@ export default function BoardPage() {
|
|||||||
cancelAddCard()
|
cancelAddCard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCreateStandardFieldValues() {
|
||||||
|
const seeds: StandardFieldValueSeed[] = STANDARD_FIELD_VALUES[groupBy] ?? []
|
||||||
|
const currentBoardId: string | undefined = boardId
|
||||||
|
const currentGroupBy: GroupBy = groupBy
|
||||||
|
let existingValues: BoardFieldValue[]
|
||||||
|
let createdValues: BoardFieldValue[]
|
||||||
|
let finalValues: BoardFieldValue[]
|
||||||
|
let seed: StandardFieldValueSeed
|
||||||
|
|
||||||
|
if (creatingStandardFieldValuesRef.current) return
|
||||||
|
if (!currentBoardId || seeds.length === 0) return
|
||||||
|
creatingStandardFieldValuesRef.current = true
|
||||||
|
setCreatingStandardFieldValues(true)
|
||||||
|
setLoadError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
existingValues = await api.listBoardFieldValues(currentBoardId, currentGroupBy)
|
||||||
|
if ((existingValues || []).length > 0) {
|
||||||
|
setFieldValues((prev: FieldValuesMap) => ({ ...prev, [currentGroupBy]: existingValues || [] }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
createdValues = []
|
||||||
|
for (seed of seeds) {
|
||||||
|
createdValues.push(await api.createBoardFieldValue(currentBoardId, currentGroupBy, {
|
||||||
|
value: seed.value,
|
||||||
|
label: seed.label,
|
||||||
|
color: seed.color,
|
||||||
|
start_date: '',
|
||||||
|
end_date: '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
finalValues = await api.listBoardFieldValues(currentBoardId, currentGroupBy)
|
||||||
|
setFieldValues((prev: FieldValuesMap) => ({ ...prev, [currentGroupBy]: finalValues || createdValues }))
|
||||||
|
if ((finalValues || createdValues).length > 0) setLoadError(null)
|
||||||
|
} catch (err) {
|
||||||
|
setLoadError(err instanceof Error ? err.message : String(err))
|
||||||
|
try {
|
||||||
|
finalValues = await api.listBoardFieldValues(currentBoardId, currentGroupBy)
|
||||||
|
setFieldValues((prev: FieldValuesMap) => ({ ...prev, [currentGroupBy]: finalValues || [] }))
|
||||||
|
if ((finalValues || []).length > 0) setLoadError(null)
|
||||||
|
} catch (reloadErr) {
|
||||||
|
setLoadError(reloadErr instanceof Error ? reloadErr.message : String(reloadErr))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
creatingStandardFieldValuesRef.current = false
|
||||||
|
setCreatingStandardFieldValues(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const propsByBlockId = allProperties.reduce<Record<string, BlockProperties>>((acc, p) => {
|
const propsByBlockId = allProperties.reduce<Record<string, BlockProperties>>((acc, p) => {
|
||||||
acc[p.block_id] = p
|
acc[p.block_id] = p
|
||||||
return acc
|
return acc
|
||||||
@@ -202,14 +277,7 @@ export default function BoardPage() {
|
|||||||
|
|
||||||
const activeFieldValues = fieldValues[groupBy] ?? []
|
const activeFieldValues = fieldValues[groupBy] ?? []
|
||||||
|
|
||||||
// Columns: defined values + a trailing "None" bucket for unassigned cards
|
const columns: ColDef[] = fieldValuesToCols(activeFieldValues)
|
||||||
const noneLabelByField: Record<GroupBy, string> = {
|
|
||||||
status: 'Backlog',
|
|
||||||
type: 'No Type',
|
|
||||||
priority: 'No Priority',
|
|
||||||
sprint: 'No Sprint',
|
|
||||||
}
|
|
||||||
const columns: ColDef[] = fieldValuesToCols(activeFieldValues, noneLabelByField[groupBy])
|
|
||||||
|
|
||||||
const blocksByGroup = columns.reduce<Record<string, Block[]>>((acc, col) => {
|
const blocksByGroup = columns.reduce<Record<string, Block[]>>((acc, col) => {
|
||||||
acc[col.value] = blocks.filter((b) => getCardGroupValue(b, groupBy, propsByBlockId[b.id]) === col.value)
|
acc[col.value] = blocks.filter((b) => getCardGroupValue(b, groupBy, propsByBlockId[b.id]) === col.value)
|
||||||
@@ -223,6 +291,8 @@ export default function BoardPage() {
|
|||||||
projectId={projectId ?? ''}
|
projectId={projectId ?? ''}
|
||||||
parentItems={[{ label: 'Boards', to: `/projects/${projectId}/boards` }]}
|
parentItems={[{ label: 'Boards', to: `/projects/${projectId}/boards` }]}
|
||||||
currentLabel={board?.title ?? 'Board'}
|
currentLabel={board?.title ?? 'Board'}
|
||||||
|
fillHeight
|
||||||
|
contentSx={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}
|
||||||
contextToolbar={
|
contextToolbar={
|
||||||
<ContextToolbar
|
<ContextToolbar
|
||||||
kind="Board"
|
kind="Board"
|
||||||
@@ -320,6 +390,34 @@ export default function BoardPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PageAlert message={loadError} onClose={() => setLoadError(null)} />
|
<PageAlert message={loadError} onClose={() => setLoadError(null)} />
|
||||||
|
{view === 'board' && columns.length === 0 ? (
|
||||||
|
<PageAlert severity="warning">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
No {FIELD_LABELS[groupBy].toLowerCase()} values are defined for this board. Create columns first.
|
||||||
|
</Typography>
|
||||||
|
{(STANDARD_FIELD_VALUES[groupBy] ?? []).length > 0 ? (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
variant="body2"
|
||||||
|
underline="hover"
|
||||||
|
onClick={() => {
|
||||||
|
handleCreateStandardFieldValues()
|
||||||
|
}}
|
||||||
|
aria-disabled={creatingStandardFieldValues}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
cursor: creatingStandardFieldValues ? 'default' : 'pointer',
|
||||||
|
pointerEvents: creatingStandardFieldValues ? 'none' : 'auto',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{creatingStandardFieldValues ? 'Creating standard values...' : 'Create standard values'}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</PageAlert>
|
||||||
|
) : null}
|
||||||
{board?.description ? (
|
{board?.description ? (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
{board.description}
|
{board.description}
|
||||||
@@ -349,6 +447,7 @@ export default function BoardPage() {
|
|||||||
|
|
||||||
{/* Kanban columns */}
|
{/* Kanban columns */}
|
||||||
{view === 'board' ? (
|
{view === 'board' ? (
|
||||||
|
<Box sx={{ display: 'flex', flex: 1, minHeight: 0 }}>
|
||||||
<BoardKanbanView
|
<BoardKanbanView
|
||||||
boardId={boardId ?? ''}
|
boardId={boardId ?? ''}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -367,6 +466,7 @@ export default function BoardPage() {
|
|||||||
onBlocksChanged={() => { loadBlocks(); loadAllProperties() }}
|
onBlocksChanged={() => { loadBlocks(); loadAllProperties() }}
|
||||||
onColumnsReordered={() => loadFieldValues(groupBy)}
|
onColumnsReordered={() => loadFieldValues(groupBy)}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Board edit dialog */}
|
{/* Board edit dialog */}
|
||||||
|
|||||||
Reference in New Issue
Block a user