mirror of
https://github.com/langgenius/dify.git
synced 2026-08-15 04:59:46 +08:00
fix(web): improve markdown form rendering (#40070)
This commit is contained in:
parent
046df1260a
commit
e5a07e97a4
@ -14,6 +14,8 @@ MAX_ITERATIONS_NUM=99
|
||||
TEXT_GENERATION_TIMEOUT_MS=60000
|
||||
WORKFLOW_GENERATION_TIMEOUT_MS=180000
|
||||
ALLOW_INLINE_STYLES=false
|
||||
# Example: ()!*&()!*&-。.;;+=—
|
||||
MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS=
|
||||
ALLOW_UNSAFE_DATA_SCHEME=false
|
||||
MAX_TREE_DEPTH=50
|
||||
MARKETPLACE_API_URL=https://marketplace.dify.ai
|
||||
|
||||
@ -58,6 +58,10 @@ NEXT_PUBLIC_ALLOW_EMBED=
|
||||
# Allow inline style attributes in Markdown rendering (self-hosted opt-in).
|
||||
NEXT_PUBLIC_ALLOW_INLINE_STYLES=false
|
||||
|
||||
# Additional literal characters allowed in Markdown form field names.
|
||||
# Example: ()!*&()!*&-。.;;+=—
|
||||
NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS=
|
||||
|
||||
# Allow rendering unsafe URLs which have "data:" scheme.
|
||||
NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=false
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
describe('env runtime transport', () => {
|
||||
const originalAgentV2Env = process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
const originalMarkdownFormFieldNameExtraChars =
|
||||
process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -7,12 +9,19 @@ describe('env runtime transport', () => {
|
||||
vi.doUnmock('../utils/client')
|
||||
document.body.removeAttribute('data-enable-agent-v2')
|
||||
document.body.removeAttribute('data-enable-agent-v-2')
|
||||
document.body.removeAttribute('data-markdown-form-field-name-extra-chars')
|
||||
delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
delete process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (originalAgentV2Env === undefined) delete process.env.NEXT_PUBLIC_ENABLE_AGENT_V2
|
||||
else process.env.NEXT_PUBLIC_ENABLE_AGENT_V2 = originalAgentV2Env
|
||||
if (originalMarkdownFormFieldNameExtraChars === undefined)
|
||||
delete process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS
|
||||
else
|
||||
process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS =
|
||||
originalMarkdownFormFieldNameExtraChars
|
||||
})
|
||||
|
||||
it('should read NEXT_PUBLIC_ENABLE_AGENT_V2 from the browser runtime dataset key', async () => {
|
||||
@ -37,4 +46,26 @@ describe('env runtime transport', () => {
|
||||
expect(datasetMap['data-enable-agent-v2']).toBe(true)
|
||||
expect(datasetMap['data-enable-agent-v-2']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should read Markdown form field name extra characters from the browser runtime dataset', async () => {
|
||||
document.body.setAttribute('data-markdown-form-field-name-extra-chars', '()!*&()!*&-')
|
||||
|
||||
const { env } = await import('../env')
|
||||
|
||||
expect(env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS).toBe('()!*&()!*&-')
|
||||
})
|
||||
|
||||
it('should emit Markdown form field name extra characters in the server runtime dataset', async () => {
|
||||
process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS = '()!*&()!*&-'
|
||||
|
||||
vi.doMock('../utils/client', () => ({
|
||||
isClient: false,
|
||||
isServer: true,
|
||||
}))
|
||||
|
||||
const { getDatasetMap } = await import('../env')
|
||||
const datasetMap = getDatasetMap()
|
||||
|
||||
expect(datasetMap['data-markdown-form-field-name-extra-chars']).toBe('()!*&()!*&-')
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import MarkdownForm from '../form'
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/context', () => ({
|
||||
useChatContext: () => ({}),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/config')>('@/config')
|
||||
return {
|
||||
...actual,
|
||||
MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS: '',
|
||||
}
|
||||
})
|
||||
|
||||
describe('MarkdownForm default field name characters', () => {
|
||||
it('should reject punctuation that requires explicit configuration', () => {
|
||||
const node = {
|
||||
type: 'element',
|
||||
tagName: 'form',
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'input',
|
||||
properties: {
|
||||
type: 'text',
|
||||
name: '营业&售后(SD)',
|
||||
placeholder: 'mixed-width-punctuation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'input',
|
||||
properties: {
|
||||
type: 'text',
|
||||
name: '字段()!*&-',
|
||||
placeholder: 'full-width-punctuation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'input',
|
||||
properties: {
|
||||
type: 'text',
|
||||
name: 'field()!*&-',
|
||||
placeholder: 'half-width-punctuation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
} satisfies ComponentProps<typeof MarkdownForm>['node']
|
||||
|
||||
render(<MarkdownForm node={node} />)
|
||||
|
||||
expect(screen.queryByPlaceholderText('mixed-width-punctuation')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('full-width-punctuation')).not.toBeInTheDocument()
|
||||
expect(screen.queryByPlaceholderText('half-width-punctuation')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -48,6 +48,14 @@ vi.mock('@/app/components/base/date-and-time-picker/utils/dayjs', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/config', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/config')>('@/config')
|
||||
return {
|
||||
...actual,
|
||||
MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS: '()!*&()!*&-。.;;+=—',
|
||||
}
|
||||
})
|
||||
|
||||
const createTextNode = (value: string): TextNode => ({
|
||||
type: 'text',
|
||||
value,
|
||||
@ -144,6 +152,24 @@ describe('MarkdownForm', () => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith('name: Bob\nbio: Hi there')
|
||||
})
|
||||
})
|
||||
|
||||
it('should omit fields without initial values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode([
|
||||
createElementNode('input', { type: 'text', name: 'name', value: 'Alice' }),
|
||||
createElementNode('input', { type: 'text', name: 'department' }),
|
||||
createElementNode('input', { type: 'checkbox', name: 'acceptTerms' }),
|
||||
createElementNode('button', {}, [createTextNode('Submit')]),
|
||||
])
|
||||
|
||||
render(<MarkdownForm node={node} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith('name: Alice')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Emit serialized JSON when data-format requests JSON output.
|
||||
@ -279,6 +305,29 @@ describe('MarkdownForm', () => {
|
||||
|
||||
// Checkbox interactions should update form state and be reflected in submission output.
|
||||
describe('Checkbox interaction', () => {
|
||||
it('should omit an untouched checkbox without an initial value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode(
|
||||
[
|
||||
createElementNode('input', {
|
||||
type: 'checkbox',
|
||||
name: 'acceptTerms',
|
||||
dataTip: 'Accept terms',
|
||||
}),
|
||||
createElementNode('button', {}, [createTextNode('Submit')]),
|
||||
],
|
||||
{ dataFormat: 'json' },
|
||||
)
|
||||
|
||||
render(<MarkdownForm node={node} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith('{}')
|
||||
})
|
||||
})
|
||||
|
||||
it('should toggle checkbox value and submit updated value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode([
|
||||
@ -539,10 +588,15 @@ describe('MarkdownForm', () => {
|
||||
|
||||
// Unicode letters should be valid form field names.
|
||||
describe('Unicode name support', () => {
|
||||
it('should include fields whose names contain supported full-width and half-width punctuation', async () => {
|
||||
it('should include fields whose names contain configured full-width and half-width punctuation', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode(
|
||||
[
|
||||
createElementNode('input', {
|
||||
type: 'hidden',
|
||||
name: '营业&售后(SD)',
|
||||
value: 'mixed-width',
|
||||
}),
|
||||
createElementNode('input', {
|
||||
type: 'hidden',
|
||||
name: '字段()!*&-',
|
||||
@ -560,11 +614,48 @@ describe('MarkdownForm', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
'{"字段()!*&-":"full-width","field()!*&-":"half-width"}',
|
||||
'{"营业&售后(SD)":"mixed-width","字段()!*&-":"full-width","field()!*&-":"half-width"}',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should include fields whose names contain configured extra characters', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode(
|
||||
[
|
||||
createElementNode('input', {
|
||||
type: 'hidden',
|
||||
name: '字段。.;;+=—',
|
||||
value: 'configured',
|
||||
}),
|
||||
createElementNode('button', {}, [createTextNode('Submit')]),
|
||||
],
|
||||
{ dataFormat: 'json' },
|
||||
)
|
||||
|
||||
render(<MarkdownForm node={node} />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith('{"字段。.;;+=—":"configured"}')
|
||||
})
|
||||
})
|
||||
|
||||
it('should reject extra characters that are not configured', () => {
|
||||
const node = createRootNode([
|
||||
createElementNode('input', {
|
||||
type: 'text',
|
||||
name: '字段/部门',
|
||||
placeholder: 'unconfigured-character',
|
||||
}),
|
||||
])
|
||||
|
||||
render(<MarkdownForm node={node} />)
|
||||
|
||||
expect(screen.queryByPlaceholderText('unconfigured-character')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should include Unicode-named fields from all supported controls in JSON output', async () => {
|
||||
const user = userEvent.setup()
|
||||
const node = createRootNode(
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
toDayjs,
|
||||
} from '@/app/components/base/date-and-time-picker/utils/dayjs'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS } from '@/config'
|
||||
import { getMarkdownButtonAppearance } from './button-appearance'
|
||||
|
||||
const DATA_FORMAT = {
|
||||
@ -54,20 +55,26 @@ const SUPPORTED_TYPES_SET = new Set<string>(Object.values(SUPPORTED_TYPES))
|
||||
|
||||
const SAFE_NAME_RE = (() => {
|
||||
try {
|
||||
return new RegExp('^\\p{L}[\\p{L}\\p{M}\\p{N}_()!*&()!*&--]*$', 'u')
|
||||
return new RegExp('^\\p{L}[\\p{L}\\p{M}\\p{N}_-]*$', 'u')
|
||||
} catch {
|
||||
// Fallback for browsers without Unicode property escape support.
|
||||
return /^[a-z][\w-]*$/i
|
||||
}
|
||||
})()
|
||||
// Treat operator-provided characters literally instead of interpolating them into a regular expression.
|
||||
const EXTRA_SAFE_NAME_CHARS = new Set(MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS)
|
||||
const PROTOTYPE_POISON_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
|
||||
|
||||
function isSafeName(name: unknown): name is string {
|
||||
if (typeof name !== 'string' || name.length === 0 || name.length > 128) return false
|
||||
|
||||
const [firstChar, ...remainingChars] = Array.from(name)
|
||||
return (
|
||||
typeof name === 'string' &&
|
||||
name.length > 0 &&
|
||||
name.length <= 128 &&
|
||||
SAFE_NAME_RE.test(name) &&
|
||||
firstChar !== undefined &&
|
||||
SAFE_NAME_RE.test(firstChar) &&
|
||||
remainingChars.every(
|
||||
(char) => SAFE_NAME_RE.test(`A${char}`) || EXTRA_SAFE_NAME_CHARS.has(char),
|
||||
) &&
|
||||
!PROTOTYPE_POISON_KEYS.has(name)
|
||||
)
|
||||
}
|
||||
@ -126,7 +133,8 @@ function computeInitialFormValues(children: HastElement[]): FormValues {
|
||||
init[name] = raw != null ? toDayjs(String(raw)) : undefined
|
||||
} else if (type === SUPPORTED_TYPES.CHECKBOX) {
|
||||
const { checked, value } = child.properties
|
||||
init[name] = !!checked || value === true || value === 'true'
|
||||
const hasInitialValue = checked != null || value != null
|
||||
init[name] = hasInitialValue ? !!checked || value === true || value === 'true' : undefined
|
||||
} else {
|
||||
init[name] = child.properties.value != null ? str(child.properties.value) : undefined
|
||||
}
|
||||
@ -202,8 +210,9 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
|
||||
const includeTime = child.properties.type === SUPPORTED_TYPES.DATETIME
|
||||
value = formatDateForOutput(value as Dayjs, includeTime)
|
||||
}
|
||||
if (value === undefined) continue
|
||||
if (typeof value === 'boolean') out[name] = value
|
||||
else out[name] = value != null ? String(value) : undefined
|
||||
else out[name] = String(value)
|
||||
}
|
||||
return out
|
||||
}, [elementChildren, formValues])
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
const { mockOnSend } = vi.hoisted(() => ({
|
||||
mockOnSend: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/context', () => ({
|
||||
useChatContext: () => ({ onSend: mockOnSend }),
|
||||
}))
|
||||
|
||||
describe('StreamdownWrapper Markdown form field names', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
document.body.setAttribute(
|
||||
'data-markdown-form-field-name-extra-chars',
|
||||
'。.;;+=—()!*&()!*&-',
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeAttribute('data-markdown-form-field-name-extra-chars')
|
||||
})
|
||||
|
||||
it('should preserve and submit field names containing configured punctuation', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { default: StreamdownWrapper } = await import('../streamdown-wrapper')
|
||||
const content = `
|
||||
<form data-format="json">
|
||||
<input type="text" name=营业&售后(SD) value="mixed" placeholder="mixed-width" />
|
||||
<input type="text" name=字段()!*&- value="full" placeholder="full-width" />
|
||||
<input type="text" name=field()!*&- value="half" placeholder="half-width" />
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
`
|
||||
|
||||
render(<StreamdownWrapper latexContent={content} mode="static" />)
|
||||
|
||||
expect(screen.getByPlaceholderText('mixed-width')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('full-width')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('half-width')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnSend).toHaveBeenCalledWith(
|
||||
'{"营业&售后(SD)":"mixed","字段()!*&-":"full","field()!*&-":"half"}',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Markdown punctuation after a multiline form', async () => {
|
||||
const { default: StreamdownWrapper } = await import('../streamdown-wrapper')
|
||||
const content = `<form data-format="json">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="财务。.;;+=—"
|
||||
data-tip="财务"
|
||||
/>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="营业&售后(SD)"
|
||||
data-tip="营业&售后"
|
||||
/>
|
||||
<input type="text" name="备注" />
|
||||
<input type="text" name="normal-name" placeholder="正常字段" />
|
||||
<input type="text" name="字段()!*&-" value="full-width" />
|
||||
<input type="text" name="field()!*&-" value="half-width" />
|
||||
<button type="submit" data-variant="primary">提交</button>
|
||||
</form>`
|
||||
|
||||
render(<StreamdownWrapper latexContent={content} />)
|
||||
|
||||
expect(await screen.findByRole('button', { name: '提交' })).toBeInTheDocument()
|
||||
expect(screen.queryByText('*')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep incomplete Markdown repair enabled when no form is present', async () => {
|
||||
const { default: StreamdownWrapper } = await import('../streamdown-wrapper')
|
||||
|
||||
render(<StreamdownWrapper latexContent="This is *italic" />)
|
||||
|
||||
const italic = await screen.findByText('italic')
|
||||
expect(italic.tagName).toBe('EM')
|
||||
})
|
||||
})
|
||||
@ -35,6 +35,8 @@ type SanitizeSchema = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const MARKDOWN_FORM_TAG_RE = /<form(?:\s|>)/i
|
||||
|
||||
const CodeBlock = dynamic(() => import('@/app/components/base/markdown-blocks/code-block'), {
|
||||
ssr: false,
|
||||
})
|
||||
@ -167,6 +169,10 @@ const StreamdownWrapper = (props: StreamdownWrapperProps) => {
|
||||
className,
|
||||
mode = 'streaming',
|
||||
} = props
|
||||
// Remend treats Markdown punctuation inside raw HTML attributes as incomplete syntax.
|
||||
// Form markup must reach the HTML parser unchanged or a field name such as `field()!*&-`
|
||||
// gains a synthetic trailing `*` after the closing form tag.
|
||||
const shouldParseIncompleteMarkdown = !MARKDOWN_FORM_TAG_RE.test(latexContent)
|
||||
|
||||
const remarkPlugins = useMemo(
|
||||
() => [
|
||||
@ -245,6 +251,7 @@ const StreamdownWrapper = (props: StreamdownWrapperProps) => {
|
||||
components={components}
|
||||
isAnimating={isAnimating}
|
||||
mode={mode}
|
||||
parseIncompleteMarkdown={shouldParseIncompleteMarkdown}
|
||||
>
|
||||
{latexContent}
|
||||
</Streamdown>
|
||||
|
||||
@ -268,6 +268,8 @@ export const WORKFLOW_GENERATION_TIMEOUT_MS = env.NEXT_PUBLIC_WORKFLOW_GENERATIO
|
||||
export const LOOP_NODE_MAX_COUNT = env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT
|
||||
export const MAX_ITERATIONS_NUM = env.NEXT_PUBLIC_MAX_ITERATIONS_NUM
|
||||
export const MAX_TREE_DEPTH = env.NEXT_PUBLIC_MAX_TREE_DEPTH
|
||||
export const MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS =
|
||||
env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS
|
||||
|
||||
export const ALLOW_INLINE_STYLES = env.NEXT_PUBLIC_ALLOW_INLINE_STYLES
|
||||
export const ALLOW_UNSAFE_DATA_SCHEME = env.NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME
|
||||
|
||||
@ -34,6 +34,7 @@ export NEXT_PUBLIC_WORKFLOW_GENERATION_TIMEOUT_MS=${NEXT_PUBLIC_WORKFLOW_GENERAT
|
||||
export NEXT_PUBLIC_CSP_WHITELIST=${CSP_WHITELIST}
|
||||
export NEXT_PUBLIC_ALLOW_EMBED=${ALLOW_EMBED}
|
||||
export NEXT_PUBLIC_ALLOW_INLINE_STYLES=${ALLOW_INLINE_STYLES:-false}
|
||||
export NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS="${NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS:-${MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS:-}}"
|
||||
export NEXT_PUBLIC_ALLOW_UNSAFE_DATA_SCHEME=${ALLOW_UNSAFE_DATA_SCHEME:-false}
|
||||
export NEXT_PUBLIC_TOP_K_MAX_VALUE=${TOP_K_MAX_VALUE}
|
||||
export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH}
|
||||
|
||||
@ -96,6 +96,10 @@ const clientSchema = {
|
||||
*/
|
||||
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT: coercedNumber.default(100),
|
||||
NEXT_PUBLIC_MAINTENANCE_NOTICE: z.string().optional(),
|
||||
/**
|
||||
* Additional literal characters allowed in Markdown form field names.
|
||||
*/
|
||||
NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS: z.string().default(''),
|
||||
/**
|
||||
* The API PREFIX for MARKETPLACE
|
||||
*/
|
||||
@ -249,6 +253,9 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_MAINTENANCE_NOTICE: isServer
|
||||
? process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE
|
||||
: getRuntimeEnvFromBody('maintenanceNotice'),
|
||||
NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS: isServer
|
||||
? process.env.NEXT_PUBLIC_MARKDOWN_FORM_FIELD_NAME_EXTRA_CHARS
|
||||
: getRuntimeEnvFromBody('markdownFormFieldNameExtraChars'),
|
||||
NEXT_PUBLIC_MARKETPLACE_API_PREFIX: isServer
|
||||
? process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX
|
||||
: getRuntimeEnvFromBody('marketplaceApiPrefix'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user