mirror of
https://github.com/langgenius/dify.git
synced 2026-08-15 04:59:46 +08:00
feat: add tool plugin param type date and date-picker (#36163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hjlarry <hjlarry@163.com>
This commit is contained in:
parent
b8934b743c
commit
df250423d4
@ -16,6 +16,8 @@ class CommonParameterType(StrEnum):
|
||||
TOOLS_SELECTOR = "array[tools]"
|
||||
CHECKBOX = "checkbox"
|
||||
ANY = auto()
|
||||
DATE = "date"
|
||||
DATE_RANGE = "date-range"
|
||||
|
||||
# Dynamic select parameter
|
||||
# Once you are not sure about the available options until authorization is done
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import json
|
||||
from datetime import date
|
||||
from enum import StrEnum, auto
|
||||
from typing import Any, Union
|
||||
|
||||
@ -46,6 +47,8 @@ class PluginParameterType(StrEnum):
|
||||
# MCP object and array type parameters
|
||||
ARRAY = CommonParameterType.ARRAY
|
||||
OBJECT = CommonParameterType.OBJECT
|
||||
DATE = CommonParameterType.DATE
|
||||
DATE_RANGE = CommonParameterType.DATE_RANGE
|
||||
|
||||
|
||||
class MCPServerParameterType(StrEnum):
|
||||
@ -92,15 +95,32 @@ class PluginParameter(BaseModel):
|
||||
|
||||
|
||||
def as_normal_type(typ: StrEnum):
|
||||
if typ.value == PluginParameterType.DATE_RANGE:
|
||||
return "object"
|
||||
if typ.value in {
|
||||
PluginParameterType.SECRET_INPUT,
|
||||
PluginParameterType.SELECT,
|
||||
PluginParameterType.CHECKBOX,
|
||||
PluginParameterType.DATE,
|
||||
}:
|
||||
return "string"
|
||||
return typ.value
|
||||
|
||||
|
||||
def _validate_date(value: Any, name: str = "date") -> str:
|
||||
if not isinstance(value, str):
|
||||
raise ValueError(f"The {name} parameter must be a string in YYYY-MM-DD format.")
|
||||
|
||||
try:
|
||||
parsed = date.fromisoformat(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"The {name} parameter must be a valid date in YYYY-MM-DD format.") from exc
|
||||
|
||||
if parsed.isoformat() != value:
|
||||
raise ValueError(f"The {name} parameter must use YYYY-MM-DD format.")
|
||||
return value
|
||||
|
||||
|
||||
def cast_parameter_value(typ: StrEnum, value: Any, /):
|
||||
try:
|
||||
match typ.value:
|
||||
@ -115,6 +135,34 @@ def cast_parameter_value(typ: StrEnum, value: Any, /):
|
||||
return ""
|
||||
else:
|
||||
return value if isinstance(value, str) else str(value)
|
||||
case PluginParameterType.DATE:
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
return _validate_date(value)
|
||||
case PluginParameterType.DATE_RANGE:
|
||||
if value is None or value == "":
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
out: dict[str, str] = {}
|
||||
for key in ("start", "end"):
|
||||
if key not in value or value[key] is None or value[key] == "":
|
||||
continue
|
||||
out[key] = _validate_date(value[key], f"date-range {key}")
|
||||
if out.get("start") and out.get("end") and out["start"] > out["end"]:
|
||||
raise ValueError("The date-range start date must not be after the end date.")
|
||||
return out
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed_value = json.loads(value)
|
||||
if isinstance(parsed_value, dict):
|
||||
return cast_parameter_value(typ, parsed_value)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
stripped = value.strip()
|
||||
if not stripped:
|
||||
return {}
|
||||
return {"start": _validate_date(stripped, "date-range start")}
|
||||
raise ValueError("The date-range parameter must be a JSON object, JSON string, or empty.")
|
||||
|
||||
case PluginParameterType.BOOLEAN:
|
||||
match value:
|
||||
|
||||
@ -311,6 +311,8 @@ class ToolParameter(PluginParameter):
|
||||
MODEL_SELECTOR = PluginParameterType.MODEL_SELECTOR
|
||||
ANY = PluginParameterType.ANY
|
||||
DYNAMIC_SELECT = PluginParameterType.DYNAMIC_SELECT
|
||||
DATE = PluginParameterType.DATE
|
||||
DATE_RANGE = PluginParameterType.DATE_RANGE
|
||||
|
||||
# MCP object and array type parameters
|
||||
ARRAY = MCPServerParameterType.ARRAY
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import pytest
|
||||
|
||||
from core.entities.parameter_entities import (
|
||||
AppSelectorScope,
|
||||
CommonParameterType,
|
||||
ModelSelectorScope,
|
||||
ToolSelectorScope,
|
||||
)
|
||||
from core.plugin.entities.parameters import PluginParameterType, cast_parameter_value
|
||||
|
||||
|
||||
def test_common_parameter_type_values_are_stable() -> None:
|
||||
@ -13,6 +16,42 @@ def test_common_parameter_type_values_are_stable() -> None:
|
||||
assert CommonParameterType.DYNAMIC_SELECT.value == "dynamic-select"
|
||||
assert CommonParameterType.ARRAY.value == "array"
|
||||
assert CommonParameterType.OBJECT.value == "object"
|
||||
assert CommonParameterType.DATE.value == "date"
|
||||
assert CommonParameterType.DATE_RANGE.value == "date-range"
|
||||
with pytest.raises(ValueError):
|
||||
PluginParameterType("date-picker")
|
||||
|
||||
|
||||
def test_cast_date_accepts_only_canonical_calendar_dates() -> None:
|
||||
assert cast_parameter_value(PluginParameterType.DATE, None) == ""
|
||||
assert cast_parameter_value(PluginParameterType.DATE, "") == ""
|
||||
assert cast_parameter_value(PluginParameterType.DATE, "2024-02-29") == "2024-02-29"
|
||||
|
||||
for value in ("a", "2024-02-30", "20240229", 20240229):
|
||||
with pytest.raises(ValueError):
|
||||
cast_parameter_value(PluginParameterType.DATE, value)
|
||||
|
||||
|
||||
def test_cast_date_range_validates_optional_range() -> None:
|
||||
assert cast_parameter_value(PluginParameterType.DATE_RANGE, "") == {}
|
||||
assert cast_parameter_value(PluginParameterType.DATE_RANGE, {}) == {}
|
||||
assert cast_parameter_value(PluginParameterType.DATE_RANGE, "2024-01-01") == {"start": "2024-01-01"}
|
||||
assert cast_parameter_value(PluginParameterType.DATE_RANGE, {"end": "2024-01-02"}) == {"end": "2024-01-02"}
|
||||
assert cast_parameter_value(
|
||||
PluginParameterType.DATE_RANGE,
|
||||
'{"start":"2024-01-01","end":"2024-01-02"}',
|
||||
) == {"start": "2024-01-01", "end": "2024-01-02"}
|
||||
|
||||
for value in (
|
||||
"a",
|
||||
{"start": "a"},
|
||||
{"start": "2024-02-30"},
|
||||
{"start": 20240101},
|
||||
{"start": "2024-01-02", "end": "2024-01-01"},
|
||||
'{"start":"2024-01-02","end":"2024-01-01"}',
|
||||
):
|
||||
with pytest.raises(ValueError):
|
||||
cast_parameter_value(PluginParameterType.DATE_RANGE, value)
|
||||
|
||||
|
||||
def test_selector_scope_values_are_stable() -> None:
|
||||
|
||||
@ -64,14 +64,6 @@
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx": {
|
||||
"jsx_a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx_a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
@ -2332,6 +2332,8 @@ export type ToolParameterType =
|
||||
| 'array'
|
||||
| 'boolean'
|
||||
| 'checkbox'
|
||||
| 'date'
|
||||
| 'date-range'
|
||||
| 'dynamic-select'
|
||||
| 'file'
|
||||
| 'files'
|
||||
|
||||
@ -2478,6 +2478,8 @@ export const zToolParameterType = z.enum([
|
||||
'array',
|
||||
'boolean',
|
||||
'checkbox',
|
||||
'date',
|
||||
'date-range',
|
||||
'dynamic-select',
|
||||
'file',
|
||||
'files',
|
||||
|
||||
@ -1,19 +1,10 @@
|
||||
'use client'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
DatePickerProps,
|
||||
TriggerProps,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import dayjs from 'dayjs'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import Picker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import DateRangePicker from '@/app/components/base/date-and-time-picker/date-range-picker'
|
||||
|
||||
type Props = Readonly<{
|
||||
start: Dayjs
|
||||
@ -24,29 +15,6 @@ type Props = Readonly<{
|
||||
|
||||
const today = dayjs()
|
||||
const DatePicker: FC<Props> = ({ start, end, onStartChange, onEndChange }) => {
|
||||
const locale = useLocale()
|
||||
|
||||
const renderDate = useCallback<NonNullable<DatePickerProps['renderTrigger']>>(
|
||||
(props, _state, { value, handleClickTrigger }: TriggerProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex h-7 cursor-pointer items-center rounded-lg px-1 system-sm-regular text-components-input-text-filled hover:bg-state-base-hover data-popup-open:bg-state-base-hover',
|
||||
props.className,
|
||||
)}
|
||||
onClick={(event) => {
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
>
|
||||
{value ? formatToLocalTime(value, locale, 'MMM D') : ''}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[locale],
|
||||
)
|
||||
|
||||
const availableStartDate = end.subtract(30, 'day')
|
||||
const startDateDisabled = useCallback(
|
||||
(date: Dayjs) => {
|
||||
@ -72,30 +40,14 @@ const DatePicker: FC<Props> = ({ start, end, onStartChange, onEndChange }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex h-8 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2">
|
||||
<div className="p-px">
|
||||
<RiCalendarLine className="size-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
<Picker
|
||||
value={start}
|
||||
onChange={onStartChange}
|
||||
renderTrigger={renderDate}
|
||||
needTimePicker={false}
|
||||
onClear={noop}
|
||||
noConfirm
|
||||
getIsDateDisabled={startDateDisabled}
|
||||
/>
|
||||
<span className="system-sm-regular text-text-tertiary">-</span>
|
||||
<Picker
|
||||
value={end}
|
||||
onChange={onEndChange}
|
||||
renderTrigger={renderDate}
|
||||
needTimePicker={false}
|
||||
onClear={noop}
|
||||
noConfirm
|
||||
getIsDateDisabled={endDateDisabled}
|
||||
/>
|
||||
</div>
|
||||
<DateRangePicker
|
||||
start={start}
|
||||
end={end}
|
||||
onStartChange={onStartChange}
|
||||
onEndChange={onEndChange}
|
||||
getIsStartDateDisabled={startDateDisabled}
|
||||
getIsEndDateDisabled={endDateDisabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatePicker)
|
||||
|
||||
@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import type { DatePickerProps, TriggerProps } from '../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
import DatePicker from '../date-picker'
|
||||
|
||||
type DateRangePickerProps = Readonly<{
|
||||
start?: Dayjs
|
||||
end?: Dayjs
|
||||
timezone?: string
|
||||
displayFormat?: string
|
||||
startPlaceholder?: string
|
||||
endPlaceholder?: string
|
||||
onStartChange: (date?: Dayjs) => void
|
||||
onEndChange: (date?: Dayjs) => void
|
||||
clearable?: boolean
|
||||
disabled?: boolean
|
||||
getIsStartDateDisabled?: DatePickerProps['getIsDateDisabled']
|
||||
getIsEndDateDisabled?: DatePickerProps['getIsDateDisabled']
|
||||
className?: string
|
||||
}>
|
||||
|
||||
const DateRangePicker: FC<DateRangePickerProps> = ({
|
||||
start,
|
||||
end,
|
||||
timezone,
|
||||
displayFormat = 'MMM D',
|
||||
startPlaceholder,
|
||||
endPlaceholder,
|
||||
onStartChange,
|
||||
onEndChange,
|
||||
clearable = false,
|
||||
disabled = false,
|
||||
getIsStartDateDisabled,
|
||||
getIsEndDateDisabled,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
|
||||
const renderDate = (
|
||||
placeholder: string | undefined,
|
||||
onChange: (date?: Dayjs) => void,
|
||||
): NonNullable<DatePickerProps['renderTrigger']> => {
|
||||
return (props, _state, { value, handleClear, handleClickTrigger }: TriggerProps) => {
|
||||
const displayValue = value ? formatToLocalTime(value, locale, displayFormat) : ''
|
||||
const triggerLabel = placeholder
|
||||
? `${placeholder}${displayValue ? `: ${displayValue}` : ''}`
|
||||
: displayValue
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
aria-disabled={disabled || undefined}
|
||||
aria-label={triggerLabel}
|
||||
className={cn(
|
||||
'group/date-trigger relative flex h-7 min-w-0 cursor-pointer items-center rounded-lg px-1 system-sm-regular text-components-input-text-filled hover:bg-state-base-hover data-popup-open:bg-state-base-hover',
|
||||
disabled && 'cursor-default hover:bg-transparent',
|
||||
props.className,
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={(event) => {
|
||||
if (disabled) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (disabled) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
props.onKeyDown?.(event)
|
||||
}}
|
||||
>
|
||||
<span className={cn('truncate', !value && 'text-components-input-text-placeholder')}>
|
||||
{displayValue || placeholder}
|
||||
</span>
|
||||
{clearable && value && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
placeholder
|
||||
? `${placeholder}: ${t(($) => $['operation.clear'], { ns: 'common' })}`
|
||||
: t(($) => $['operation.clear'], { ns: 'common' })
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-1 z-[1] flex size-4 -translate-y-1/2 items-center justify-center rounded border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg text-text-tertiary opacity-0 shadow-xs group-focus-within/date-trigger:pointer-events-auto group-focus-within/date-trigger:opacity-100 group-hover/date-trigger:pointer-events-auto group-hover/date-trigger:opacity-100 hover:bg-components-button-secondary-bg-hover hover:text-text-secondary [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100"
|
||||
onClick={(event) => {
|
||||
handleClear(event)
|
||||
onChange(undefined)
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 min-w-0 items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2',
|
||||
disabled && 'opacity-60',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 p-px">
|
||||
<span className="i-ri-calendar-line block size-3.5 text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
<DatePicker
|
||||
value={start}
|
||||
timezone={timezone}
|
||||
onChange={onStartChange}
|
||||
onClear={noop}
|
||||
renderTrigger={renderDate(startPlaceholder, onStartChange)}
|
||||
triggerWrapClassName="min-w-0"
|
||||
needTimePicker={false}
|
||||
noConfirm
|
||||
getIsDateDisabled={getIsStartDateDisabled}
|
||||
/>
|
||||
<span className="shrink-0 system-sm-regular text-text-tertiary" aria-hidden="true">
|
||||
-
|
||||
</span>
|
||||
<DatePicker
|
||||
value={end}
|
||||
timezone={timezone}
|
||||
onChange={onEndChange}
|
||||
onClear={noop}
|
||||
renderTrigger={renderDate(endPlaceholder, onEndChange)}
|
||||
triggerWrapClassName="min-w-0"
|
||||
needTimePicker={false}
|
||||
noConfirm
|
||||
getIsDateDisabled={getIsEndDateDisabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DateRangePicker)
|
||||
@ -26,6 +26,8 @@ export enum FormTypeEnum {
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
date = 'date',
|
||||
dateRange = 'date-range',
|
||||
}
|
||||
|
||||
export type FormOption = {
|
||||
|
||||
@ -22,10 +22,14 @@ describe('reasoning-config-form helpers', () => {
|
||||
expect(getVarKindType(FormTypeEnum.files)).toBe(VarKindType.variable)
|
||||
expect(getVarKindType(FormTypeEnum.textNumber)).toBe(VarKindType.constant)
|
||||
expect(getVarKindType(FormTypeEnum.textInput)).toBe(VarKindType.mixed)
|
||||
expect(getVarKindType(FormTypeEnum.date)).toBe(VarKindType.constant)
|
||||
expect(getVarKindType(FormTypeEnum.dateRange)).toBe(VarKindType.constant)
|
||||
expect(getVarKindType(FormTypeEnum.dynamicSelect)).toBeUndefined()
|
||||
|
||||
expect(resolveTargetVarType(FormTypeEnum.textInput)).toBe(VarType.string)
|
||||
expect(resolveTargetVarType(FormTypeEnum.textNumber)).toBe(VarType.number)
|
||||
expect(resolveTargetVarType(FormTypeEnum.date)).toBe(VarType.string)
|
||||
expect(resolveTargetVarType(FormTypeEnum.dateRange)).toBe(VarType.string)
|
||||
expect(resolveTargetVarType(FormTypeEnum.files)).toBe(VarType.arrayFile)
|
||||
expect(resolveTargetVarType(FormTypeEnum.file)).toBe(VarType.file)
|
||||
expect(resolveTargetVarType(FormTypeEnum.checkbox)).toBe(VarType.boolean)
|
||||
@ -44,6 +48,26 @@ describe('reasoning-config-form helpers', () => {
|
||||
expect(fileFilter?.({ type: VarType.arrayFile } as never)).toBe(true)
|
||||
})
|
||||
|
||||
it('creates variable filters for date fields', () => {
|
||||
const dateFilter = createPickerProps({
|
||||
type: FormTypeEnum.date,
|
||||
value: {},
|
||||
language: 'en_US',
|
||||
schema: { type: FormTypeEnum.date } as never,
|
||||
}).filterVar
|
||||
expect(dateFilter?.({ type: VarType.string } as never)).toBe(true)
|
||||
expect(dateFilter?.({ type: VarType.number } as never)).toBe(true)
|
||||
expect(dateFilter?.({ type: VarType.file } as never)).toBe(false)
|
||||
expect(
|
||||
createPickerProps({
|
||||
type: FormTypeEnum.dateRange,
|
||||
value: {},
|
||||
language: 'en_US',
|
||||
schema: { type: FormTypeEnum.dateRange } as never,
|
||||
}).filterVar,
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('filters select options based on show_on conditions', () => {
|
||||
const options = [
|
||||
{
|
||||
@ -152,6 +176,23 @@ describe('reasoning-config-form helpers', () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(getFieldFlags(FormTypeEnum.date)).toEqual(
|
||||
expect.objectContaining({
|
||||
isDate: true,
|
||||
isDateRange: false,
|
||||
isString: false,
|
||||
showTypeSwitch: true,
|
||||
}),
|
||||
)
|
||||
expect(getFieldFlags(FormTypeEnum.dateRange)).toEqual(
|
||||
expect.objectContaining({
|
||||
isDate: false,
|
||||
isDateRange: true,
|
||||
isString: false,
|
||||
showTypeSwitch: false,
|
||||
}),
|
||||
)
|
||||
|
||||
expect(
|
||||
createPickerProps({
|
||||
type: FormTypeEnum.select,
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { AppSelectorValue } from '@/app/components/plugins/plugin-detail-panel/app-selector'
|
||||
import type { ToolFormSchema } from '@/app/components/tools/utils/to-form-schema'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { Type } from '@/app/components/workflow/nodes/llm/types'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import ReasoningConfigForm from '../reasoning-config-form'
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
|
||||
@ -29,6 +29,8 @@ export const getVarKindType = (type: string) => {
|
||||
FormTypeEnum.textNumber,
|
||||
FormTypeEnum.array,
|
||||
FormTypeEnum.object,
|
||||
FormTypeEnum.date,
|
||||
FormTypeEnum.dateRange,
|
||||
].includes(type as FormTypeEnum)
|
||||
)
|
||||
return VarKindType.constant
|
||||
@ -54,7 +56,11 @@ export const createFilterVar = (type: string) => {
|
||||
if (type === FormTypeEnum.textNumber)
|
||||
return (varPayload: Var) => varPayload.type === VarType.number
|
||||
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput)
|
||||
if (
|
||||
type === FormTypeEnum.textInput ||
|
||||
type === FormTypeEnum.secretInput ||
|
||||
type === FormTypeEnum.date
|
||||
)
|
||||
return (varPayload: Var) =>
|
||||
[VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
|
||||
@ -166,6 +172,8 @@ export const updateVariableSelectorValue = (
|
||||
}
|
||||
|
||||
export const getFieldFlags = (type: string, varInput?: ReasoningConfigInputValue) => {
|
||||
const isDateRange = type === FormTypeEnum.dateRange
|
||||
const isDate = type === FormTypeEnum.date
|
||||
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isObject = type === FormTypeEnum.object
|
||||
@ -188,7 +196,9 @@ export const getFieldFlags = (type: string, varInput?: ReasoningConfigInputValue
|
||||
isSelect,
|
||||
isAppSelector,
|
||||
isModelSelector,
|
||||
showTypeSwitch: isNumber || isObject || isArray,
|
||||
isDate,
|
||||
isDateRange,
|
||||
showTypeSwitch: isNumber || isObject || isArray || isDate,
|
||||
isConstant,
|
||||
showVariableSelector: isFile || varInput?.type === VarKindType.variable,
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
import { Switch } from '@langgenius/dify-ui/switch'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
@ -29,7 +30,10 @@ import FormInputTypeSwitch from '@/app/components/workflow/nodes/_base/component
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import ToolDatePicker from '@/app/components/workflow/nodes/tool/components/tool-date-picker'
|
||||
import ToolDateRangePicker from '@/app/components/workflow/nodes/tool/components/tool-date-range-picker'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { userProfileAtom } from '@/context/account-state'
|
||||
import {
|
||||
createPickerProps,
|
||||
getFieldFlags,
|
||||
@ -64,6 +68,8 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useLanguage()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const timezone = userProfile.timezone ?? 'UTC'
|
||||
|
||||
const handleAutomatic = (key: string, val: boolean, type: string) => {
|
||||
onChange(updateInputAutoState(value, key, val, type))
|
||||
@ -153,6 +159,8 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
isSelect,
|
||||
isAppSelector,
|
||||
isModelSelector,
|
||||
isDate,
|
||||
isDateRange,
|
||||
showTypeSwitch,
|
||||
isConstant,
|
||||
showVariableSelector,
|
||||
@ -240,6 +248,25 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isDate && isConstant && (
|
||||
<div className="min-w-0 grow">
|
||||
<ToolDatePicker
|
||||
value={typeof varInput?.value === 'string' ? varInput.value : ''}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
timezone={timezone}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDateRange && varInput?.type !== VarKindType.variable && (
|
||||
<div className="grow">
|
||||
<ToolDateRangePicker
|
||||
value={varInput?.value}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
timezone={timezone}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isBoolean && (
|
||||
<FormInputBoolean
|
||||
value={varInput?.value as boolean}
|
||||
|
||||
@ -188,6 +188,12 @@ const correctInitialData = (
|
||||
target.value = Number.parseFloat(defaultValue)
|
||||
}
|
||||
|
||||
if (type === FormTypeEnum.date || type === FormTypeEnum.dateRange) {
|
||||
if (typeof defaultValue === 'string') target.value = defaultValue
|
||||
if (typeof defaultValue === 'object' && defaultValue !== null && !Array.isArray(defaultValue))
|
||||
target.value = JSON.stringify(defaultValue)
|
||||
}
|
||||
|
||||
if (type === 'app-selector' || type === 'model-selector') target.value = defaultValue
|
||||
|
||||
return target
|
||||
@ -271,7 +277,9 @@ const getVarKindType = (type: FormTypeEnum) => {
|
||||
if (
|
||||
type === FormTypeEnum.select ||
|
||||
type === FormTypeEnum.checkbox ||
|
||||
type === FormTypeEnum.textNumber
|
||||
type === FormTypeEnum.textNumber ||
|
||||
type === FormTypeEnum.date ||
|
||||
type === FormTypeEnum.dateRange
|
||||
)
|
||||
return VarKindType.constant
|
||||
if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) return VarKindType.mixed
|
||||
|
||||
@ -123,6 +123,26 @@ describe('form-input-item helpers', () => {
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should expose date field state', () => {
|
||||
const dateState = getFormInputState(createSchema({ type: FormTypeEnum.date }), {
|
||||
type: VarKindType.constant,
|
||||
value: '2024-01-01',
|
||||
})
|
||||
expect(dateState.isDate).toBe(true)
|
||||
expect(dateState.isDateRange).toBe(false)
|
||||
expect(getTargetVarType(dateState)).toBe(VarType.string)
|
||||
expect(getVarKindType(dateState)).toBe(VarKindType.constant)
|
||||
expect(getFilterVar(dateState)?.({ type: VarType.string } as Var)).toBe(true)
|
||||
|
||||
const rangeState = getFormInputState(createSchema({ type: FormTypeEnum.dateRange }), {
|
||||
type: VarKindType.constant,
|
||||
value: '{}',
|
||||
})
|
||||
expect(rangeState.isDateRange).toBe(true)
|
||||
expect(rangeState.isDate).toBe(false)
|
||||
expect(getVarKindType(rangeState)).toBe(VarKindType.constant)
|
||||
})
|
||||
|
||||
it('should filter and map visible options using show_on rules', () => {
|
||||
const options = [
|
||||
createOption('always'),
|
||||
|
||||
@ -49,6 +49,8 @@ type FormInputState = {
|
||||
isBoolean: boolean
|
||||
isCheckbox: boolean
|
||||
isConstant: boolean
|
||||
isDate: boolean
|
||||
isDateRange: boolean
|
||||
isDynamicSelect: boolean
|
||||
isFile: boolean
|
||||
isFiles: boolean
|
||||
@ -92,6 +94,8 @@ export const getFormInputState = (
|
||||
_type,
|
||||
} = schema
|
||||
|
||||
const isDateRange = type === FormTypeEnum.dateRange
|
||||
const isDate = type === FormTypeEnum.date
|
||||
const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const isNumber = type === FormTypeEnum.textNumber
|
||||
const isObject = type === FormTypeEnum.object
|
||||
@ -105,7 +109,7 @@ export const getFormInputState = (
|
||||
const isDynamicSelect = type === FormTypeEnum.dynamicSelect
|
||||
const isAppSelector = type === FormTypeEnum.appSelector
|
||||
const isModelSelector = type === FormTypeEnum.modelSelector
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect
|
||||
const showTypeSwitch = isNumber || isBoolean || isObject || isArray || isSelect || isDate
|
||||
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
|
||||
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
|
||||
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
|
||||
@ -117,6 +121,8 @@ export const getFormInputState = (
|
||||
isBoolean,
|
||||
isCheckbox,
|
||||
isConstant,
|
||||
isDate,
|
||||
isDateRange,
|
||||
isDynamicSelect,
|
||||
isFile,
|
||||
isFiles,
|
||||
@ -152,7 +158,7 @@ export const getFilterVar = (state: FormInputState) => {
|
||||
if (state.isNumber) return (varPayload: Var) => varPayload.type === VarType.number
|
||||
if (state.isSelect && state.isMultipleSelect)
|
||||
return (varPayload: Var) => [VarType.array, VarType.arrayString].includes(varPayload.type)
|
||||
if (state.isString)
|
||||
if (state.isString || state.isDate)
|
||||
return (varPayload: Var) =>
|
||||
[VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
if (state.isFile)
|
||||
@ -175,7 +181,9 @@ export const getVarKindType = (state: FormInputState) => {
|
||||
state.isBoolean ||
|
||||
state.isNumber ||
|
||||
state.isArray ||
|
||||
state.isObject
|
||||
state.isObject ||
|
||||
state.isDate ||
|
||||
state.isDateRange
|
||||
)
|
||||
return VarKindType.constant
|
||||
if (state.isString) return VarKindType.mixed
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { CheckboxList } from '@/app/components/base/checkbox-list'
|
||||
import Input from '@/app/components/base/input'
|
||||
@ -28,7 +29,10 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
|
||||
import ToolDatePicker from '@/app/components/workflow/nodes/tool/components/tool-date-picker'
|
||||
import ToolDateRangePicker from '@/app/components/workflow/nodes/tool/components/tool-date-range-picker'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { userProfileAtom } from '@/context/account-state'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
import { VarKindType } from '../types'
|
||||
@ -87,8 +91,11 @@ const FormInputItem: FC<Props> = ({
|
||||
extraParams,
|
||||
providerType,
|
||||
disableVariableInsertion = false,
|
||||
inPanel,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const userProfile = useAtomValue(userProfileAtom)
|
||||
const timezone = userProfile.timezone ?? 'UTC'
|
||||
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
|
||||
|
||||
@ -108,6 +115,8 @@ const FormInputItem: FC<Props> = ({
|
||||
isBoolean,
|
||||
isCheckbox,
|
||||
isConstant,
|
||||
isDate,
|
||||
isDateRange,
|
||||
isDynamicSelect,
|
||||
isModelSelector,
|
||||
isMultipleSelect,
|
||||
@ -224,12 +233,14 @@ const FormInputItem: FC<Props> = ({
|
||||
|
||||
const handleValueChange = (newValue: FormInputValue) => {
|
||||
const nextType = getVarKindType(formState) ?? varInput?.type ?? VarKindType.constant
|
||||
let nextValue: FormInputValue = newValue
|
||||
if (isNumber) nextValue = Number.parseFloat(String(newValue ?? ''))
|
||||
onChange({
|
||||
...value,
|
||||
[variable]: {
|
||||
...varInput,
|
||||
type: nextType,
|
||||
value: isNumber ? Number.parseFloat(String(newValue ?? '')) : newValue,
|
||||
value: nextValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -338,6 +349,28 @@ const FormInputItem: FC<Props> = ({
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
{isDate && isConstant && (
|
||||
<div className="min-w-0 grow">
|
||||
<ToolDatePicker
|
||||
value={varInput?.value}
|
||||
onChange={handleValueChange}
|
||||
timezone={timezone}
|
||||
readOnly={readOnly}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isDateRange && varInput?.type !== VarKindType.variable && (
|
||||
<div className="min-w-0 grow">
|
||||
<ToolDateRangePicker
|
||||
value={varInput?.value}
|
||||
onChange={handleValueChange}
|
||||
readOnly={readOnly}
|
||||
timezone={timezone}
|
||||
inPanel={inPanel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isCheckbox && isConstant && (
|
||||
<CheckboxList
|
||||
name={variable}
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import ToolDatePicker from '../tool-date-picker'
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock({
|
||||
'time.dateFormats.display': 'MMMM D, YYYY',
|
||||
'time.operation.pickDate': 'Select date',
|
||||
'common.operation.clear': 'Clear',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ToolDatePicker', () => {
|
||||
it('preserves a stored date-only value when rendering in a negative timezone', () => {
|
||||
render(
|
||||
<ToolDatePicker
|
||||
value="2024-05-01"
|
||||
onChange={vi.fn()}
|
||||
timezone="America/Los_Angeles"
|
||||
placeholder="Select date"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Select date: May 1, 2024' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('returns an empty string when clearing the date', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<ToolDatePicker
|
||||
value="2024-05-01"
|
||||
onChange={onChange}
|
||||
timezone="UTC"
|
||||
placeholder="Select date"
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Clear' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('disables interaction and clearing when read only', () => {
|
||||
render(
|
||||
<ToolDatePicker
|
||||
value="2024-05-01"
|
||||
onChange={vi.fn()}
|
||||
timezone="UTC"
|
||||
placeholder="Select date"
|
||||
readOnly
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Select date: May 1, 2024' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true',
|
||||
)
|
||||
expect(screen.queryByRole('button', { name: 'Clear' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,65 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import ToolDateRangePicker from '../tool-date-range-picker'
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return {
|
||||
...actual,
|
||||
...createReactI18nextMock({
|
||||
'time.dateFormats.display': 'MMMM D, YYYY',
|
||||
'time.defaultPlaceholder': 'Select date',
|
||||
'common.operation.clear': 'Clear',
|
||||
'workflow.nodes.tool.dateRange.start': 'Start',
|
||||
'workflow.nodes.tool.dateRange.end': 'End',
|
||||
'workflow.nodes.tool.dateRange.startPlaceholder': 'Start date',
|
||||
'workflow.nodes.tool.dateRange.endPlaceholder': 'End date',
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ToolDateRangePicker', () => {
|
||||
it('preserves stored date-only values when rendering in a negative timezone', () => {
|
||||
render(
|
||||
<ToolDateRangePicker
|
||||
value={JSON.stringify({ start: '2024-05-01', end: '2024-05-03' })}
|
||||
onChange={vi.fn()}
|
||||
timezone="America/Los_Angeles"
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Start date: May 1, 2024' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'End date: May 3, 2024' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clears one boundary without changing the other', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<ToolDateRangePicker
|
||||
value={JSON.stringify({ start: '2024-05-01', end: '2024-05-03' })}
|
||||
onChange={onChange}
|
||||
timezone="UTC"
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Start date: Clear' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(JSON.stringify({ end: '2024-05-03' }))
|
||||
})
|
||||
|
||||
it('returns an empty value when clearing the last boundary', () => {
|
||||
const onChange = vi.fn()
|
||||
render(
|
||||
<ToolDateRangePicker
|
||||
value={JSON.stringify({ start: '2024-05-01' })}
|
||||
onChange={onChange}
|
||||
timezone="UTC"
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Start date: Clear' }))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import type {
|
||||
DatePickerProps,
|
||||
TriggerProps,
|
||||
} from '@/app/components/base/date-and-time-picker/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
|
||||
import { toDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { formatToLocalTime } from '@/utils/format'
|
||||
|
||||
const DATE_FMT = 'YYYY-MM-DD'
|
||||
|
||||
type Props = Readonly<{
|
||||
value: unknown
|
||||
onChange: (next: string) => void
|
||||
timezone: string
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
}>
|
||||
|
||||
const ToolDatePicker: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
timezone,
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const displayFormat = t(($) => $['dateFormats.display'], { ns: 'time' })
|
||||
const resolvedPlaceholder = placeholder || t(($) => $['operation.pickDate'], { ns: 'time' })
|
||||
const rawValue = typeof value === 'string' ? value : ''
|
||||
const parsedValue = toDayjs(rawValue, { timezone, format: DATE_FMT })
|
||||
|
||||
const renderTrigger = useCallback<NonNullable<DatePickerProps['renderTrigger']>>(
|
||||
(props, _state, { value: date, handleClear, handleClickTrigger }: TriggerProps) => {
|
||||
const displayValue = date ? formatToLocalTime(date, locale, displayFormat) : ''
|
||||
const triggerLabel = resolvedPlaceholder
|
||||
? `${resolvedPlaceholder}${displayValue ? `: ${displayValue}` : ''}`
|
||||
: displayValue
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
aria-disabled={readOnly || undefined}
|
||||
aria-label={triggerLabel}
|
||||
className={cn(
|
||||
'group/date-trigger relative flex h-8 min-w-0 grow cursor-pointer items-center space-x-0.5 rounded-lg bg-components-input-bg-normal px-2 system-sm-regular text-components-input-text-filled hover:bg-state-base-hover data-popup-open:bg-state-base-hover',
|
||||
readOnly && 'cursor-default opacity-60 hover:bg-components-input-bg-normal',
|
||||
props.className,
|
||||
)}
|
||||
role="button"
|
||||
tabIndex={readOnly ? -1 : 0}
|
||||
onClick={(event) => {
|
||||
if (readOnly) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
handleClickTrigger(event)
|
||||
props.onClick?.(event)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (readOnly) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
props.onKeyDown?.(event)
|
||||
}}
|
||||
>
|
||||
<div className="shrink-0 p-px">
|
||||
<span
|
||||
className="i-ri-calendar-line block size-3.5 text-text-tertiary"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate px-1',
|
||||
!date && 'text-components-input-text-placeholder',
|
||||
date && 'pr-5',
|
||||
)}
|
||||
>
|
||||
{displayValue || resolvedPlaceholder}
|
||||
</span>
|
||||
{date && !readOnly && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $['operation.clear'], { ns: 'common' })}
|
||||
className="pointer-events-none absolute top-1/2 right-2 z-[1] flex size-4 -translate-y-1/2 items-center justify-center rounded border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg text-text-tertiary opacity-0 shadow-xs group-focus-within/date-trigger:pointer-events-auto group-focus-within/date-trigger:opacity-100 group-hover/date-trigger:pointer-events-auto group-hover/date-trigger:opacity-100 hover:bg-components-button-secondary-bg-hover hover:text-text-secondary [@media(hover:none)]:pointer-events-auto [@media(hover:none)]:opacity-100"
|
||||
onClick={(event) => {
|
||||
handleClear(event)
|
||||
onChange('')
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill size-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
[displayFormat, locale, onChange, readOnly, resolvedPlaceholder, t],
|
||||
)
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
value={parsedValue}
|
||||
timezone={timezone}
|
||||
onChange={(date) => onChange(date?.format(DATE_FMT) ?? '')}
|
||||
onClear={noop}
|
||||
renderTrigger={renderTrigger}
|
||||
triggerWrapClassName="w-full min-w-0"
|
||||
needTimePicker={false}
|
||||
noConfirm
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolDatePicker
|
||||
@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DateRangePicker from '@/app/components/base/date-and-time-picker/date-range-picker'
|
||||
import { toDayjs as parseDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import { parseToolDateRangeValue, stringifyToolDateRangeValue } from './tool-date-range-value'
|
||||
|
||||
const DATE_FMT = 'YYYY-MM-DD'
|
||||
|
||||
const toDayjs = (raw?: string, timezone?: string): Dayjs | undefined => {
|
||||
if (!raw) return undefined
|
||||
return parseDayjs(raw, { timezone, format: DATE_FMT })
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: unknown
|
||||
onChange: (next: string) => void
|
||||
readOnly?: boolean
|
||||
timezone: string
|
||||
inPanel?: boolean
|
||||
}
|
||||
|
||||
const ToolDateRangePicker: FC<Props> = ({ value, onChange, readOnly = false, timezone }) => {
|
||||
const { t } = useTranslation()
|
||||
const parsed = parseToolDateRangeValue(value)
|
||||
|
||||
const patch = (partial: Partial<{ start?: string; end?: string }>) => {
|
||||
const next = { ...parsed, ...partial }
|
||||
if (!next.start) delete next.start
|
||||
if (!next.end) delete next.end
|
||||
onChange(stringifyToolDateRangeValue(next))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<DateRangePicker
|
||||
className="max-w-full"
|
||||
start={toDayjs(parsed.start, timezone)}
|
||||
end={toDayjs(parsed.end, timezone)}
|
||||
timezone={timezone}
|
||||
displayFormat={t(($) => $['dateFormats.display'], { ns: 'time' })}
|
||||
startPlaceholder={t(($) => $['nodes.tool.dateRange.startPlaceholder'], { ns: 'workflow' })}
|
||||
endPlaceholder={t(($) => $['nodes.tool.dateRange.endPlaceholder'], { ns: 'workflow' })}
|
||||
onStartChange={(date) => patch({ start: date?.format(DATE_FMT) })}
|
||||
onEndChange={(date) => patch({ end: date?.format(DATE_FMT) })}
|
||||
clearable
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolDateRangePicker
|
||||
@ -0,0 +1,36 @@
|
||||
export type ToolDateRangeStored = {
|
||||
start?: string
|
||||
end?: string
|
||||
}
|
||||
|
||||
export function parseToolDateRangeValue(raw: unknown): ToolDateRangeStored {
|
||||
if (raw === null || raw === undefined || raw === '') return {}
|
||||
if (typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
const o = raw as Record<string, unknown>
|
||||
return {
|
||||
start: typeof o.start === 'string' && o.start ? o.start : undefined,
|
||||
end: typeof o.end === 'string' && o.end ? o.end : undefined,
|
||||
}
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
const trimmed = raw.trim()
|
||||
if (!trimmed) return {}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed))
|
||||
return parseToolDateRangeValue(parsed)
|
||||
} catch {
|
||||
// legacy single-day value from the earlier date-range implementation
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return { start: trimmed, end: undefined }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function stringifyToolDateRangeValue(v: ToolDateRangeStored): string {
|
||||
const out: ToolDateRangeStored = {}
|
||||
if (v.start) out.start = v.start
|
||||
if (v.end) out.end = v.end
|
||||
if (!out.start && !out.end) return ''
|
||||
return JSON.stringify(out)
|
||||
}
|
||||
@ -80,7 +80,12 @@ const ToolFormItem: FC<Props> = ({
|
||||
const language = useLanguage()
|
||||
const { name, label, type, required, tooltip, input_schema } = schema
|
||||
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
|
||||
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const showDescription =
|
||||
type === FormTypeEnum.textInput ||
|
||||
type === FormTypeEnum.textNumber ||
|
||||
type === FormTypeEnum.secretInput ||
|
||||
type === FormTypeEnum.date ||
|
||||
type === FormTypeEnum.dateRange
|
||||
const [isShowSchema, setIsShowSchema] = useState(false)
|
||||
return (
|
||||
<div className="space-y-0.5 py-1">
|
||||
|
||||
@ -40,7 +40,12 @@ const TriggerFormItem: FC<Props> = ({
|
||||
const language = useLanguage()
|
||||
const { name, label, type, required, tooltip, input_schema } = schema
|
||||
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
|
||||
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
|
||||
const showDescription =
|
||||
type === FormTypeEnum.textInput ||
|
||||
type === FormTypeEnum.textNumber ||
|
||||
type === FormTypeEnum.secretInput ||
|
||||
type === FormTypeEnum.date ||
|
||||
type === FormTypeEnum.dateRange
|
||||
const [isShowSchema, setIsShowSchema] = useState(false)
|
||||
return (
|
||||
<div className="space-y-0.5 py-1">
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "المحتوى المحول",
|
||||
"nodes.tool.authorizationRequired": "التفويض مطلوب",
|
||||
"nodes.tool.authorize": "تخويل",
|
||||
"nodes.tool.dateRange.end": "الانتهاء",
|
||||
"nodes.tool.dateRange.endPlaceholder": "تاريخ الانتهاء",
|
||||
"nodes.tool.dateRange.start": "البدء",
|
||||
"nodes.tool.dateRange.startPlaceholder": "تاريخ البدء",
|
||||
"nodes.tool.inputVars": "متغيرات الإدخال",
|
||||
"nodes.tool.insertPlaceholder1": "اكتب أو اضغط",
|
||||
"nodes.tool.insertPlaceholder2": "لإدراج متغير",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Transformierter Inhalt",
|
||||
"nodes.tool.authorizationRequired": "Autorisierung erforderlich",
|
||||
"nodes.tool.authorize": "Autorisieren",
|
||||
"nodes.tool.dateRange.end": "Ende",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Enddatum",
|
||||
"nodes.tool.dateRange.start": "Start",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Startdatum",
|
||||
"nodes.tool.inputVars": "Eingabevariablen",
|
||||
"nodes.tool.insertPlaceholder1": "Tippen oder drücken",
|
||||
"nodes.tool.insertPlaceholder2": "Fügen Sie die Variable ein.",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Transformed content",
|
||||
"nodes.tool.authorizationRequired": "Authorization required",
|
||||
"nodes.tool.authorize": "Authorize",
|
||||
"nodes.tool.dateRange.end": "End",
|
||||
"nodes.tool.dateRange.endPlaceholder": "End date",
|
||||
"nodes.tool.dateRange.start": "Start",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Start date",
|
||||
"nodes.tool.inputVars": "Input Variables",
|
||||
"nodes.tool.insertPlaceholder1": "Type or press",
|
||||
"nodes.tool.insertPlaceholder2": "insert variable",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Contenido transformado",
|
||||
"nodes.tool.authorizationRequired": "Autorización requerida",
|
||||
"nodes.tool.authorize": "autorizar",
|
||||
"nodes.tool.dateRange.end": "Fin",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Fecha de fin",
|
||||
"nodes.tool.dateRange.start": "Inicio",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Fecha de inicio",
|
||||
"nodes.tool.inputVars": "Variables de entrada",
|
||||
"nodes.tool.insertPlaceholder1": "Escribe o presiona",
|
||||
"nodes.tool.insertPlaceholder2": "insertar variable",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "محتوای تبدیلشده",
|
||||
"nodes.tool.authorizationRequired": "احراز هویت الزامی است",
|
||||
"nodes.tool.authorize": "مجوزدهی",
|
||||
"nodes.tool.dateRange.end": "پایان",
|
||||
"nodes.tool.dateRange.endPlaceholder": "تاریخ پایان",
|
||||
"nodes.tool.dateRange.start": "شروع",
|
||||
"nodes.tool.dateRange.startPlaceholder": "تاریخ شروع",
|
||||
"nodes.tool.inputVars": "متغیرهای ورودی",
|
||||
"nodes.tool.insertPlaceholder1": "تایپ کنید یا فشار دهید",
|
||||
"nodes.tool.insertPlaceholder2": "متغیر را وارد کنید",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Contenu transformé",
|
||||
"nodes.tool.authorizationRequired": "Autorisation requise",
|
||||
"nodes.tool.authorize": "Autoriser",
|
||||
"nodes.tool.dateRange.end": "Fin",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Date de fin",
|
||||
"nodes.tool.dateRange.start": "Début",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Date de début",
|
||||
"nodes.tool.inputVars": "Variables de saisie",
|
||||
"nodes.tool.insertPlaceholder1": "Tapez ou appuyez",
|
||||
"nodes.tool.insertPlaceholder2": "insérer une variable",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "रूपांतरित सामग्री",
|
||||
"nodes.tool.authorizationRequired": "प्राधिकरण आवश्यक",
|
||||
"nodes.tool.authorize": "अधिकृत करें",
|
||||
"nodes.tool.dateRange.end": "समाप्ति",
|
||||
"nodes.tool.dateRange.endPlaceholder": "समाप्ति तिथि",
|
||||
"nodes.tool.dateRange.start": "प्रारंभ",
|
||||
"nodes.tool.dateRange.startPlaceholder": "प्रारंभ तिथि",
|
||||
"nodes.tool.inputVars": "इनपुट वेरिएबल्स",
|
||||
"nodes.tool.insertPlaceholder1": "टाइप करें या दबाएँ",
|
||||
"nodes.tool.insertPlaceholder2": "वेरिएबल डालें",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Konten yang diubah",
|
||||
"nodes.tool.authorizationRequired": "Otorisasi diperlukan",
|
||||
"nodes.tool.authorize": "Otorisasi",
|
||||
"nodes.tool.dateRange.end": "Berakhir",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Tanggal berakhir",
|
||||
"nodes.tool.dateRange.start": "Mulai",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Tanggal mulai",
|
||||
"nodes.tool.inputVars": "Variabel Masukan",
|
||||
"nodes.tool.insertPlaceholder1": "Ketik atau tekan",
|
||||
"nodes.tool.insertPlaceholder2": "Sisipkan variabel",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Contenuto trasformato",
|
||||
"nodes.tool.authorizationRequired": "Autorizzazione richiesta",
|
||||
"nodes.tool.authorize": "Autorizza",
|
||||
"nodes.tool.dateRange.end": "Fine",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Data di fine",
|
||||
"nodes.tool.dateRange.start": "Inizio",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Data di inizio",
|
||||
"nodes.tool.inputVars": "Variabili di Input",
|
||||
"nodes.tool.insertPlaceholder1": "Digita o premi",
|
||||
"nodes.tool.insertPlaceholder2": "inserisci variabile",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "変換されたコンテンツ",
|
||||
"nodes.tool.authorizationRequired": "認証が必要です",
|
||||
"nodes.tool.authorize": "認証する",
|
||||
"nodes.tool.dateRange.end": "終了",
|
||||
"nodes.tool.dateRange.endPlaceholder": "終了日",
|
||||
"nodes.tool.dateRange.start": "開始",
|
||||
"nodes.tool.dateRange.startPlaceholder": "開始日",
|
||||
"nodes.tool.inputVars": "入力変数",
|
||||
"nodes.tool.insertPlaceholder1": "タイプするか押してください",
|
||||
"nodes.tool.insertPlaceholder2": "変数を挿入する",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "변환된 내용",
|
||||
"nodes.tool.authorizationRequired": "인증 필요",
|
||||
"nodes.tool.authorize": "권한 부여",
|
||||
"nodes.tool.dateRange.end": "종료",
|
||||
"nodes.tool.dateRange.endPlaceholder": "종료일",
|
||||
"nodes.tool.dateRange.start": "시작",
|
||||
"nodes.tool.dateRange.startPlaceholder": "시작일",
|
||||
"nodes.tool.inputVars": "입력 변수",
|
||||
"nodes.tool.insertPlaceholder1": "타이프하거나 누르세요",
|
||||
"nodes.tool.insertPlaceholder2": "변수를 삽입하다",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Transformed content",
|
||||
"nodes.tool.authorizationRequired": "Autorisatie vereist",
|
||||
"nodes.tool.authorize": "Authorize",
|
||||
"nodes.tool.dateRange.end": "Einde",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Einddatum",
|
||||
"nodes.tool.dateRange.start": "Start",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Startdatum",
|
||||
"nodes.tool.inputVars": "Input Variables",
|
||||
"nodes.tool.insertPlaceholder1": "Type or press",
|
||||
"nodes.tool.insertPlaceholder2": "insert variable",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Przekształcona treść",
|
||||
"nodes.tool.authorizationRequired": "Wymagana autoryzacja",
|
||||
"nodes.tool.authorize": "Autoryzuj",
|
||||
"nodes.tool.dateRange.end": "Koniec",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Data zakończenia",
|
||||
"nodes.tool.dateRange.start": "Początek",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Data rozpoczęcia",
|
||||
"nodes.tool.inputVars": "Zmienne wejściowe",
|
||||
"nodes.tool.insertPlaceholder1": "Wpisz lub naciśnij",
|
||||
"nodes.tool.insertPlaceholder2": "wstaw zmienną",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Conteúdo transformado",
|
||||
"nodes.tool.authorizationRequired": "Autorização necessária",
|
||||
"nodes.tool.authorize": "Autorizar",
|
||||
"nodes.tool.dateRange.end": "Fim",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Data de Término",
|
||||
"nodes.tool.dateRange.start": "Início",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Data de Início",
|
||||
"nodes.tool.inputVars": "Variáveis de entrada",
|
||||
"nodes.tool.insertPlaceholder1": "Digite ou pressione",
|
||||
"nodes.tool.insertPlaceholder2": "inserir variável",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Conținut transformat",
|
||||
"nodes.tool.authorizationRequired": "Autorizare necesară",
|
||||
"nodes.tool.authorize": "Autorizați",
|
||||
"nodes.tool.dateRange.end": "Sfârșit",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Data încheierii",
|
||||
"nodes.tool.dateRange.start": "Început",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Data începerii",
|
||||
"nodes.tool.inputVars": "Variabile de intrare",
|
||||
"nodes.tool.insertPlaceholder1": "Scrieți sau apăsați",
|
||||
"nodes.tool.insertPlaceholder2": "introduce o variabilă",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Преобразованный контент",
|
||||
"nodes.tool.authorizationRequired": "Требуется авторизация",
|
||||
"nodes.tool.authorize": "Авторизовать",
|
||||
"nodes.tool.dateRange.end": "Конец",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Дата окончания",
|
||||
"nodes.tool.dateRange.start": "Начало",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Дата начала",
|
||||
"nodes.tool.inputVars": "Входные переменные",
|
||||
"nodes.tool.insertPlaceholder1": "Наберите или нажмите",
|
||||
"nodes.tool.insertPlaceholder2": "вставьте переменную",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Transformirana vsebina",
|
||||
"nodes.tool.authorizationRequired": "Zahtevana je avtorizacija",
|
||||
"nodes.tool.authorize": "Pooblasti",
|
||||
"nodes.tool.dateRange.end": "Konec",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Datum konca",
|
||||
"nodes.tool.dateRange.start": "Začetek",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Datum začetka",
|
||||
"nodes.tool.inputVars": "Vhodne spremenljivke",
|
||||
"nodes.tool.insertPlaceholder1": "Vnesite ali pritisnite",
|
||||
"nodes.tool.insertPlaceholder2": "vstavite spremenljivko",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "เนื้อหาที่แปลงโฉม",
|
||||
"nodes.tool.authorizationRequired": "ต้องการการอนุญาต",
|
||||
"nodes.tool.authorize": "อนุญาต",
|
||||
"nodes.tool.dateRange.end": "สิ้นสุด",
|
||||
"nodes.tool.dateRange.endPlaceholder": "วันที่สิ้นสุด",
|
||||
"nodes.tool.dateRange.start": "เริ่มต้น",
|
||||
"nodes.tool.dateRange.startPlaceholder": "วันที่เริ่มต้น",
|
||||
"nodes.tool.inputVars": "ตัวแปรอินพุต",
|
||||
"nodes.tool.insertPlaceholder1": "พิมพ์หรือลงทะเบียน",
|
||||
"nodes.tool.insertPlaceholder2": "แทรกตัวแปร",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Dönüştürülmüş içerik",
|
||||
"nodes.tool.authorizationRequired": "Yetkilendirme gerekli",
|
||||
"nodes.tool.authorize": "Yetkilendir",
|
||||
"nodes.tool.dateRange.end": "Bitiş",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Bitiş Tarihi",
|
||||
"nodes.tool.dateRange.start": "Başlangıç",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Başlangıç Tarihi",
|
||||
"nodes.tool.inputVars": "Giriş Değişkenleri",
|
||||
"nodes.tool.insertPlaceholder1": "Yazın veya basın",
|
||||
"nodes.tool.insertPlaceholder2": "değişken ekle",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Трансформований вміст",
|
||||
"nodes.tool.authorizationRequired": "Потрібна авторизація",
|
||||
"nodes.tool.authorize": "Уповноважити",
|
||||
"nodes.tool.dateRange.end": "Кінець",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Дата завершення",
|
||||
"nodes.tool.dateRange.start": "Початок",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Дата початку",
|
||||
"nodes.tool.inputVars": "Вхідні змінні",
|
||||
"nodes.tool.insertPlaceholder1": "Введіть або натисніть",
|
||||
"nodes.tool.insertPlaceholder2": "вставте змінну",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "Nội dung chuyển đổi",
|
||||
"nodes.tool.authorizationRequired": "Yêu cầu ủy quyền",
|
||||
"nodes.tool.authorize": "Ủy quyền",
|
||||
"nodes.tool.dateRange.end": "Kết thúc",
|
||||
"nodes.tool.dateRange.endPlaceholder": "Ngày kết thúc",
|
||||
"nodes.tool.dateRange.start": "Bắt đầu",
|
||||
"nodes.tool.dateRange.startPlaceholder": "Ngày bắt đầu",
|
||||
"nodes.tool.inputVars": "Biến đầu vào",
|
||||
"nodes.tool.insertPlaceholder1": "Gõ hoặc nhấn",
|
||||
"nodes.tool.insertPlaceholder2": "Chèn biến vào",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "转换后内容",
|
||||
"nodes.tool.authorizationRequired": "需要授权",
|
||||
"nodes.tool.authorize": "授权",
|
||||
"nodes.tool.dateRange.end": "结束",
|
||||
"nodes.tool.dateRange.endPlaceholder": "结束日期",
|
||||
"nodes.tool.dateRange.start": "开始",
|
||||
"nodes.tool.dateRange.startPlaceholder": "开始日期",
|
||||
"nodes.tool.inputVars": "输入变量",
|
||||
"nodes.tool.insertPlaceholder1": "键入",
|
||||
"nodes.tool.insertPlaceholder2": "插入变量",
|
||||
|
||||
@ -1002,6 +1002,10 @@
|
||||
"nodes.templateTransform.outputVars.output": "轉換後內容",
|
||||
"nodes.tool.authorizationRequired": "需要授權",
|
||||
"nodes.tool.authorize": "授權",
|
||||
"nodes.tool.dateRange.end": "結束",
|
||||
"nodes.tool.dateRange.endPlaceholder": "結束日期",
|
||||
"nodes.tool.dateRange.start": "開始",
|
||||
"nodes.tool.dateRange.startPlaceholder": "開始日期",
|
||||
"nodes.tool.inputVars": "輸入變數",
|
||||
"nodes.tool.insertPlaceholder1": "輸入或按壓",
|
||||
"nodes.tool.insertPlaceholder2": "插入變數",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user