mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
feat: Enhance AutoScore functionality with interval triggers and JSON tree support
- Added support for trigger trees in AutoScoreRule interface. - Implemented functions to convert query trees to JSON and vice versa. - Updated IntervalValue to use days, hours, and minutes instead of a single amount and unit. - Refactored IntervalValueWidget to allow separate input for days, hours, and minutes. - Added validation to ensure at least one interval trigger is present. - Enhanced user interface with informative alerts and tooltips for better user experience. - Updated localization files to include new strings for interval triggers and UI elements.
This commit is contained in:
@@ -1,116 +1,87 @@
|
||||
import React, { useMemo } from "react"
|
||||
import { InputNumber, Select } from "antd"
|
||||
import React from "react"
|
||||
import { InputNumber } from "antd"
|
||||
import type { WidgetProps } from "@react-awesome-query-builder/antd"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
getDefaultIntervalValue,
|
||||
parseIntervalTriggerValue,
|
||||
stringifyIntervalTriggerValue,
|
||||
type IntervalValue,
|
||||
} from "./IntervalValueCodec"
|
||||
|
||||
export type IntervalUnit = "month" | "day" | "minute"
|
||||
export { parseIntervalTriggerValue, stringifyIntervalTriggerValue } from "./IntervalValueCodec"
|
||||
export type { IntervalValue, IntervalUnit } from "./IntervalValueCodec"
|
||||
|
||||
export interface IntervalValue {
|
||||
amount: number
|
||||
unit: IntervalUnit
|
||||
}
|
||||
|
||||
export const DEFAULT_INTERVAL_UNIT: IntervalUnit = "day"
|
||||
|
||||
export const getDefaultIntervalValue = (): IntervalValue => ({
|
||||
amount: 1,
|
||||
unit: DEFAULT_INTERVAL_UNIT,
|
||||
})
|
||||
|
||||
export const parseIntervalTriggerValue = (value: unknown): IntervalValue | null => {
|
||||
if (value === null || value === undefined) return null
|
||||
|
||||
const str = String(value).trim()
|
||||
if (!str) return null
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(str)
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
typeof parsed.amount === "number" &&
|
||||
typeof parsed.unit === "string"
|
||||
) {
|
||||
const validUnits: IntervalUnit[] = ["month", "day", "minute"]
|
||||
if (validUnits.includes(parsed.unit as IntervalUnit)) {
|
||||
return {
|
||||
amount: Math.max(1, Math.floor(parsed.amount)),
|
||||
unit: parsed.unit as IntervalUnit,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
void 0
|
||||
const buildNextIntervalValue = (
|
||||
currentValue: IntervalValue | null,
|
||||
patch: Partial<IntervalValue>
|
||||
): IntervalValue => {
|
||||
const base = currentValue ?? getDefaultIntervalValue()
|
||||
return {
|
||||
days: patch.days ?? base.days,
|
||||
hours: patch.hours ?? base.hours,
|
||||
minutes: patch.minutes ?? base.minutes,
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const stringifyIntervalTriggerValue = (value: IntervalValue): string => {
|
||||
return JSON.stringify({
|
||||
amount: value.amount,
|
||||
unit: value.unit,
|
||||
})
|
||||
}
|
||||
const getDisplayValue = (value: number | undefined, fallback = 0) =>
|
||||
typeof value === "number" && Number.isFinite(value) ? value : fallback
|
||||
|
||||
export const IntervalValueWidget: React.FC<WidgetProps> = ({
|
||||
value,
|
||||
setValue,
|
||||
readonly,
|
||||
placeholder,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const parsedValue = parseIntervalTriggerValue(value)
|
||||
|
||||
const unitOptions = useMemo(
|
||||
() => [
|
||||
{ label: t("autoScore.intervalUnitMonth"), value: "month" },
|
||||
{ label: t("autoScore.intervalUnitDay"), value: "day" },
|
||||
{ label: t("autoScore.intervalUnitMinute"), value: "minute" },
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
const handleAmountChange = (nextAmount: number | null) => {
|
||||
if (nextAmount === null) {
|
||||
setValue(null)
|
||||
return
|
||||
}
|
||||
|
||||
const serializedValue = stringifyIntervalTriggerValue({
|
||||
amount: nextAmount,
|
||||
unit: parsedValue?.unit ?? DEFAULT_INTERVAL_UNIT,
|
||||
})
|
||||
setValue(serializedValue)
|
||||
}
|
||||
|
||||
const handleUnitChange = (nextUnit: IntervalUnit) => {
|
||||
const baseValue = parsedValue ?? getDefaultIntervalValue()
|
||||
const serializedValue = stringifyIntervalTriggerValue({
|
||||
amount: baseValue.amount,
|
||||
unit: nextUnit,
|
||||
})
|
||||
setValue(serializedValue)
|
||||
const updateValue = (patch: Partial<IntervalValue>) => {
|
||||
const nextValue = buildNextIntervalValue(parsedValue, patch)
|
||||
const serialized = stringifyIntervalTriggerValue(nextValue)
|
||||
setValue(serialized)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: 8, minWidth: 280 }}>
|
||||
<InputNumber
|
||||
min={1}
|
||||
precision={0}
|
||||
disabled={readonly}
|
||||
value={parsedValue?.amount}
|
||||
placeholder={placeholder || t("autoScore.intervalAmountPlaceholder")}
|
||||
onChange={handleAmountChange}
|
||||
style={{ width: 130 }}
|
||||
/>
|
||||
<Select
|
||||
disabled={readonly}
|
||||
value={parsedValue?.unit ?? DEFAULT_INTERVAL_UNIT}
|
||||
options={unitOptions}
|
||||
onChange={handleUnitChange}
|
||||
style={{ width: 140 }}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: 12, minWidth: 360 }}>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
|
||||
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{t("autoScore.intervalPartDay")}
|
||||
</span>
|
||||
<InputNumber
|
||||
min={0}
|
||||
precision={0}
|
||||
disabled={readonly}
|
||||
value={getDisplayValue(parsedValue?.days)}
|
||||
onChange={(next) => updateValue({ days: Math.max(0, Number(next ?? 0)) })}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
|
||||
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{t("autoScore.intervalPartHour")}
|
||||
</span>
|
||||
<InputNumber
|
||||
min={0}
|
||||
precision={0}
|
||||
disabled={readonly}
|
||||
value={getDisplayValue(parsedValue?.hours)}
|
||||
onChange={(next) => updateValue({ hours: Math.max(0, Number(next ?? 0)) })}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, minWidth: 96 }}>
|
||||
<span style={{ fontSize: 12, color: "var(--ss-text-secondary)" }}>
|
||||
{t("autoScore.intervalPartMinute")}
|
||||
</span>
|
||||
<InputNumber
|
||||
min={0}
|
||||
precision={0}
|
||||
disabled={readonly}
|
||||
value={getDisplayValue(parsedValue?.minutes)}
|
||||
onChange={(next) => updateValue({ minutes: Math.max(0, Number(next ?? 0)) })}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user