162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
import { mdiChartTimelineVariant } from '@mdi/js'
|
|
import Head from 'next/head'
|
|
import { useRouter } from 'next/router'
|
|
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
|
|
import axios from 'axios'
|
|
import CardBox from '../components/CardBox'
|
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
|
import SectionMain from '../components/SectionMain'
|
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
|
import { getPageTitle } from '../config'
|
|
import ElementPreview from '../components/UiElements/ElementPreview'
|
|
import {
|
|
getDefaultSettings,
|
|
normalizeUiElement,
|
|
toElementLabel,
|
|
UI_ELEMENT_TYPES,
|
|
} from '../components/UiElements/defaults'
|
|
import { UiElementItem, UiElementRow } from '../components/UiElements/types'
|
|
|
|
const UiElementsPage = () => {
|
|
const router = useRouter()
|
|
|
|
const [elements, setElements] = useState<UiElementItem[]>([])
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [errorMessage, setErrorMessage] = useState('')
|
|
|
|
const fetchRows = useCallback(async () => {
|
|
const response = await axios.get('/ui-elements?limit=1000&page=0&sort=asc&field=sort_order')
|
|
return Array.isArray(response?.data?.rows) ? response.data.rows : []
|
|
}, [])
|
|
|
|
const bootstrapMissingElements = useCallback(async (rows: UiElementRow[]) => {
|
|
const existingTypes = new Set(rows.map((row) => row.element_type))
|
|
const missingTypes = UI_ELEMENT_TYPES.filter((type) => !existingTypes.has(type))
|
|
|
|
const safeStartOrder = Math.max(
|
|
1,
|
|
...rows
|
|
.map((row) => Number(row.sort_order))
|
|
.filter((value) => Number.isFinite(value))
|
|
.map((value) => Math.trunc(value) + 1),
|
|
)
|
|
|
|
if (!missingTypes.length) return
|
|
|
|
const createResults = await Promise.allSettled(
|
|
missingTypes.map((elementType, index) =>
|
|
axios.post('/ui-elements', {
|
|
data: {
|
|
element_type: elementType,
|
|
name: toElementLabel(elementType),
|
|
sort_order: safeStartOrder + index,
|
|
settings_json: JSON.stringify(getDefaultSettings(elementType)),
|
|
},
|
|
}),
|
|
),
|
|
)
|
|
|
|
const rejected = createResults.filter((result) => result.status === 'rejected') as PromiseRejectedResult[]
|
|
|
|
if (rejected.length) {
|
|
const reasons = rejected
|
|
.map((result) => {
|
|
const err = result.reason
|
|
return err?.response?.data?.message || err?.message || 'Unknown create error'
|
|
})
|
|
.join('; ')
|
|
|
|
throw new Error(`Failed to bootstrap some UI elements: ${reasons}`)
|
|
}
|
|
}, [])
|
|
|
|
const loadElements = useCallback(async () => {
|
|
setIsLoading(true)
|
|
setErrorMessage('')
|
|
|
|
try {
|
|
const initialRows = (await fetchRows()) as UiElementRow[]
|
|
await bootstrapMissingElements(initialRows)
|
|
|
|
const rowsAfterBootstrap = (await fetchRows()) as UiElementRow[]
|
|
const normalized = rowsAfterBootstrap
|
|
.map((row) => normalizeUiElement(row))
|
|
.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
|
|
|
|
setElements(normalized)
|
|
} catch (error: any) {
|
|
const message = error?.response?.data?.message || error?.message || 'Failed to load UI elements.'
|
|
setErrorMessage(message)
|
|
setElements([])
|
|
console.error('Failed to load UI elements:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [bootstrapMissingElements, fetchRows])
|
|
|
|
useEffect(() => {
|
|
loadElements()
|
|
}, [loadElements])
|
|
|
|
const sortedElements = useMemo(
|
|
() => [...elements].sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)),
|
|
[elements],
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{getPageTitle('UI Elements')}</title>
|
|
</Head>
|
|
<SectionMain>
|
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='UI Elements' main>
|
|
{''}
|
|
</SectionTitleLineWithButton>
|
|
|
|
{errorMessage ? <CardBox className='mb-4 text-sm text-red-600'>{errorMessage}</CardBox> : null}
|
|
|
|
<CardBox>
|
|
{isLoading ? (
|
|
<p className='text-sm text-gray-500'>Loading platform elements...</p>
|
|
) : !sortedElements.length ? (
|
|
<p className='text-sm text-gray-500'>No UI elements found.</p>
|
|
) : (
|
|
<div className='overflow-x-auto'>
|
|
<table className='w-full min-w-[680px] text-sm'>
|
|
<thead>
|
|
<tr className='border-b border-gray-200 text-left dark:border-dark-700'>
|
|
<th className='py-2 pr-3 font-semibold'>Name</th>
|
|
<th className='py-2 pr-3 font-semibold'>Type</th>
|
|
<th className='py-2 pr-3 font-semibold'>Preview</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{sortedElements.map((item) => (
|
|
<tr
|
|
key={item.id}
|
|
className='cursor-pointer border-b border-gray-100 hover:bg-gray-50 dark:border-dark-800 dark:hover:bg-dark-800'
|
|
onClick={() => router.push(`/ui-elements/${item.id}`)}
|
|
>
|
|
<td className='py-3 pr-3 font-semibold'>{item.name}</td>
|
|
<td className='py-3 pr-3 text-xs text-gray-500'>{toElementLabel(item.elementType)}</td>
|
|
<td className='py-3 pr-3'>
|
|
<ElementPreview item={item} />
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</CardBox>
|
|
</SectionMain>
|
|
</>
|
|
)
|
|
}
|
|
|
|
UiElementsPage.getLayout = function getLayout(page: ReactElement) {
|
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
|
}
|
|
|
|
export default UiElementsPage
|