Enhance Dashboard Components with Loading State and Styling Improvements
- Updated ListViewEditButtons to disable actions during initial loading, improving user experience. - Refactored ListViewTabs to adjust icon styles based on loading state and removed unnecessary imports for cleaner code. - Modified ObjectListViewContext to use viewId instead of view reference for better clarity and consistency. - Updated TableStateContext to reflect changes in view parameter naming, enhancing code maintainability. - Adjusted App.css for improved styling of segmented navigation items and loading indicators.
This commit is contained in:
parent
96b809b9d8
commit
73de7ac8ee
@ -2647,6 +2647,7 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
|
||||
.segmented-nav-item-icon {
|
||||
padding-inline: 8px;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.segmented-nav-item-icon-contents {
|
||||
@ -2667,7 +2668,12 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
||||
.segmented-nav-item-selected,
|
||||
.segmented-nav-item:not(.segmented-nav-item-disabled):hover {
|
||||
background: var(--color-button-background);
|
||||
border: 1px solid var(--color-button-border);
|
||||
border: 1px solid
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--color-button-border) 40%,
|
||||
var(--color-button-background)
|
||||
);
|
||||
}
|
||||
|
||||
.segmented-nav-item-disabled {
|
||||
|
||||
@ -4,12 +4,14 @@ import EditButtons from './EditButtons'
|
||||
import { useObjectListView } from '../context/ObjectListViewContext'
|
||||
|
||||
const ListViewEditButtons = ({ showStartingDivider = false }) => {
|
||||
const { isEditing, saving, saveEdits, cancelEdits, startEditing } =
|
||||
useObjectListView()
|
||||
|
||||
if (!isEditing) {
|
||||
return null
|
||||
}
|
||||
const {
|
||||
isEditing,
|
||||
initialLoading,
|
||||
saving,
|
||||
saveEdits,
|
||||
cancelEdits,
|
||||
startEditing
|
||||
} = useObjectListView()
|
||||
|
||||
return (
|
||||
<Flex align='center' gap='small'>
|
||||
@ -24,6 +26,7 @@ const ListViewEditButtons = ({ showStartingDivider = false }) => {
|
||||
cancelEditing={cancelEdits}
|
||||
startEditing={startEditing}
|
||||
formValid={true}
|
||||
disabled={initialLoading}
|
||||
loading={saving}
|
||||
requirePermission={false}
|
||||
/>
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
Button,
|
||||
Flex,
|
||||
Typography,
|
||||
Spin,
|
||||
Divider,
|
||||
theme
|
||||
} from 'antd'
|
||||
@ -18,7 +17,6 @@ import { getModelByName } from '../../../database/ObjectModels'
|
||||
import { useObjectListView } from '../context/ObjectListViewContext'
|
||||
import classNames from 'classnames'
|
||||
import PlusIcon from '../../Icons/PlusIcon'
|
||||
import EditIcon from '../../Icons/EditIcon'
|
||||
import BinIcon from '../../Icons/BinIcon'
|
||||
import ScrollBox from './ScrollBox'
|
||||
import ColorSelector from './ColorSelector'
|
||||
@ -154,14 +152,16 @@ const ViewTabLabel = ({
|
||||
className='list-view-tabs-label list-view-tabs-label-editing'
|
||||
onFocusCapture={() => onSelect?.()}
|
||||
>
|
||||
<ColorSelector
|
||||
size='small'
|
||||
showText={false}
|
||||
required
|
||||
value={view.color || '#3498DB'}
|
||||
onChange={(color) => onDraftChange({ color })}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
<div style={{ marginRight: 2 }}>
|
||||
<ColorSelector
|
||||
size='small'
|
||||
showText={false}
|
||||
required
|
||||
value={view.color || '#3498DB'}
|
||||
onChange={(color) => onDraftChange({ color })}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<ViewTabNameInput
|
||||
value={view.name}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
@ -196,7 +196,6 @@ const ListViewTabs = () => {
|
||||
activeTabKey,
|
||||
initialLoading,
|
||||
isEditing,
|
||||
startEditing,
|
||||
selectTab,
|
||||
handleTabEdit,
|
||||
updateDraftView,
|
||||
@ -207,10 +206,19 @@ const ListViewTabs = () => {
|
||||
const model = getModelByName(objectType)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const Icon = model?.icon
|
||||
const Icon = initialLoading ? LoadingOutlined : model?.icon
|
||||
const styles = initialLoading
|
||||
? { fontSize: 12, color: 'var(--color-button-border)' }
|
||||
: { fontSize: 14 }
|
||||
const allOption = {
|
||||
value: ALL_TAB_KEY,
|
||||
icon: Icon ? <Icon style={{ fontSize: 14 }} /> : null
|
||||
icon: Icon ? (
|
||||
<Icon
|
||||
style={styles}
|
||||
color={'secondary'}
|
||||
spin={initialLoading || undefined}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
const viewOptions = views.map((view) => ({
|
||||
@ -242,6 +250,7 @@ const ListViewTabs = () => {
|
||||
ALL_TAB_KEY,
|
||||
activeTabKey,
|
||||
handleTabEdit,
|
||||
initialLoading,
|
||||
isEditing,
|
||||
model?.icon,
|
||||
selectTab,
|
||||
@ -256,51 +265,40 @@ const ListViewTabs = () => {
|
||||
style={{ minHeight: 0, width: '100%', minWidth: 0 }}
|
||||
>
|
||||
<Divider type='vertical' style={{ margin: '0 4px', height: '18px' }} />
|
||||
<Spin
|
||||
spinning={initialLoading === true}
|
||||
wrapperClassName='list-view-tabs-spin'
|
||||
indicator={<LoadingOutlined spin />}
|
||||
|
||||
<Flex
|
||||
align='center'
|
||||
gap={4}
|
||||
className={classNames('list-view-tabs', {
|
||||
'list-view-tabs-editing': isEditing
|
||||
})}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<Flex
|
||||
align='center'
|
||||
gap={4}
|
||||
className={classNames('list-view-tabs', {
|
||||
'list-view-tabs-editing': isEditing
|
||||
})}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
|
||||
<Flex
|
||||
align='center'
|
||||
gap={8}
|
||||
className='list-view-tabs-segmented-inner'
|
||||
>
|
||||
<SegmentedNav
|
||||
className='list-view-tabs-segmented'
|
||||
value={activeTabKey}
|
||||
options={options}
|
||||
onChange={selectTab}
|
||||
onReorder={isEditing ? reorderDraftViews : undefined}
|
||||
/>
|
||||
<ScrollBox className='list-view-tabs-segmented-wrap' horizontal>
|
||||
<Flex
|
||||
align='center'
|
||||
gap={8}
|
||||
className='list-view-tabs-segmented-inner'
|
||||
>
|
||||
<SegmentedNav
|
||||
className='list-view-tabs-segmented'
|
||||
value={activeTabKey}
|
||||
options={options}
|
||||
onChange={selectTab}
|
||||
onReorder={isEditing ? reorderDraftViews : undefined}
|
||||
/>
|
||||
{isEditing && (
|
||||
<Button
|
||||
type='text'
|
||||
size='small'
|
||||
className='list-view-tabs-add-button'
|
||||
onClick={
|
||||
isEditing ? () => handleTabEdit(null, 'add') : startEditing
|
||||
}
|
||||
icon={
|
||||
isEditing ? (
|
||||
<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />
|
||||
) : (
|
||||
<EditIcon style={{ fontSize: 12, marginBottom: 5 }} />
|
||||
)
|
||||
}
|
||||
onClick={() => handleTabEdit(null, 'add')}
|
||||
icon={<PlusIcon style={{ fontSize: 14, marginTop: 3 }} />}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
</Spin>
|
||||
)}
|
||||
</Flex>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -37,15 +37,12 @@ const DEFAULT_VIEW = {
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const getObjectViewKey = (view) =>
|
||||
view?._reference || view?._id || null
|
||||
view?._id != null ? String(view._id) : null
|
||||
|
||||
const resolveViewFromRef = (views, viewRef) => {
|
||||
if (!viewRef) return null
|
||||
const resolveViewFromId = (views, viewId) => {
|
||||
if (!viewId) return null
|
||||
return (
|
||||
views.find(
|
||||
(view) =>
|
||||
view._reference === viewRef || String(view._id) === String(viewRef)
|
||||
) || null
|
||||
views.find((view) => String(view._id) === String(viewId)) || null
|
||||
)
|
||||
}
|
||||
|
||||
@ -206,15 +203,15 @@ export const ObjectListViewProvider = ({
|
||||
useEffect(() => {
|
||||
if (initialLoading || urlSyncedRef.current) return
|
||||
|
||||
const viewRef = getViewFromUrl()
|
||||
if (!viewRef) {
|
||||
const viewId = getViewFromUrl()
|
||||
if (!viewId) {
|
||||
urlSyncedRef.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const matched =
|
||||
resolveViewFromRef(views, viewRef) ||
|
||||
resolveViewFromRef(draftViews, viewRef)
|
||||
resolveViewFromId(views, viewId) ||
|
||||
resolveViewFromId(draftViews, viewId)
|
||||
|
||||
if (matched) {
|
||||
setActiveTabKey(String(matched._id))
|
||||
@ -222,7 +219,7 @@ export const ObjectListViewProvider = ({
|
||||
return
|
||||
}
|
||||
|
||||
// Wait until views have loaded before deciding the URL reference is invalid
|
||||
// Wait until views have loaded before deciding the URL viewId is invalid
|
||||
if (views.length === 0) return
|
||||
|
||||
if (!isEditing) {
|
||||
|
||||
@ -12,7 +12,7 @@ const TableStateContext = createContext()
|
||||
|
||||
export const FILTER_URL_PARAM = 'filter'
|
||||
export const SORT_URL_PARAM = 'sort'
|
||||
export const VIEW_URL_PARAM = 'view'
|
||||
export const VIEW_URL_PARAM = 'viewId'
|
||||
|
||||
const getSessionFilterKey = (scope) => `tableState:${scope}:filter`
|
||||
const getSessionSortKey = (scope) => `tableState:${scope}:sort`
|
||||
@ -137,10 +137,12 @@ export const TableStateProvider = ({ children }) => {
|
||||
)
|
||||
|
||||
const persistView = useCallback(
|
||||
(viewRef) => {
|
||||
(viewId) => {
|
||||
const next = new URLSearchParams(searchParams)
|
||||
if (viewRef) {
|
||||
next.set(VIEW_URL_PARAM, viewRef)
|
||||
// Drop legacy `view` param that previously stored `_reference`
|
||||
next.delete('view')
|
||||
if (viewId) {
|
||||
next.set(VIEW_URL_PARAM, String(viewId))
|
||||
} else {
|
||||
next.delete(VIEW_URL_PARAM)
|
||||
}
|
||||
|
||||
@ -182,7 +182,7 @@ export const ThemeProvider = ({ children }) => {
|
||||
)
|
||||
root.style.setProperty(
|
||||
'--color-list-view-tabs-border',
|
||||
isDarkMode ? '#2a2a2a' : '#E7E7E7'
|
||||
isDarkMode ? '#1f1f1f' : '#E7E7E7'
|
||||
)
|
||||
root.style.setProperty(
|
||||
'--color-button-background',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user