Files
supabase/apps/studio/components/interfaces/ProjectHome/Home.tsx
Joshen Lim b648db233b joshen/fe 4113 explorer markdown cells (#48846)
## Context

More groundwork for the Explorer - this PR introduces the Markdown cells
and some basic (non data persisting) editing

Markdown cells will save either on
- Save button click
- Losing focus on the code editor

Hitting esc will cancel the changes

<img width="1387" height="674" alt="image"
src="https://github.com/user-attachments/assets/f0614b37-7a11-404f-9940-8bcd4de25c57"
/>

<img width="1087" height="516" alt="image"
src="https://github.com/user-attachments/assets/0a104168-2001-4ef7-934a-7e864956b3cb"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added editable Markdown cells to notebooks, with save and cancel
controls.
* Added drag-and-drop reordering for notebook cells, including keyboard
support.
* New notebooks now include sample Markdown content to help users get
started.
* **Improvements**
* Improved drag-handle placement and consistency across sortable
sections.
* Updated notebook empty and populated states to reflect the current
cell content.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-11 10:41:59 +07:00

175 lines
6.6 KiB
TypeScript

import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { useFlag, useParams } from 'common'
import dayjs from 'dayjs'
import { useEffect, useRef } from 'react'
import { cn } from 'ui'
import { AdvisorSection } from './AdvisorSection'
import { ConnectSection } from './ConnectSection'
import { CustomReportSection } from './CustomReportSection'
import { DEFAULT_SECTION_ORDER, mergeSectionOrder } from './Home.utils'
import { ProjectUsageSection } from './ProjectUsageSection'
import { ProjectUsageSectionDeltas } from './ProjectUsageSectionDeltas'
import { TopSection } from '@/components/interfaces/ProjectHome/TopSection'
import { ProjectNeedsSecuring } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring'
import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
import { SortableSection } from '@/components/ui/SortableSection'
import { useLocalStorage } from '@/hooks/misc/useLocalStorage'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
import { useAppStateSnapshot } from '@/state/app-state'
const SORT_GRIP_CLASS = 'absolute -left-6 top-1.5'
export const ProjectHome = () => {
const { enableBranching } = useParams()
const snap = useAppStateSnapshot()
const { data: project } = useSelectedProjectQuery()
const track = useTrack()
const showHomepageUsageDeltas = useFlag('newHomepageUsageDeltas')
const isMatureProject = dayjs(project?.inserted_at).isBefore(dayjs().subtract(10, 'day'))
const hasShownEnableBranchingModalRef = useRef(false)
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const isComingUp = project?.status === PROJECT_STATUS.COMING_UP
const [sectionOrder, setSectionOrder] = useLocalStorage<string[]>(
`home-section-order-${project?.ref || 'default'}`,
DEFAULT_SECTION_ORDER
)
const UsageSection = showHomepageUsageDeltas ? ProjectUsageSectionDeltas : ProjectUsageSection
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
setSectionOrder((items) => {
const oldIndex = items.indexOf(String(active.id))
const newIndex = items.indexOf(String(over.id))
if (oldIndex === -1 || newIndex === -1) return items
track('home_section_rows_moved', {
section_moved: String(active.id),
old_position: oldIndex,
new_position: newIndex,
})
return arrayMove(items, oldIndex, newIndex)
})
}
useEffect(() => {
if (enableBranching && !hasShownEnableBranchingModalRef.current) {
hasShownEnableBranchingModalRef.current = true
snap.setShowCreateBranchModal(true)
}
}, [enableBranching, snap])
useEffect(() => {
setSectionOrder(mergeSectionOrder)
}, [setSectionOrder])
// On self-hosted the project's `inserted_at` is hard-coded to a past date
// (so it always looks "mature"), and the home page is otherwise sparse —
// always surface the Get Connected pane there. Platform keeps the
// maturity gate so long-running projects don't see it forever.
const showConnectSection = !!project && (!IS_PLATFORM || !isMatureProject)
const renderOrder = mergeSectionOrder(sectionOrder).filter((id) => {
if (id === 'connect') return showConnectSection
if (id === 'usage' || id === 'custom-report') return IS_PLATFORM
return true
})
return (
<ProjectNeedsSecuring>
<div className="w-full h-full">
<ScaffoldContainer size="large" className={cn(isPaused && 'h-full')}>
<ScaffoldSection
isFullWidth
className={cn(isPaused ? 'h-full flex justify-center p-0!' : 'pb-0')}
>
<TopSection />
</ScaffoldSection>
</ScaffoldContainer>
{!isPaused && (
<ScaffoldContainer size="large">
<ScaffoldSection isFullWidth className="gap-12 pb-32">
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
<SortableContext items={renderOrder} strategy={verticalListSortingStrategy}>
{renderOrder.map((id) => {
if (IS_PLATFORM && id === 'usage') {
return (
<div
key={id}
className={cn(isComingUp && 'opacity-60 pointer-events-none')}
>
<SortableSection gripClassName={SORT_GRIP_CLASS} id={id}>
<UsageSection />
</SortableSection>
</div>
)
}
if (id === 'connect' && showConnectSection) {
return (
<SortableSection gripClassName={SORT_GRIP_CLASS} key={id} id={id}>
<ConnectSection />
</SortableSection>
)
}
if (id === 'advisor') {
return (
<div
key={id}
className={cn(isComingUp && 'opacity-60 pointer-events-none')}
>
<SortableSection gripClassName={SORT_GRIP_CLASS} id={id}>
<AdvisorSection showEmptyState={isComingUp} />
</SortableSection>
</div>
)
}
if (IS_PLATFORM && id === 'custom-report') {
return (
<div
key={id}
className={cn(isComingUp && 'opacity-60 pointer-events-none')}
>
<SortableSection gripClassName={SORT_GRIP_CLASS} id={id}>
<CustomReportSection />
</SortableSection>
</div>
)
}
})}
</SortableContext>
</DndContext>
</ScaffoldSection>
</ScaffoldContainer>
)}
</div>
</ProjectNeedsSecuring>
)
}