Enhance Date Filtering and Input Components with New Date Utilities and UI Improvements
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Added new date utility functions for better handling of date ranges, including start and end of month, week, and year. - Updated FilterInput component to support date unit highlighting and improved tokenization for date expressions. - Enhanced QuickPropertyFilters to include additional date filter options such as 'Today', 'Yesterday', and relative date ranges. - Improved CSS styles for date input elements to enhance visibility and user interaction. - Refactored AuditLogs component to integrate new ObjectTableViewButton for better data representation.
This commit is contained in:
parent
d02895b78d
commit
8c4911809c
@ -2800,6 +2800,11 @@ body.objectKanbanColumnResizing * {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-input-date-unit,
|
||||||
|
[data-date-unit] {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-input-operand-tag {
|
.filter-input-operand-tag {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import ColumnViewButton from '../common/ColumnViewButton'
|
|||||||
import ExportListButton from '../common/ExportListButton'
|
import ExportListButton from '../common/ExportListButton'
|
||||||
import ListViewTabs from '../common/ListViewTabs'
|
import ListViewTabs from '../common/ListViewTabs'
|
||||||
import ListViewEditButtons from '../common/ListViewEditButtons'
|
import ListViewEditButtons from '../common/ListViewEditButtons'
|
||||||
|
import ObjectTableViewButton from '../common/ObjectTableViewButton'
|
||||||
import { ObjectListViewProvider } from '../context/ObjectListViewContext'
|
import { ObjectListViewProvider } from '../context/ObjectListViewContext'
|
||||||
|
|
||||||
const AuditLogs = () => {
|
const AuditLogs = () => {
|
||||||
@ -26,15 +27,11 @@ const AuditLogs = () => {
|
|||||||
const [showSortSidebar, setShowSortSidebar] =
|
const [showSortSidebar, setShowSortSidebar] =
|
||||||
useSortSidebarVisibility('AuditLogs')
|
useSortSidebarVisibility('AuditLogs')
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ObjectListViewProvider
|
<ObjectListViewProvider objectType='auditLog' tableRef={tableRef}>
|
||||||
objectType='auditLog'
|
<Flex vertical={'true'} gap='large' className='h-100'>
|
||||||
tableRef={tableRef}
|
<Flex justify='space-between' style={{ minHeight: 0 }} gap='small'>
|
||||||
>
|
<Space size='small'>
|
||||||
<Flex vertical={'true'} gap='large' className='h-100'>
|
|
||||||
<Flex justify='space-between' style={{ minHeight: 0 }} gap='small'>
|
|
||||||
<Space size='small'>
|
|
||||||
<ObjectActions
|
<ObjectActions
|
||||||
type='auditLog'
|
type='auditLog'
|
||||||
pageName='list'
|
pageName='list'
|
||||||
@ -48,8 +45,8 @@ const AuditLogs = () => {
|
|||||||
/>
|
/>
|
||||||
<ExportListButton objectType='auditLog' />
|
<ExportListButton objectType='auditLog' />
|
||||||
</Space>
|
</Space>
|
||||||
<ListViewTabs />
|
<ListViewTabs />
|
||||||
<Space>
|
<Space>
|
||||||
<SortSidebarButton
|
<SortSidebarButton
|
||||||
active={showSortSidebar}
|
active={showSortSidebar}
|
||||||
onClick={() => setShowSortSidebar(!showSortSidebar)}
|
onClick={() => setShowSortSidebar(!showSortSidebar)}
|
||||||
@ -58,9 +55,10 @@ const AuditLogs = () => {
|
|||||||
active={showFilterSidebar}
|
active={showFilterSidebar}
|
||||||
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
|
onClick={() => setShowFilterSidebar(!showFilterSidebar)}
|
||||||
/>
|
/>
|
||||||
|
<ObjectTableViewButton objectType='auditLog' />
|
||||||
<ListViewEditButtons showStartingDivider={true} />
|
<ListViewEditButtons showStartingDivider={true} />
|
||||||
</Space>
|
</Space>
|
||||||
</Flex>
|
</Flex>
|
||||||
<ObjectTable
|
<ObjectTable
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
visibleColumns={columnVisibility}
|
visibleColumns={columnVisibility}
|
||||||
@ -76,10 +74,9 @@ const AuditLogs = () => {
|
|||||||
saveSortInUrl={true}
|
saveSortInUrl={true}
|
||||||
useSortInSession={true}
|
useSortInSession={true}
|
||||||
useSortInUrl={true}
|
useSortInUrl={true}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</ObjectListViewProvider>
|
</ObjectListViewProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -84,28 +84,58 @@ const OPERAND_BY_SYMBOL = Object.fromEntries(
|
|||||||
OPERANDS.map((operand) => [operand.symbol, operand])
|
OPERANDS.map((operand) => [operand.symbol, operand])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DATE_HIGHLIGHT_RE = /TODAY|[+-]?(?:\d+|C)[DdWwYyHhMm]/gi
|
||||||
|
|
||||||
|
const expandDateHighlightTokens = (text) => {
|
||||||
|
const tokens = []
|
||||||
|
const pattern = new RegExp(DATE_HIGHLIGHT_RE.source, DATE_HIGHLIGHT_RE.flags)
|
||||||
|
let lastIndex = 0
|
||||||
|
let match = pattern.exec(text)
|
||||||
|
|
||||||
|
while (match) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
tokens.push({ type: 'text', value: text.slice(lastIndex, match.index) })
|
||||||
|
}
|
||||||
|
const raw = match[0]
|
||||||
|
if (/^TODAY$/i.test(raw)) {
|
||||||
|
tokens.push({ type: 'dateUnit', value: raw })
|
||||||
|
} else {
|
||||||
|
tokens.push({ type: 'text', value: raw.slice(0, -1) })
|
||||||
|
tokens.push({ type: 'dateUnit', value: raw.slice(-1) })
|
||||||
|
}
|
||||||
|
lastIndex = match.index + raw.length
|
||||||
|
match = pattern.exec(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
tokens.push({ type: 'text', value: text.slice(lastIndex) })
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
const tokenize = (raw = '') => {
|
const tokenize = (raw = '') => {
|
||||||
const value = String(raw)
|
const value = String(raw)
|
||||||
const tokens = []
|
const tokens = []
|
||||||
let text = ''
|
let text = ''
|
||||||
let i = 0
|
let i = 0
|
||||||
|
|
||||||
|
const pushText = (value) => {
|
||||||
|
if (!value) return
|
||||||
|
tokens.push(...expandDateHighlightTokens(value))
|
||||||
|
}
|
||||||
|
|
||||||
while (i < value.length) {
|
while (i < value.length) {
|
||||||
const match = OPERANDS.find((operand) =>
|
const match = OPERANDS.find((operand) =>
|
||||||
value.startsWith(operand.symbol, i)
|
value.startsWith(operand.symbol, i)
|
||||||
)
|
)
|
||||||
if (match) {
|
if (match) {
|
||||||
if (text) {
|
pushText(text)
|
||||||
tokens.push({ type: 'text', value: text })
|
text = ''
|
||||||
text = ''
|
|
||||||
}
|
|
||||||
tokens.push({ type: 'operand', value: match.symbol })
|
tokens.push({ type: 'operand', value: match.symbol })
|
||||||
i += match.symbol.length
|
i += match.symbol.length
|
||||||
} else if (WILDCARDS.has(value[i])) {
|
} else if (WILDCARDS.has(value[i])) {
|
||||||
if (text) {
|
pushText(text)
|
||||||
tokens.push({ type: 'text', value: text })
|
text = ''
|
||||||
text = ''
|
|
||||||
}
|
|
||||||
tokens.push({ type: 'wildcard', value: value[i] })
|
tokens.push({ type: 'wildcard', value: value[i] })
|
||||||
i += 1
|
i += 1
|
||||||
} else {
|
} else {
|
||||||
@ -114,7 +144,7 @@ const tokenize = (raw = '') => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text) tokens.push({ type: 'text', value: text })
|
pushText(text)
|
||||||
return tokens
|
return tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,6 +156,14 @@ const renderWildcardNode = (char) => {
|
|||||||
return span
|
return span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderDateUnitNode = (value) => {
|
||||||
|
const span = document.createElement('span')
|
||||||
|
span.setAttribute('data-date-unit', value)
|
||||||
|
span.className = 'filter-input-date-unit'
|
||||||
|
span.textContent = value
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
|
||||||
const normalize = (raw = '') =>
|
const normalize = (raw = '') =>
|
||||||
tokenize(raw)
|
tokenize(raw)
|
||||||
.map((token) => token.value)
|
.map((token) => token.value)
|
||||||
@ -375,7 +413,8 @@ const domMatchesTokens = (root, raw) => {
|
|||||||
if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? '') !== ''
|
if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? '') !== ''
|
||||||
return (
|
return (
|
||||||
node.getAttribute?.('data-operand') != null ||
|
node.getAttribute?.('data-operand') != null ||
|
||||||
node.getAttribute?.('data-wildcard') != null
|
node.getAttribute?.('data-wildcard') != null ||
|
||||||
|
node.getAttribute?.('data-date-unit') != null
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -404,6 +443,12 @@ const domMatchesTokens = (root, raw) => {
|
|||||||
(child.textContent ?? '') === token.value
|
(child.textContent ?? '') === token.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (token.type === 'dateUnit') {
|
||||||
|
return (
|
||||||
|
child.getAttribute?.('data-date-unit') === token.value &&
|
||||||
|
(child.textContent ?? '') === token.value
|
||||||
|
)
|
||||||
|
}
|
||||||
return child.getAttribute?.('data-operand') === token.value
|
return child.getAttribute?.('data-operand') === token.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -598,6 +643,8 @@ const FilterInput = ({
|
|||||||
nextChildren.push(document.createTextNode(item.value))
|
nextChildren.push(document.createTextNode(item.value))
|
||||||
} else if (item.type === 'wildcard') {
|
} else if (item.type === 'wildcard') {
|
||||||
nextChildren.push(renderWildcardNode(item.value))
|
nextChildren.push(renderWildcardNode(item.value))
|
||||||
|
} else if (item.type === 'dateUnit') {
|
||||||
|
nextChildren.push(renderDateUnitNode(item.value))
|
||||||
} else {
|
} else {
|
||||||
nextChildren.push(takeChip(item.value))
|
nextChildren.push(takeChip(item.value))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,11 +13,7 @@ import ScrollBox from './ScrollBox'
|
|||||||
import MissingPlaceholder from './MissingPlaceholder'
|
import MissingPlaceholder from './MissingPlaceholder'
|
||||||
import LoadingPlaceholder from './LoadingPlaceholder'
|
import LoadingPlaceholder from './LoadingPlaceholder'
|
||||||
import ObjectTimelineRow from './ObjectTimelineRow'
|
import ObjectTimelineRow from './ObjectTimelineRow'
|
||||||
import {
|
import { getTimelineRangeFromValues, getTimelineTicks } from './timelineUtils'
|
||||||
getTimelineRange,
|
|
||||||
getTimelineRangeFromValues,
|
|
||||||
getTimelineTicks
|
|
||||||
} from './timelineUtils'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import { getStateTagInfo } from '../utils/Utils'
|
import { getStateTagInfo } from '../utils/Utils'
|
||||||
|
|
||||||
@ -75,8 +71,6 @@ const ObjectTimeline = ({
|
|||||||
JSON.parse(rangeQueryKey)
|
JSON.parse(rangeQueryKey)
|
||||||
|
|
||||||
const loadRangeValues = async () => {
|
const loadRangeValues = async () => {
|
||||||
setStartValues([])
|
|
||||||
setEndValues([])
|
|
||||||
try {
|
try {
|
||||||
const startPromise = getModelPropertyValuesRef.current(
|
const startPromise = getModelPropertyValuesRef.current(
|
||||||
type,
|
type,
|
||||||
@ -124,14 +118,20 @@ const ObjectTimeline = ({
|
|||||||
[onScroll]
|
[onScroll]
|
||||||
)
|
)
|
||||||
|
|
||||||
const timelineRange = useMemo(() => {
|
const timelineRange = useMemo(
|
||||||
const fromValues = getTimelineRangeFromValues(startValues, endValues)
|
() => getTimelineRangeFromValues(startValues, endValues),
|
||||||
if (fromValues) return fromValues
|
[endValues, startValues]
|
||||||
return getTimelineRange(records, startDate, endDate)
|
)
|
||||||
}, [endDate, endValues, records, startDate, startValues])
|
const lastTimelineRangeRef = useRef(null)
|
||||||
|
if (timelineRange) {
|
||||||
|
lastTimelineRangeRef.current = timelineRange
|
||||||
|
}
|
||||||
|
|
||||||
const range = useMemo(
|
const range = useMemo(
|
||||||
() => timelineRange || getTimelineRangeFromValues([new Date()]),
|
() =>
|
||||||
|
timelineRange ||
|
||||||
|
lastTimelineRangeRef.current ||
|
||||||
|
getTimelineRangeFromValues([new Date()]),
|
||||||
[timelineRange]
|
[timelineRange]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -182,12 +182,118 @@ const getFilterOptions = (category) => {
|
|||||||
]
|
]
|
||||||
case 'date':
|
case 'date':
|
||||||
return [
|
return [
|
||||||
{ key: 'equals', label: 'On', needsValue: true },
|
{ key: 'equals', label: 'Equals', needsValue: true },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'lessThan', label: 'Before', needsValue: true },
|
{ key: 'lessThan', label: 'Before', needsValue: true },
|
||||||
{ key: 'greaterThan', label: 'After', needsValue: true },
|
{ key: 'greaterThan', label: 'After', needsValue: true },
|
||||||
|
{ key: 'between', label: 'Between', needsRange: true },
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{ key: 'between', label: 'Between', needsRange: true }
|
{ key: 'yesterday', label: 'Yesterday', expression: '-1D' },
|
||||||
|
{ key: 'today', label: 'Today', expression: 'TODAY' },
|
||||||
|
{ key: 'tomorrow', label: 'Tomorrow', expression: '+1D' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'this',
|
||||||
|
label: 'This',
|
||||||
|
children: [
|
||||||
|
{ key: 'thisMinute', label: 'This minute', expression: 'Cm' },
|
||||||
|
{ key: 'thisHour', label: 'This hour', expression: 'CH' },
|
||||||
|
{ key: 'thisDay', label: 'This day', expression: 'CD' },
|
||||||
|
{ key: 'thisWeek', label: 'This week', expression: 'CW' },
|
||||||
|
{ key: 'thisMonth', label: 'This month', expression: 'CM' },
|
||||||
|
{ key: 'thisYear', label: 'This year', expression: 'CY' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'last',
|
||||||
|
label: 'Last',
|
||||||
|
children: [
|
||||||
|
{ key: 'last5Minutes', label: '5 mins', expression: '-5m..TODAY' },
|
||||||
|
{
|
||||||
|
key: 'last15Minutes',
|
||||||
|
label: '15 mins',
|
||||||
|
expression: '-15m..TODAY'
|
||||||
|
},
|
||||||
|
{ key: 'lastMinute', label: '30 mins', expression: '-30m..TODAY' },
|
||||||
|
{ key: 'lastHour', label: 'Hour', expression: '-1H..TODAY' },
|
||||||
|
{
|
||||||
|
key: 'last12Hours',
|
||||||
|
label: '12 hours',
|
||||||
|
expression: '-12H..TODAY'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'lastDay', label: 'Day', expression: '-1D..TODAY' },
|
||||||
|
{ key: 'last3Days', label: '3 days', expression: '-3D..TODAY' },
|
||||||
|
{ key: 'lastWeek', label: 'Week', expression: '-1W' },
|
||||||
|
{ key: 'last7Days', label: '7 days', expression: '-7D..TODAY' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'lastMonth', label: 'Month', expression: '-1M' },
|
||||||
|
{ key: 'last30Days', label: '30 days', expression: '-30D..TODAY' },
|
||||||
|
{ key: 'last3Months', label: '3 months', expression: '-3M' },
|
||||||
|
{ key: 'last90Days', label: '90 days', expression: '-90D' },
|
||||||
|
{ key: 'last6Months', label: '6 months', expression: '-6M' },
|
||||||
|
{ key: 'last180Days', label: '180 days', expression: '-180D' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'lastYear', label: 'Year', expression: '-1Y' },
|
||||||
|
{ key: 'last2Years', label: '2 years', expression: '-2Y' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'next',
|
||||||
|
label: 'Next',
|
||||||
|
children: [
|
||||||
|
{ key: 'next5Minutes', label: '5 mins', expression: '+5m..TODAY' },
|
||||||
|
{
|
||||||
|
key: 'next15Minutes',
|
||||||
|
label: '15 mins',
|
||||||
|
expression: '+15m..TODAY'
|
||||||
|
},
|
||||||
|
{ key: 'nextMinute', label: '30 mins', expression: '+30m..TODAY' },
|
||||||
|
{ key: 'nextHour', label: 'Hour', expression: '+1H..TODAY' },
|
||||||
|
{
|
||||||
|
key: 'next12Hours',
|
||||||
|
label: '12 hours',
|
||||||
|
expression: '+12H..TODAY'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'nextDay', label: 'Day', expression: 'Today..+1D' },
|
||||||
|
{ key: 'next3Days', label: '3 days', expression: 'Today..+3D' },
|
||||||
|
{ key: 'nextWeek', label: 'Week', expression: '+1W' },
|
||||||
|
{ key: 'next7Days', label: '7 days', expression: 'Today..+7D' },
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'nextMonth', label: 'Month', expression: '+1M' },
|
||||||
|
{ key: 'next30Days', label: '30 days', expression: 'Today..+30D' },
|
||||||
|
{ key: 'next3Months', label: '3 months', expression: '+3M' },
|
||||||
|
{ key: 'next90Days', label: '90 days', expression: 'Today..+90D' },
|
||||||
|
{ key: 'next6Months', label: '6 months', expression: '+6M' },
|
||||||
|
{
|
||||||
|
key: 'next180Days',
|
||||||
|
label: '180 days',
|
||||||
|
expression: 'Today..+180D'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{ key: 'nextYear', label: 'Year', expression: '+1Y' },
|
||||||
|
{ key: 'next2Years', label: '2 years', expression: '+2Y' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'fromToday',
|
||||||
|
label: 'From today',
|
||||||
|
expression: 'TODAY..'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'untilToday',
|
||||||
|
label: 'Until today',
|
||||||
|
expression: '..TODAY'
|
||||||
|
},
|
||||||
|
{ type: 'divider' },
|
||||||
|
{
|
||||||
|
key: 'yearToDate',
|
||||||
|
label: 'Year to Date',
|
||||||
|
expression: 'CY..TODAY'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
case 'object':
|
case 'object':
|
||||||
return [
|
return [
|
||||||
@ -225,7 +331,14 @@ const buildFilterMenuItems = (options, onOptionClick) =>
|
|||||||
: {
|
: {
|
||||||
key: item.key,
|
key: item.key,
|
||||||
label: item.label,
|
label: item.label,
|
||||||
onClick: () => onOptionClick(item)
|
onClick: () =>
|
||||||
|
!item.children || item.children.length === 0
|
||||||
|
? onOptionClick(item)
|
||||||
|
: undefined,
|
||||||
|
children:
|
||||||
|
item.children && item.children.length > 0
|
||||||
|
? buildFilterMenuItems(item.children, onOptionClick)
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -275,6 +388,10 @@ const QuickPropertyFilters = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleOptionClick = (option) => {
|
const handleOptionClick = (option) => {
|
||||||
|
if (option.expression != null) {
|
||||||
|
onChange?.([option.expression])
|
||||||
|
return
|
||||||
|
}
|
||||||
if (option.key === 'isTrue' || option.key === 'isFalse') {
|
if (option.key === 'isTrue' || option.key === 'isFalse') {
|
||||||
applyFilter(option.key)
|
applyFilter(option.key)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -53,16 +53,213 @@ const endOfDay = (date) => {
|
|||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startOfMonth = (date) =>
|
||||||
|
new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
const endOfMonth = (date) =>
|
||||||
|
new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999)
|
||||||
|
|
||||||
|
const shiftCalendarMonths = (date, n) =>
|
||||||
|
new Date(date.getFullYear(), date.getMonth() + n, 1)
|
||||||
|
|
||||||
|
const startOfWeek = (date) => {
|
||||||
|
const d = startOfDay(date)
|
||||||
|
const day = d.getDay()
|
||||||
|
const mondayOffset = day === 0 ? -6 : 1 - day
|
||||||
|
d.setDate(d.getDate() + mondayOffset)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOfWeek = (date) => {
|
||||||
|
const start = startOfWeek(date)
|
||||||
|
const end = new Date(start)
|
||||||
|
end.setDate(end.getDate() + 6)
|
||||||
|
return endOfDay(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftWeeks = (date, n) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setDate(d.getDate() + n * 7)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfYear = (date) => new Date(date.getFullYear(), 0, 1, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
const endOfYear = (date) =>
|
||||||
|
new Date(date.getFullYear(), 11, 31, 23, 59, 59, 999)
|
||||||
|
|
||||||
|
const shiftYears = (date, n) => new Date(date.getFullYear() + n, 0, 1)
|
||||||
|
|
||||||
|
const startOfHour = (date) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setMinutes(0, 0, 0)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOfHour = (date) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setMinutes(59, 59, 999)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
const startOfMinute = (date) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setSeconds(0, 0)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
const endOfMinute = (date) => {
|
||||||
|
const d = new Date(date)
|
||||||
|
d.setSeconds(59, 999)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
const normalizeYear = (year) => {
|
const normalizeYear = (year) => {
|
||||||
if (year >= 100) return year
|
if (year >= 100) return year
|
||||||
return year < 70 ? 2000 + year : 1900 + year
|
return year < 70 ? 2000 + year : 1900 + year
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RELATIVE_OFFSET_RE = /^([+-])?(\d+)([DdWwYyHhMm])$/
|
||||||
|
const RELATIVE_CURRENT_RE = /^([+-])?C([DdWwYyHhMm])$/
|
||||||
|
|
||||||
|
const normalizeRelativeUnit = (letter) => {
|
||||||
|
if (letter === 'M' || letter === 'm') return letter
|
||||||
|
return letter.toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveRelativeUnit = (
|
||||||
|
now,
|
||||||
|
unit,
|
||||||
|
amount,
|
||||||
|
boundary,
|
||||||
|
isCurrent,
|
||||||
|
minusCurrent
|
||||||
|
) => {
|
||||||
|
const end = boundary === 'end'
|
||||||
|
|
||||||
|
if (unit === 'D') {
|
||||||
|
if (isCurrent) return end ? endOfDay(now) : startOfDay(now)
|
||||||
|
const date = new Date(now)
|
||||||
|
date.setDate(date.getDate() + amount)
|
||||||
|
return end ? endOfDay(date) : startOfDay(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'W') {
|
||||||
|
if (isCurrent && minusCurrent) {
|
||||||
|
return end ? endOfWeek(shiftWeeks(now, -1)) : startOfWeek(now)
|
||||||
|
}
|
||||||
|
const target = isCurrent ? now : shiftWeeks(now, amount)
|
||||||
|
return end ? endOfWeek(target) : startOfWeek(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'M') {
|
||||||
|
if (isCurrent) {
|
||||||
|
if (minusCurrent) {
|
||||||
|
return end
|
||||||
|
? endOfMonth(shiftCalendarMonths(now, -1))
|
||||||
|
: startOfMonth(now)
|
||||||
|
}
|
||||||
|
return end ? endOfMonth(now) : startOfMonth(now)
|
||||||
|
}
|
||||||
|
const month = shiftCalendarMonths(now, amount)
|
||||||
|
return end ? endOfMonth(month) : startOfMonth(month)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'Y') {
|
||||||
|
if (isCurrent) {
|
||||||
|
if (minusCurrent) {
|
||||||
|
return end ? endOfYear(shiftYears(now, -1)) : startOfYear(now)
|
||||||
|
}
|
||||||
|
return end ? endOfYear(now) : startOfYear(now)
|
||||||
|
}
|
||||||
|
const year = shiftYears(now, amount)
|
||||||
|
return end ? endOfYear(year) : startOfYear(year)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'H') {
|
||||||
|
if (isCurrent) {
|
||||||
|
if (minusCurrent) {
|
||||||
|
const prev = new Date(now)
|
||||||
|
prev.setHours(prev.getHours() - 1)
|
||||||
|
return end ? endOfHour(prev) : startOfHour(now)
|
||||||
|
}
|
||||||
|
return end ? endOfHour(now) : startOfHour(now)
|
||||||
|
}
|
||||||
|
const date = new Date(now)
|
||||||
|
date.setHours(date.getHours() + amount)
|
||||||
|
return end ? endOfHour(date) : startOfHour(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unit === 'm') {
|
||||||
|
if (isCurrent) {
|
||||||
|
if (minusCurrent) {
|
||||||
|
const prev = new Date(now)
|
||||||
|
prev.setMinutes(prev.getMinutes() - 1)
|
||||||
|
return end ? endOfMinute(prev) : startOfMinute(now)
|
||||||
|
}
|
||||||
|
return end ? endOfMinute(now) : startOfMinute(now)
|
||||||
|
}
|
||||||
|
const date = new Date(now)
|
||||||
|
date.setMinutes(date.getMinutes() + amount)
|
||||||
|
return end ? endOfMinute(date) : startOfMinute(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseRelativeDateOperand = (text, boundary = 'start') => {
|
||||||
|
const raw = String(text).trim()
|
||||||
|
if (!raw) return null
|
||||||
|
|
||||||
|
if (/^TODAY$/i.test(raw)) {
|
||||||
|
const now = new Date()
|
||||||
|
return boundary === 'end' ? endOfDay(now) : startOfDay(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = raw.match(RELATIVE_OFFSET_RE)
|
||||||
|
if (offset) {
|
||||||
|
const amount = (offset[1] === '-' ? -1 : 1) * Number(offset[2])
|
||||||
|
return resolveRelativeUnit(
|
||||||
|
new Date(),
|
||||||
|
normalizeRelativeUnit(offset[3]),
|
||||||
|
amount,
|
||||||
|
boundary,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = raw.match(RELATIVE_CURRENT_RE)
|
||||||
|
if (current) {
|
||||||
|
return resolveRelativeUnit(
|
||||||
|
new Date(),
|
||||||
|
normalizeRelativeUnit(current[2]),
|
||||||
|
0,
|
||||||
|
boundary,
|
||||||
|
true,
|
||||||
|
current[1] === '-'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateComponentsMatch = (date, year, month, day, hour, minute, second) =>
|
||||||
|
date.getFullYear() === year &&
|
||||||
|
date.getMonth() === month - 1 &&
|
||||||
|
date.getDate() === day &&
|
||||||
|
date.getHours() === hour &&
|
||||||
|
date.getMinutes() === minute &&
|
||||||
|
date.getSeconds() === second
|
||||||
|
|
||||||
const parseDateOperand = (value, boundary = 'start') => {
|
const parseDateOperand = (value, boundary = 'start') => {
|
||||||
const text = String(value).trim()
|
const text = String(value).trim()
|
||||||
if (!text) return null
|
if (!text) return null
|
||||||
const end = boundary === 'end'
|
const end = boundary === 'end'
|
||||||
|
|
||||||
|
const relative = parseRelativeDateOperand(text, boundary)
|
||||||
|
if (relative) return relative
|
||||||
|
|
||||||
if (/[-/T]/.test(text) || /\d:\d/.test(text)) {
|
if (/[-/T]/.test(text) || /\d:\d/.test(text)) {
|
||||||
const parsed = new Date(text)
|
const parsed = new Date(text)
|
||||||
if (Number.isNaN(parsed.getTime())) return null
|
if (Number.isNaN(parsed.getTime())) return null
|
||||||
@ -84,9 +281,46 @@ const parseDateOperand = (value, boundary = 'start') => {
|
|||||||
const second = nums.length >= 6 ? nums[5] : end ? 59 : 0
|
const second = nums.length >= 6 ? nums[5] : end ? 59 : 0
|
||||||
const ms = end ? 999 : 0
|
const ms = end ? 999 : 0
|
||||||
const date = new Date(year, month - 1, day, hour, minute, second, ms)
|
const date = new Date(year, month - 1, day, hour, minute, second, ms)
|
||||||
return Number.isNaN(date.getTime()) ? null : date
|
if (Number.isNaN(date.getTime())) return null
|
||||||
|
if (!dateComponentsMatch(date, year, month, day, hour, minute, second)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isValidDateOperand = (value) => parseDateOperand(value, 'start') != null
|
||||||
|
|
||||||
|
const isValidDateFilterLeaf = (rawToken) => {
|
||||||
|
const token = rawToken.trim()
|
||||||
|
if (token === '') return true
|
||||||
|
|
||||||
|
const rangeIdx = token.indexOf('..')
|
||||||
|
if (rangeIdx !== -1) {
|
||||||
|
const lo = stripIgnoreCase(token.slice(0, rangeIdx).trim())
|
||||||
|
const hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim())
|
||||||
|
if (lo !== '' && !isValidDateOperand(lo)) return false
|
||||||
|
if (hi !== '' && !isValidDateOperand(hi)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [symbol] of OPERATORS) {
|
||||||
|
if (token.startsWith(symbol)) {
|
||||||
|
const operand = stripIgnoreCase(token.slice(symbol.length).trim())
|
||||||
|
return operand !== '' && isValidDateOperand(operand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValidDateOperand(stripIgnoreCase(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidDateFilterNode = (node) => {
|
||||||
|
if (node.type === 'leaf') return isValidDateFilterLeaf(node.token)
|
||||||
|
return node.items.every(isValidDateFilterNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidDateFilterExpression = (expression) =>
|
||||||
|
isValidDateFilterNode(parseExpression(String(expression)))
|
||||||
|
|
||||||
const coerceScalar = (value) => {
|
const coerceScalar = (value) => {
|
||||||
const lower = String(value).toLowerCase()
|
const lower = String(value).toLowerCase()
|
||||||
if (lower === 'true') return true
|
if (lower === 'true') return true
|
||||||
@ -97,8 +331,7 @@ const coerceScalar = (value) => {
|
|||||||
|
|
||||||
const coerceBoundary = (value, isDateField, boundary) => {
|
const coerceBoundary = (value, isDateField, boundary) => {
|
||||||
if (isDateField) {
|
if (isDateField) {
|
||||||
const date = parseDateOperand(value, boundary)
|
return parseDateOperand(value, boundary)
|
||||||
if (date) return date
|
|
||||||
}
|
}
|
||||||
return coerceScalar(value)
|
return coerceScalar(value)
|
||||||
}
|
}
|
||||||
@ -377,6 +610,7 @@ export const matchesFilterExpression = (expression, candidate, fieldOptions = {}
|
|||||||
if (!text) return true
|
if (!text) return true
|
||||||
try {
|
try {
|
||||||
const ast = parseExpression(text)
|
const ast = parseExpression(text)
|
||||||
|
if (fieldOptions.isDateField && !isValidDateFilterNode(ast)) return true
|
||||||
return evalNode(candidate, ast, fieldOptions)
|
return evalNode(candidate, ast, fieldOptions)
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const AuditLog = {
|
|||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
sorters: ['createdAt', 'updatedAt'],
|
sorters: ['createdAt', 'updatedAt', 'owner', 'parent', 'operation'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user