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:
zyqzyq 2026-08-10 17:31:38 +08:00 committed by GitHub
parent b8934b743c
commit df250423d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 866 additions and 75 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -2332,6 +2332,8 @@ export type ToolParameterType =
| 'array'
| 'boolean'
| 'checkbox'
| 'date'
| 'date-range'
| 'dynamic-select'
| 'file'
| 'files'

View File

@ -2478,6 +2478,8 @@ export const zToolParameterType = z.enum([
'array',
'boolean',
'checkbox',
'date',
'date-range',
'dynamic-select',
'file',
'files',

View File

@ -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)

View File

@ -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)

View File

@ -26,6 +26,8 @@ export enum FormTypeEnum {
object = 'object',
array = 'array',
dynamicSelect = 'dynamic-select',
date = 'date',
dateRange = 'date-range',
}
export type FormOption = {

View File

@ -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,

View File

@ -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', () => ({

View File

@ -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,
}

View File

@ -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}

View File

@ -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

View File

@ -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'),

View File

@ -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

View File

@ -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}

View File

@ -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()
})
})

View File

@ -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('')
})
})

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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">

View File

@ -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">

View File

@ -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": "لإدراج متغير",

View File

@ -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.",

View File

@ -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",

View File

@ -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",

View File

@ -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": "متغیر را وارد کنید",

View File

@ -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",

View File

@ -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": "वेरिएबल डालें",

View File

@ -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",

View File

@ -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",

View File

@ -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": "変数を挿入する",

View File

@ -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": "변수를 삽입하다",

View File

@ -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",

View File

@ -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ą",

View File

@ -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",

View File

@ -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ă",

View File

@ -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": "вставьте переменную",

View File

@ -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",

View File

@ -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": "แทรกตัวแปร",

View File

@ -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",

View File

@ -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": "вставте змінну",

View File

@ -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",

View File

@ -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": "插入变量",

View File

@ -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": "插入變數",