<select/> -> <Select /> olarak değiştirildi.

This commit is contained in:
Sedat ÖZTÜRK 2026-08-20 17:54:11 +03:00
parent 7186cb0ab0
commit 4c09de9ad0
27 changed files with 1114 additions and 913 deletions

View file

@ -1,6 +1,6 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa' import { FaPlus, FaSearch, FaTimes } from 'react-icons/fa'
import { Button } from '@/components/ui' import { Button, Select } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
@ -352,6 +352,11 @@ const StyleModal = ({
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [customClass, setCustomClass] = useState('') const [customClass, setCustomClass] = useState('')
const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | '__all__'>('__all__') const [category, setCategory] = useState<keyof typeof STYLE_GROUPS | '__all__'>('__all__')
const categoryOptions = [
{ value: '__all__', label: translate('::App.StyleModal.AllCategories') },
...Object.keys(STYLE_GROUPS).map((name) => ({ value: name, label: name })),
]
const selected = useMemo(() => new Set(splitClasses(value)), [value]) const selected = useMemo(() => new Set(splitClasses(value)), [value])
const classes = useMemo(() => { const classes = useMemo(() => {
const source = const source =
@ -409,18 +414,15 @@ const StyleModal = ({
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
/> />
</label> </label>
<select <Select
className="rounded-md border border-slate-300 bg-white px-3 text-sm dark:border-slate-700 dark:bg-slate-950 dark:text-white" size="sm"
value={category} menuPortalTarget={document.body}
onChange={(event) => setCategory(event.target.value as typeof category)} options={categoryOptions}
> value={categoryOptions.filter((option) => option.value === category)}
<option value="__all__">{translate('::App.StyleModal.AllCategories')}</option> onChange={(option) =>
{Object.keys(STYLE_GROUPS).map((name) => ( setCategory((option?.value ?? '__all__') as typeof category)
<option key={name} value={name}> }
{name} />
</option>
))}
</select>
</div> </div>
)} )}
{mode === 'class' && ( {mode === 'class' && (

View file

@ -1,4 +1,4 @@
import { Button, Dialog } from '@/components/ui' import { Button, Dialog, Select } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Editor, { type Monaco } from '@monaco-editor/react' import Editor, { type Monaco } from '@monaco-editor/react'
import type * as monacoApi from 'monaco-editor' import type * as monacoApi from 'monaco-editor'
@ -276,18 +276,20 @@ function ScriptBuilderDialog({
) )
} }
return ( return (
<select <Select
className={controlClass} size="sm"
value={options.includes(selectedValue) ? selectedValue : ''} className="w-full"
onChange={(event) => onChange(event.target.value)} isClearable
> placeholder={placeholder}
<option value="">{placeholder}</option> menuPortalTarget={document.body}
{options.map((option) => ( options={options.map((option) => ({ value: option, label: option }))}
<option key={option} value={option}> value={
{option} options.includes(selectedValue)
</option> ? { value: selectedValue, label: selectedValue }
))} : null
</select> }
onChange={(option) => onChange(option?.value ?? '')}
/>
) )
} }
@ -318,24 +320,21 @@ function ScriptBuilderDialog({
</button> </button>
</span> </span>
))} ))}
<select <Select
className={`${controlClass} !h-8 !w-44`} size="sm"
value="" className="w-44"
onChange={(event) => { placeholder={translate('::App.ScriptBuilder.AddOption')}
const next = event.target.value menuPortalTarget={document.body}
options={options
.filter((fieldName) => !selectedFields.includes(fieldName))
.map((fieldName) => ({ value: fieldName, label: fieldName }))}
value={null}
onChange={(option) => {
const next = option?.value
if (!next || selectedFields.includes(next)) return if (!next || selectedFields.includes(next)) return
updateRule(rule.id, { fields: [...selectedFields, next] }) updateRule(rule.id, { fields: [...selectedFields, next] })
}} }}
> />
<option value="">{translate('::App.ScriptBuilder.AddOption')}</option>
{options
.filter((fieldName) => !selectedFields.includes(fieldName))
.map((fieldName) => (
<option key={fieldName} value={fieldName}>
{fieldName}
</option>
))}
</select>
</div> </div>
</div> </div>
) )
@ -357,17 +356,21 @@ function ScriptBuilderDialog({
param.placeholder || translate('::App.ScriptBuilder.Choose'), param.placeholder || translate('::App.ScriptBuilder.Choose'),
)} )}
{param.type === 'select' && ( {param.type === 'select' && (
<select <Select
className={controlClass} size="sm"
value={currentValue} className="w-full"
onChange={(event) => updateParam(rule.id, param.key, event.target.value)} menuPortalTarget={document.body}
> options={(param.choices ?? []).map((choice) => ({
{param.choices?.map((choice) => ( value: choice.value,
<option key={choice.value} value={choice.value}> label: choice.label,
{choice.label} }))}
</option> value={(param.choices ?? [])
))} .filter((choice) => choice.value === currentValue)
</select> .map((choice) => ({ value: choice.value, label: choice.label }))}
onChange={(option) =>
option && updateParam(rule.id, param.key, option.value)
}
/>
)} )}
{(param.type === 'text' || param.type === 'number') && ( {(param.type === 'text' || param.type === 'number') && (
<Input <Input
@ -388,6 +391,26 @@ function ScriptBuilderDialog({
) )
} }
const joinOptions = [
{ value: 'and', label: translate('::App.Platform.And') },
{ value: 'or', label: translate('::App.Platform.Or') },
]
const operatorOptions = dialect.operators.map((item) => ({
value: item.value,
label: item.label.startsWith('App.') ? translate('::' + item.label) : item.label,
}))
const conditionKindOptions = dialect.conditionKinds.map((item) => ({
value: item.value,
label: item.label,
}))
const triggerOptions = (dialect.triggers ?? []).map((item) => ({
value: item.value,
label: item.label,
}))
const renderCondition = (rule: ScriptRule, condition: ScriptRuleCondition, index: number) => { const renderCondition = (rule: ScriptRule, condition: ScriptRuleCondition, index: number) => {
const operator = dialect.operators.find((item) => item.value === condition.operator) const operator = dialect.operators.find((item) => item.value === condition.operator)
const kind = const kind =
@ -401,16 +424,16 @@ function ScriptBuilderDialog({
<span className="mb-1 block text-xs text-gray-500"> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Conjunction')} {translate('::App.ScriptBuilder.Conjunction')}
</span> </span>
<select <Select
className={controlClass} size="sm"
value={rule.join ?? 'and'} className="w-full"
onChange={(event) => menuPortalTarget={document.body}
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] }) options={joinOptions}
value={joinOptions.filter((option) => option.value === (rule.join ?? 'and'))}
onChange={(option) =>
option && updateRule(rule.id, { join: option.value as ScriptRule['join'] })
} }
> />
<option value="and">{translate('::App.Platform.And')}</option>
<option value="or">{translate('::App.Platform.Or')}</option>
</select>
</label> </label>
) : ( ) : (
<div className="col-span-12 hidden md:col-span-2 md:block"> <div className="col-span-12 hidden md:col-span-2 md:block">
@ -427,17 +450,16 @@ function ScriptBuilderDialog({
<span className="mb-1 block text-xs text-gray-500"> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.ScriptBuilder.Comparison')} {translate('::App.ScriptBuilder.Comparison')}
</span> </span>
<select <Select
className={controlClass} size="sm"
value={condition.operator} className="w-full"
onChange={(event) => updateCondition(rule, index, { operator: event.target.value })} menuPortalTarget={document.body}
> options={operatorOptions}
{dialect.operators.map((item) => ( value={operatorOptions.filter((option) => option.value === condition.operator)}
<option key={item.value} value={item.value}> onChange={(option) =>
{item.label.startsWith('App.') ? translate('::' + item.label) : item.label} option && updateCondition(rule, index, { operator: option.value })
</option> }
))} />
</select>
</label> </label>
{operator?.needsSource && dialect.conditionKinds.length > 1 && ( {operator?.needsSource && dialect.conditionKinds.length > 1 && (
@ -445,19 +467,19 @@ function ScriptBuilderDialog({
<span className="mb-1 block text-xs text-gray-500"> <span className="mb-1 block text-xs text-gray-500">
{translate('::App.Listform.ListformField.SourceId')} {translate('::App.Listform.ListformField.SourceId')}
</span> </span>
<select <Select
className={controlClass} size="sm"
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''} className="w-full"
onChange={(event) => menuPortalTarget={document.body}
updateCondition(rule, index, { kind: event.target.value, source: '' }) options={conditionKindOptions}
value={conditionKindOptions.filter(
(option) =>
option.value === (condition.kind ?? dialect.conditionKinds[0]?.value ?? ''),
)}
onChange={(option) =>
option && updateCondition(rule, index, { kind: option.value, source: '' })
} }
> />
{dialect.conditionKinds.map((item) => (
<option key={item.value} value={item.value}>
{item.label}
</option>
))}
</select>
</label> </label>
)} )}
@ -602,17 +624,18 @@ function ScriptBuilderDialog({
{dialect.triggers && ( {dialect.triggers && (
<label className="w-full md:w-56"> <label className="w-full md:w-56">
<span className="mb-1 block text-xs text-gray-500">Ne zaman</span> <span className="mb-1 block text-xs text-gray-500">Ne zaman</span>
<select <Select
className={controlClass} size="sm"
value={trigger ?? dialect.triggers[0].value} className="w-full"
onChange={(event) => updateRule(rule.id, { trigger: event.target.value })} menuPortalTarget={document.body}
> options={triggerOptions}
{dialect.triggers.map((item) => ( value={triggerOptions.filter(
<option key={item.value} value={item.value} title={item.help}> (option) => option.value === (trigger ?? dialect.triggers?.[0]?.value),
{item.label} )}
</option> onChange={(option) =>
))} option && updateRule(rule.id, { trigger: option.value })
</select> }
/>
</label> </label>
)} )}

View file

@ -248,6 +248,9 @@ function SelectBase<
placeholder: (provided) => ({ ...provided, margin: 0 }), placeholder: (provided) => ({ ...provided, margin: 0 }),
singleValue: (provided) => ({ ...provided, margin: 0 }), singleValue: (provided) => ({ ...provided, margin: 0 }),
menu: (provided) => ({ ...provided, zIndex: 50 }), menu: (provided) => ({ ...provided, zIndex: 50 }),
// Portalled menus must clear dialogs, drawers and sticky toolbars;
// react-select's own inline zIndex of 1 would leave them behind.
menuPortal: (provided) => ({ ...provided, zIndex: 9999 }),
...style, ...style,
}} }}
theme={(theme) => ({ theme={(theme) => ({

View file

@ -999,18 +999,19 @@ const SqlDataSourceView = ({
{rows.length > 1 && ( {rows.length > 1 && (
<label className="flex items-center gap-1"> <label className="flex items-center gap-1">
{translate('::App.Platform.Row')} {translate('::App.Platform.Row')}
<select <UiKit.Select
className="rounded border border-sky-300 bg-white px-1 py-0.5 text-[10px] dark:border-sky-800 dark:bg-slate-900" size="xs"
value={rowIndex} className="min-w-[4.5rem]"
onChange={(event) => goToRow(Number(event.target.value) || 0)} maxMenuHeight={200}
onClick={(event) => event.stopPropagation()} menuPosition="fixed"
> menuPortalTarget={window.document.body}
{rows.map((_, index) => ( options={rows.map((_, index) => ({
<option key={index} value={index}> value: index,
{index + 1} label: String(index + 1),
</option> }))}
))} value={{ value: rowIndex, label: String(rowIndex + 1) }}
</select> onChange={(option) => goToRow(Number(option?.value) || 0)}
/>
</label> </label>
)} )}
</div> </div>

View file

@ -1,4 +1,4 @@
import { Button, Card, FormItem, Input } from '@/components/ui' import { Button, Card, FormItem, Input, Select } from '@/components/ui'
import { ColumnFormatEditDto, ListFormFieldEditTabs } from '@/proxy/admin/list-form-field/models' import { ColumnFormatEditDto, ListFormFieldEditTabs } from '@/proxy/admin/list-form-field/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
@ -50,6 +50,10 @@ function TablePickerModal({
null, null,
) )
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([]) const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
const pickerColumnOptions = pickerColumns.map((c) => ({
value: c.columnName ?? '',
label: c.columnName ?? '',
}))
const [isLoadingColumns, setIsLoadingColumns] = useState(false) const [isLoadingColumns, setIsLoadingColumns] = useState(false)
const [keyCol, setKeyCol] = useState('') const [keyCol, setKeyCol] = useState('')
const [nameCol, setNameCol] = useState('') const [nameCol, setNameCol] = useState('')
@ -163,35 +167,31 @@ function TablePickerModal({
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
{translate('::App.ListFormFieldEdit.KeyColumn')} {translate('::App.ListFormFieldEdit.KeyColumn')}
</label> </label>
<select <Select
value={keyCol} size="sm"
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs"
onChange={(e) => setKeyCol(e.target.value)} isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{pickerColumns.map((c) => ( options={pickerColumnOptions}
<option key={c.columnName} value={c.columnName}> value={pickerColumnOptions.filter((option) => option.value === keyCol)}
{c.columnName} onChange={(option) => setKeyCol(option?.value ?? '')}
</option> />
))}
</select>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
{translate('::App.ListFormFieldEdit.NameColumn')} {translate('::App.ListFormFieldEdit.NameColumn')}
</label> </label>
<select <Select
value={nameCol} size="sm"
className="w-full text-xs h-8 px-2 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" className="w-full text-xs"
onChange={(e) => setNameCol(e.target.value)} isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{pickerColumns.map((c) => ( options={pickerColumnOptions}
<option key={c.columnName} value={c.columnName}> value={pickerColumnOptions.filter((option) => option.value === nameCol)}
{c.columnName} onChange={(option) => setNameCol(option?.value ?? '')}
</option> />
))}
</select>
</div> </div>
{keyCol && nameCol && ( {keyCol && nameCol && (
<div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all"> <div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all">

View file

@ -1,4 +1,4 @@
import { Button, Dialog } from '@/components/ui' import { Button, Dialog, Select } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import type { IconType } from 'react-icons' import type { IconType } from 'react-icons'
@ -61,6 +61,7 @@ type EditorOptionsBuilderDialogProps = {
} }
const leafTypes: LeafValueType[] = ['string', 'number', 'boolean', 'json'] const leafTypes: LeafValueType[] = ['string', 'number', 'boolean', 'json']
const leafTypeOptions = leafTypes.map((item) => ({ value: item, label: item }))
const PRESETS_KEY = '__presets' const PRESETS_KEY = '__presets'
const UNMANAGED_KEY = '__unmanaged' const UNMANAGED_KEY = '__unmanaged'
@ -545,25 +546,22 @@ function EditorOptionsBuilderDialog({
> >
{path} {path}
</span> </span>
<select <Select
className={`${controlClass} col-span-2`} size="sm"
value={type} className="col-span-2"
onChange={(event) => menuPortalTarget={document.body}
options={leafTypeOptions}
value={leafTypeOptions.filter((option) => option.value === type)}
onChange={(option) =>
setPath( setPath(
path, path,
textToLeaf( textToLeaf(
leafToText(current), leafToText(current),
event.target.value as LeafValueType, (option?.value ?? 'string') as LeafValueType,
), ),
) )
} }
> />
{leafTypes.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
<Input <Input
unstyle unstyle
className={`${controlClass} col-span-5`} className={`${controlClass} col-span-5`}
@ -596,17 +594,16 @@ function EditorOptionsBuilderDialog({
placeholder="path.to.option" placeholder="path.to.option"
onChange={(event) => setNewPath(event.target.value)} onChange={(event) => setNewPath(event.target.value)}
/> />
<select <Select
className={`${controlClass} col-span-2`} size="sm"
value={newType} className="col-span-2"
onChange={(event) => setNewType(event.target.value as LeafValueType)} menuPortalTarget={document.body}
> options={leafTypeOptions}
{leafTypes.map((item) => ( value={leafTypeOptions.filter((option) => option.value === newType)}
<option key={item} value={item}> onChange={(option) =>
{item} setNewType((option?.value ?? 'string') as LeafValueType)
</option> }
))} />
</select>
<Input <Input
unstyle unstyle
className={`${controlClass} col-span-5`} className={`${controlClass} col-span-5`}

View file

@ -4,6 +4,7 @@ import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
import type { OptionSpec } from './optionSpecs' import type { OptionSpec } from './optionSpecs'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
export const controlClass = export const controlClass =
'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60' 'w-full min-w-0 h-9 px-2 rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm text-gray-700 dark:text-gray-100 focus:outline-none focus:border-indigo-400 disabled:opacity-60'
@ -64,19 +65,24 @@ const BooleanControl = ({
? 'false' ? 'false'
: '' : ''
const booleanOptions = [
{ value: 'true', label: 'true' },
{ value: 'false', label: 'false' },
]
return ( return (
<select <Select
className={controlClass} size="sm"
value={current} isClearable
onChange={(event) => { placeholder={translate('::App.ListFormEditorOptions.Undefined')}
if (!event.target.value) return onChange(undefined) menuPortalTarget={document.body}
onChange(event.target.value === 'true') options={booleanOptions}
value={booleanOptions.filter((option) => option.value === current)}
onChange={(option) => {
if (!option?.value) return onChange(undefined)
onChange(option.value === 'true')
}} }}
> />
<option value="">{translate('::App.ListFormEditorOptions.Undefined')}</option>
<option value="true">true</option>
<option value="false">false</option>
</select>
) )
} }
@ -93,29 +99,36 @@ const SelectControl = ({
const current = value === undefined || value === null ? '' : String(value) const current = value === undefined || value === null ? '' : String(value)
const known = spec.choices?.some((choice) => String(choice.value) === current) const known = spec.choices?.some((choice) => String(choice.value) === current)
const choiceOptions = [
...(spec.choices ?? []).map((choice) => ({
value: String(choice.value),
label: String(choice.label),
})),
...(current && !known
? [
{
value: current,
label: `${current} (${translate('::App.ListFormEditorOptions.CurrentValue')})`,
},
]
: []),
]
return ( return (
<select <Select
className={controlClass} size="sm"
value={current} isClearable
onChange={(event) => { placeholder={translate('::App.ListFormEditorOptions.Undefined')}
const raw = event.target.value menuPortalTarget={document.body}
options={choiceOptions}
value={choiceOptions.filter((option) => option.value === current)}
onChange={(option) => {
const raw = option?.value
if (!raw) return onChange(undefined) if (!raw) return onChange(undefined)
const choice = spec.choices?.find((item) => String(item.value) === raw) const choice = spec.choices?.find((item) => String(item.value) === raw)
onChange(choice ? choice.value : raw) onChange(choice ? choice.value : raw)
}} }}
> />
<option value="">{translate('::App.ListFormEditorOptions.Undefined')}</option>
{spec.choices?.map((choice) => (
<option key={String(choice.value)} value={String(choice.value)}>
{choice.label}
</option>
))}
{current && !known && (
<option value={current}>
{current} ({translate('::App.ListFormEditorOptions.CurrentValue')})
</option>
)}
</select>
) )
} }

View file

@ -44,7 +44,7 @@ export const triggerLabels: { value: RuleTrigger; label: string; help: string }[
label: 'App.ScriptBuilderOpen.OpenLabel', label: 'App.ScriptBuilderOpen.OpenLabel',
help: 'App.ScriptBuilderOpen.Help', help: 'App.ScriptBuilderOpen.Help',
}, },
{ value: 'both', label: 'Her ikisi', help: 'App.ScriptBuilderBoth.Help' }, { value: 'both', label: 'App.StaticLookup.Both', help: 'App.ScriptBuilderBoth.Help' },
] ]
/** Ek koşullar nasıl birleşecek. */ /** Ek koşullar nasıl birleşecek. */

View file

@ -1,4 +1,4 @@
import { Button, Dialog } from '@/components/ui' import { Button, Dialog, Select } from '@/components/ui'
import type { SelectBoxOption } from '@/types/shared' import type { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { import {
@ -230,6 +230,10 @@ function SortableItem({
null, null,
) )
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([]) const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
const pickerColumnOptions = pickerColumns.map((c) => ({
value: c.columnName ?? '',
label: c.columnName ?? '',
}))
const [isLoadingPickerColumns, setIsLoadingPickerColumns] = useState(false) const [isLoadingPickerColumns, setIsLoadingPickerColumns] = useState(false)
const [pickerKeyCol, setPickerKeyCol] = useState('') const [pickerKeyCol, setPickerKeyCol] = useState('')
const [pickerNameCol, setPickerNameCol] = useState('') const [pickerNameCol, setPickerNameCol] = useState('')
@ -321,17 +325,17 @@ function SortableItem({
<span className="text-[10px] text-gray-400 font-medium"> <span className="text-[10px] text-gray-400 font-medium">
{translate('::App.WizardStep3.EditorType')} {translate('::App.WizardStep3.EditorType')}
</span> </span>
<select <Select
value={item.editorType} size="xs"
onChange={(e) => onEditorTypeChange(e.target.value)} className="w-full text-xs"
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" maxMenuHeight={200}
> menuPortalTarget={document.body}
{columnEditorTypeListOptions.map((et) => ( options={columnEditorTypeListOptions}
<option key={et.value} value={et.value}> value={columnEditorTypeListOptions.filter(
{et.label} (option) => option.value === item.editorType,
</option> )}
))} onChange={(option) => option && onEditorTypeChange(option.value)}
</select> />
</div> </div>
<div className="flex flex-row flex-wrap gap-1.5"> <div className="flex flex-row flex-wrap gap-1.5">
@ -339,19 +343,21 @@ function SortableItem({
<span className="text-[10px] text-gray-400 font-medium"> <span className="text-[10px] text-gray-400 font-medium">
{translate('::App.WizardStep3.LookupDataSourceType')} {translate('::App.WizardStep3.LookupDataSourceType')}
</span> </span>
<select <Select
value={item.lookupDataSourceType} size="xs"
onChange={(e) => className="w-full text-xs"
onLookupDataSourceTypeChange(e.target.value as unknown as UiLookupDataSourceTypeEnum) menuPortalTarget={document.body}
options={columnLookupDataSourceTypeListOptions}
value={columnLookupDataSourceTypeListOptions.filter(
(option) => option.value === item.lookupDataSourceType,
)}
onChange={(option) =>
option &&
onLookupDataSourceTypeChange(
option.value as unknown as UiLookupDataSourceTypeEnum,
)
} }
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" />
>
{columnLookupDataSourceTypeListOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div> </div>
<div className="flex flex-col gap-0.5 min-w-[100px] flex-1"> <div className="flex flex-col gap-0.5 min-w-[100px] flex-1">
@ -514,35 +520,35 @@ function SortableItem({
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
{translate('::App.ListFormFieldEdit.KeyColumn')} {translate('::App.ListFormFieldEdit.KeyColumn')}
</label> </label>
<select <Select
value={pickerKeyCol} size="xs"
onChange={(e) => setPickerKeyCol(e.target.value)} className="w-full text-xs"
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{pickerColumns.map((c) => ( options={pickerColumnOptions}
<option key={c.columnName} value={c.columnName}> value={pickerColumnOptions.filter(
{c.columnName} (option) => option.value === pickerKeyCol,
</option> )}
))} onChange={(option) => setPickerKeyCol(option?.value ?? '')}
</select> />
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400"> <label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
{translate('::App.ListFormFieldEdit.NameColumn')} {translate('::App.ListFormFieldEdit.NameColumn')}
</label> </label>
<select <Select
value={pickerNameCol} size="xs"
onChange={(e) => setPickerNameCol(e.target.value)} className="w-full text-xs"
className="w-full text-xs h-7 px-1.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{pickerColumns.map((c) => ( options={pickerColumnOptions}
<option key={c.columnName} value={c.columnName}> value={pickerColumnOptions.filter(
{c.columnName} (option) => option.value === pickerNameCol,
</option> )}
))} onChange={(option) => setPickerNameCol(option?.value ?? '')}
</select> />
</div> </div>
{pickerKeyCol && pickerNameCol && ( {pickerKeyCol && pickerNameCol && (
<div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all"> <div className="rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5 text-[10px] font-mono text-gray-500 dark:text-gray-400 break-all">
@ -651,17 +657,17 @@ function SortableItem({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-[10px] text-gray-400">{translate('::App.WizardStep3.Span')}</span> <span className="text-[10px] text-gray-400">{translate('::App.WizardStep3.Span')}</span>
<select <Select
value={item.colSpan} size="xs"
onChange={(e) => onColSpanChange(Number(e.target.value))} className="w-20 text-xs"
className="text-xs h-5 w-9 px-0.5 rounded border border-gray-200 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-200 focus:outline-none focus:border-indigo-400" menuPortalTarget={document.body}
> options={Array.from({ length: groupColCount }, (_, i) => ({
{Array.from({ length: groupColCount }, (_, i) => i + 1).map((n) => ( value: i + 1,
<option key={n} value={n}> label: String(i + 1),
{n} }))}
</option> value={{ value: item.colSpan, label: String(item.colSpan) }}
))} onChange={(option) => option && onColSpanChange(Number(option.value))}
</select> />
</div> </div>
<label <label
className="flex items-center gap-1 cursor-pointer ml-auto" className="flex items-center gap-1 cursor-pointer ml-auto"

View file

@ -1,4 +1,5 @@
import classNames from 'classnames' import classNames from 'classnames'
import Select from '@/components/ui/Select'
import { FaChevronRight, FaChevronDown } from 'react-icons/fa' import { FaChevronRight, FaChevronDown } from 'react-icons/fa'
import Container from '@/components/shared/Container' import Container from '@/components/shared/Container'
import { Button, Checkbox, Dialog, Input, Menu, toast } from '@/components/ui' import { Button, Checkbox, Dialog, Input, Menu, toast } from '@/components/ui'
@ -480,6 +481,10 @@ function RolesPermission({
const [copyDialogOpen, setCopyDialogOpen] = useState(false) const [copyDialogOpen, setCopyDialogOpen] = useState(false)
const [copyDialogRole, setCopyDialogRole] = useState('') const [copyDialogRole, setCopyDialogRole] = useState('')
const copyRoleOptions = roleList
.filter((role) => role !== name)
.map((role) => ({ value: role, label: role }))
// Fetch all roles for select (except current) // Fetch all roles for select (except current)
useEffect(() => { useEffect(() => {
async function fetchRoles() { async function fetchRoles() {
@ -571,20 +576,16 @@ function RolesPermission({
> >
<h5 className="mb-2">{translate('::AbpIdentity.Roles.CopyPermissions')}</h5> <h5 className="mb-2">{translate('::AbpIdentity.Roles.CopyPermissions')}</h5>
<div className="mb-4"> <div className="mb-4">
<select <Select
className="border rounded px-2 py-1 w-full" size="sm"
value={copyDialogRole} className="w-full"
onChange={(e) => setCopyDialogRole(e.target.value)} isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{roleList options={copyRoleOptions}
.filter((role) => role !== name) value={copyRoleOptions.filter((option) => option.value === copyDialogRole)}
.map((role) => ( onChange={(option) => setCopyDialogRole(option?.value ?? '')}
<option key={role} value={role}> />
{role}
</option>
))}
</select>
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setCopyDialogOpen(false)}> <Button variant="plain" onClick={() => setCopyDialogOpen(false)}>

View file

@ -9,6 +9,7 @@ import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/
import { Button } from '@/components/ui' import { Button } from '@/components/ui'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
interface ChatPanelProps { interface ChatPanelProps {
user: { id: string; name: string; role: string } user: { id: string; name: string; role: string }
@ -50,6 +51,11 @@ const ChatPanel = ({
const availableRecipients = participants.filter((p) => p.id !== user.id) const availableRecipients = participants.filter((p) => p.id !== user.id)
const recipientOptions = availableRecipients.map((p) => ({
value: p.id,
label: `${p.name}${p.isTeacher ? ` (${translate('::App.VideoRoom.Teacher')})` : ''}`,
}))
return ( return (
<div className="h-full bg-white flex flex-col text-gray-900"> <div className="h-full bg-white flex flex-col text-gray-900">
{/* Header */} {/* Header */}
@ -125,21 +131,18 @@ const ChatPanel = ({
</div> </div>
{messageMode === 'private' && ( {messageMode === 'private' && (
<select <Select
value={selectedRecipient?.id || ''} size="xs"
onChange={(e) => { className="w-full"
const recipient = availableRecipients.find((p) => p.id === e.target.value) isClearable
placeholder={translate('::App.VideoRoom.SelectPerson')}
options={recipientOptions}
value={recipientOptions.filter((o) => o.value === (selectedRecipient?.id ?? ''))}
onChange={(option) => {
const recipient = availableRecipients.find((p) => p.id === option?.value)
setSelectedRecipient(recipient ? { id: recipient.id, name: recipient.name } : null) setSelectedRecipient(recipient ? { id: recipient.id, name: recipient.name } : null)
}} }}
className="w-full px-2 py-1 text-xs border border-gray-300 rounded" />
>
<option value="">{translate('::App.VideoRoom.SelectPerson')}</option>
{availableRecipients.map((p) => (
<option key={p.id} value={p.id}>
{p.name} {p.isTeacher ? `(${translate('::App.VideoRoom.Teacher')})` : ''}
</option>
))}
</select>
)} )}
</div> </div>

View file

@ -29,7 +29,7 @@ import PageTitle from '@/components/shared/PageTitle'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { VideoroomDto } from '@/proxy/videoroom/models' import { VideoroomDto } from '@/proxy/videoroom/models'
import classNames from 'classnames' import classNames from 'classnames'
import { Button, Dialog, Input } from '@/components/ui' import { Button, Dialog, Input, Select } from '@/components/ui'
import { FcVideoCall } from 'react-icons/fc' import { FcVideoCall } from 'react-icons/fc'
export interface RoomProps { export interface RoomProps {
@ -45,6 +45,21 @@ const RoomList = () => {
const { user } = useStoreState((state) => state.auth) const { user } = useStoreState((state) => state.auth)
const { translate } = useLocalization() const { translate } = useLocalization()
const microphoneStateOptions = [
{ value: 'muted' as const, label: translate('::App.VideoRoom.MicrophoneMuted') },
{ value: 'unmuted' as const, label: translate('::App.VideoRoom.MicrophoneUnmuted') },
]
const cameraStateOptions = [
{ value: 'on' as const, label: translate('::App.VideoRoom.CameraOn') },
{ value: 'off' as const, label: translate('::App.VideoRoom.CameraOff') },
]
const layoutOptions = [
{ value: 'grid', label: translate('::App.VideoRoom.LayoutGridView') },
{ value: 'teacher-focus', label: translate('::App.VideoRoom.LayoutTeacherFocus') },
{ value: 'presentation', label: translate('::App.VideoRoom.LayoutPresentation') },
{ value: 'sidebar', label: translate('::App.VideoRoom.LayoutSidebar') },
]
const newClassEntity: VideoroomDto = { const newClassEntity: VideoroomDto = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
name: '', name: '',
@ -733,80 +748,70 @@ const RoomList = () => {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{translate('::App.VideoRoom.DefaultMicrophoneState')} {translate('::App.VideoRoom.DefaultMicrophoneState')}
</label> </label>
<select <Select
value={videoroom.settingsDto?.defaultMicrophoneState} size="sm"
onChange={(e) => menuPortalTarget={document.body}
options={microphoneStateOptions}
value={microphoneStateOptions.filter(
(option) =>
option.value === videoroom.settingsDto?.defaultMicrophoneState,
)}
onChange={(option) =>
setVideoroom({ setVideoroom({
...videoroom, ...videoroom,
settingsDto: { settingsDto: {
...videoroom.settingsDto!, ...videoroom.settingsDto!,
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted', defaultMicrophoneState: option?.value ?? 'muted',
}, },
}) })
} }
className="border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
>
<option value="muted">
{translate('::App.VideoRoom.MicrophoneMuted')}
</option>
<option value="unmuted">
{translate('::App.VideoRoom.MicrophoneUnmuted')}
</option>
</select>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{translate('::App.VideoRoom.DefaultCameraState')} {translate('::App.VideoRoom.DefaultCameraState')}
</label> </label>
<select <Select
value={videoroom.settingsDto?.defaultCameraState} size="sm"
onChange={(e) => menuPortalTarget={document.body}
options={cameraStateOptions}
value={cameraStateOptions.filter(
(option) => option.value === videoroom.settingsDto?.defaultCameraState,
)}
onChange={(option) =>
setVideoroom({ setVideoroom({
...videoroom, ...videoroom,
settingsDto: { settingsDto: {
...videoroom.settingsDto!, ...videoroom.settingsDto!,
defaultCameraState: e.target.value as 'on' | 'off', defaultCameraState: option?.value ?? 'on',
}, },
}) })
} }
className="border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
>
<option value="on">{translate('::App.VideoRoom.CameraOn')}</option>
<option value="off">{translate('::App.VideoRoom.CameraOff')}</option>
</select>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{translate('::App.VideoRoom.DefaultLayout')} {translate('::App.VideoRoom.DefaultLayout')}
</label> </label>
<select <Select
value={videoroom.settingsDto?.defaultLayout} size="sm"
onChange={(e) => menuPortalTarget={document.body}
options={layoutOptions}
value={layoutOptions.filter(
(option) => option.value === videoroom.settingsDto?.defaultLayout,
)}
onChange={(option) =>
setVideoroom({ setVideoroom({
...videoroom, ...videoroom,
settingsDto: { settingsDto: {
...videoroom.settingsDto!, ...videoroom.settingsDto!,
defaultLayout: e.target.value, defaultLayout: option?.value ?? 'grid',
}, },
}) })
} }
className="border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
>
<option value="grid">
{translate('::App.VideoRoom.LayoutGridView')}
</option>
<option value="teacher-focus">
{translate('::App.VideoRoom.LayoutTeacherFocus')}
</option>
<option value="presentation">
{translate('::App.VideoRoom.LayoutPresentation')}
</option>
<option value="sidebar">
{translate('::App.VideoRoom.LayoutSidebar')}
</option>
</select>
</div> </div>
<label className="flex items-center space-x-3"> <label className="flex items-center space-x-3">

View file

@ -25,6 +25,7 @@ import { parseComponentDependencies } from '@/contexts/componentRuntime'
import { usePermission } from '@/utils/hooks/usePermission' import { usePermission } from '@/utils/hooks/usePermission'
import { COMPONENT_PERMISSION } from '@/constants/permission.constant' import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
const ComponentManager: React.FC = () => { const ComponentManager: React.FC = () => {
const { const {
@ -45,6 +46,12 @@ const ComponentManager: React.FC = () => {
const activeComponents = components?.filter((c) => c.isActive).length || 0 const activeComponents = components?.filter((c) => c.isActive).length || 0
const inactiveComponents = totalComponents - activeComponents const inactiveComponents = totalComponents - activeComponents
const { translate } = useLocalization() const { translate } = useLocalization()
const filterActiveOptions = [
{ value: 'all' as const, label: translate('::App.ComponentFilter.All') },
{ value: 'active' as const, label: translate('::App.EntityFilter.Active') },
{ value: 'inactive' as const, label: translate('::App.EntityFilter.Inactive') },
]
const { checkPermission } = usePermission() const { checkPermission } = usePermission()
const canCreate = checkPermission(COMPONENT_PERMISSION.CREATE) const canCreate = checkPermission(COMPONENT_PERMISSION.CREATE)
const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE) const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE)
@ -143,19 +150,13 @@ const ComponentManager: React.FC = () => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FaFilter className="w-5 h-5 text-slate-500 dark:text-gray-400" /> <FaFilter className="w-5 h-5 text-slate-500 dark:text-gray-400" />
<select <Select
value={filterActive} size="sm"
className="px-2 py-1 border border-slate-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" className="min-w-[9rem]"
onChange={(e) => setFilterActive(e.target.value as 'all' | 'active' | 'inactive')} options={filterActiveOptions}
> value={filterActiveOptions.filter((o) => o.value === filterActive)}
<option value="all">{translate('::App.ComponentFilter.All')}</option> onChange={(option) => setFilterActive(option?.value ?? 'all')}
<option value="active"> />
{translate('::App.EntityFilter.Active')}
</option>
<option value="inactive">
{translate('::App.EntityFilter.Inactive')}
</option>
</select>
</div> </div>
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"

View file

@ -18,9 +18,20 @@ import PageTitle from '@/components/shared/PageTitle'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
const DynamicServiceManager: React.FC = () => { const DynamicServiceManager: React.FC = () => {
const { translate } = useLocalization() const { translate } = useLocalization()
const filterStatusOptions = [
{ value: 'all' as const, label: translate('::App.StaticLookup.All') },
{ value: 'Success' as const, label: translate('::App.Platform.Success') },
{ value: 'Failed' as const, label: translate('::App.Platform.Failed') },
{
value: 'Pending' as const,
label: translate('::App.DeveloperKitDynamicServices.FilterPending'),
},
]
const [services, setServices] = useState<DynamicServiceDto[]>([]) const [services, setServices] = useState<DynamicServiceDto[]>([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [searchTerm, setSearchTerm] = useState('') const [searchTerm, setSearchTerm] = useState('')
@ -140,22 +151,12 @@ const DynamicServiceManager: React.FC = () => {
</div> </div>
<div className="flex items-center gap-2 w-full lg:w-auto"> <div className="flex items-center gap-2 w-full lg:w-auto">
<FaFilter className="w-4 h-4 text-slate-500 dark:text-gray-400" /> <FaFilter className="w-4 h-4 text-slate-500 dark:text-gray-400" />
<select <Select
value={filterStatus} className="w-full lg:w-48"
onChange={(e) => options={filterStatusOptions}
setFilterStatus(e.target.value as 'all' | 'Success' | 'Failed' | 'Pending') value={filterStatusOptions.filter((o) => o.value === filterStatus)}
} onChange={(option) => setFilterStatus(option?.value ?? 'all')}
className="w-full lg:w-auto px-3 py-2 border border-slate-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100" />
>
<option value="all">{translate('::App.StaticLookup.All')}</option>
<option value="Success">
{translate('::App.Platform.Success')}
</option>
<option value="Failed">{translate('::App.Platform.Failed')}</option>
<option value="Pending">
{translate('::App.DeveloperKitDynamicServices.FilterPending')}
</option>
</select>
</div> </div>
<div className="w-full sm:w-auto"> <div className="w-full sm:w-auto">
<Button <Button

View file

@ -7,6 +7,7 @@ import {
FaTable, FaTable,
FaPlus, FaPlus,
FaEye, FaEye,
FaProjectDiagram,
FaCog, FaCog,
FaCode, FaCode,
FaDatabase, FaDatabase,
@ -748,7 +749,7 @@ const SqlObjectExplorer = ({
closeCtx() closeCtx()
}} }}
> >
<FaEye className="text-purple-500" /> Design View <FaProjectDiagram className="text-purple-500" /> Design View
</Button> </Button>
)} )}

View file

@ -1,5 +1,5 @@
import { lazy, Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react' import { lazy, Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { Button, Dialog, Notification, toast } from '@/components/ui' import { Button, Dialog, Notification, Select, toast } from '@/components/ui'
import Container from '@/components/shared/Container' import Container from '@/components/shared/Container'
import { getDataSources } from '@/services/data-source.service' import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source' import type { DataSourceDto } from '@/proxy/data-source'
@ -272,6 +272,11 @@ const SqlQueryManager = () => {
`[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]` `[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]`
const getSafePgFullName = (schemaName: string, objectName: string) => const getSafePgFullName = (schemaName: string, objectName: string) =>
`"${escapePgIdentifier(schemaName)}"."${escapePgIdentifier(objectName)}"` `"${escapePgIdentifier(schemaName)}"."${escapePgIdentifier(objectName)}"`
const dataSourceOptions = state.dataSources.map((ds) => ({
value: ds.code ?? '',
label: ds.code ?? '',
}))
const selectedDataSourceType = state.dataSources.find( const selectedDataSourceType = state.dataSources.find(
(item) => item.code === state.selectedDataSource, (item) => item.code === state.selectedDataSource,
)?.dataSourceType )?.dataSourceType
@ -1086,21 +1091,20 @@ GO`,
<div className="flex flex-col gap-2 px-1 py-1 lg:flex-row lg:items-center lg:justify-between"> <div className="flex flex-col gap-2 px-1 py-1 lg:flex-row lg:items-center lg:justify-between">
<div className="flex flex-wrap items-center gap-2 sm:gap-3"> <div className="flex flex-wrap items-center gap-2 sm:gap-3">
<FaDatabase className="text-lg text-blue-500" /> <FaDatabase className="text-lg text-blue-500" />
<select <Select
className="border border-gray-300 rounded px-2 py-1 max-w-full dark:bg-gray-700 dark:border-gray-600" size="sm"
disabled={state.dataSources.length === 0} className="min-w-[12rem] max-w-full"
value={state.selectedDataSource || ''} isDisabled={state.dataSources.length === 0}
onChange={(e) => { menuPortalTarget={document.body}
const ds = state.dataSources.find((d) => d.code === e.target.value) options={dataSourceOptions}
value={dataSourceOptions.filter(
(option) => option.value === (state.selectedDataSource || ''),
)}
onChange={(option) => {
const ds = state.dataSources.find((d) => d.code === option?.value)
if (ds) handleDataSourceChange(ds) if (ds) handleDataSourceChange(ds)
}} }}
> />
{state.dataSources.map((ds) => (
<option key={ds.code} value={ds.code}>
{ds.code}
</option>
))}
</select>
<DbMigrateButton /> <DbMigrateButton />
{/* Seed dosyalari (configs/seeds) File Manager uzerinden yonetilir. */} {/* Seed dosyalari (configs/seeds) File Manager uzerinden yonetilir. */}
<Link to={ROUTES_ENUM.protected.admin.files} target="_blank"> <Link to={ROUTES_ENUM.protected.admin.files} target="_blank">

View file

@ -1,6 +1,6 @@
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react' import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui' import { Button, Checkbox, Dialog, Notification, Select, toast } from '@/components/ui'
import { import {
FaPlus, FaPlus,
FaTrash, FaTrash,
@ -1336,6 +1336,28 @@ const SqlTableDesignerDialog = ({
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([]) const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([]) const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
const [targetTableKeyColumns, setTargetTableKeyColumns] = useState<string[]>([]) const [targetTableKeyColumns, setTargetTableKeyColumns] = useState<string[]>([])
const dataTypeOptions = DATA_TYPES
const cascadeOptions = CASCADE_OPTIONS
const indexOrderOptions: { value: 'ASC' | 'DESC'; label: string }[] = [
{ value: 'ASC', label: 'ASC' },
{ value: 'DESC', label: 'DESC' },
]
const fkColumnOptions = columns
.filter((c) => c.columnName.trim())
.map((c) => ({ value: c.columnName, label: c.columnName }))
const referencedTableOptions = dbTables.map((t) => ({
value: t.tableName,
label: t.tableName,
}))
const referencedColumnOptions = targetTableColumns.map((col) => {
const isKey = targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
return {
value: col,
label: isKey ? `${col} (PK/UNIQUE)` : `${col} (Not Key)`,
isDisabled: !isKey,
}
})
const [targetColsLoading, setTargetColsLoading] = useState(false) const [targetColsLoading, setTargetColsLoading] = useState(false)
const [indexes, setIndexes] = useState<TableIndex[]>([]) const [indexes, setIndexes] = useState<TableIndex[]>([])
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([]) const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
@ -2374,11 +2396,9 @@ const SqlTableDesignerDialog = ({
> >
<div className="col-span-4"> <div className="col-span-4">
<Input <Input
unstyle size="sm"
type="text" type="text"
className={`w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white ${ className={isDuplicate ? 'w-full border-red-400' : 'w-full'}
isDuplicate ? 'border-red-400' : ''
}`}
placeholder={translate('::App.SqlQueryManager.ColumnNamePlaceholder')} placeholder={translate('::App.SqlQueryManager.ColumnNamePlaceholder')}
value={col.columnName} value={col.columnName}
onChange={(e) => updateColumn(col.id, 'columnName', e.target.value)} onChange={(e) => updateColumn(col.id, 'columnName', e.target.value)}
@ -2391,28 +2411,27 @@ const SqlTableDesignerDialog = ({
/> />
</div> </div>
<div className="col-span-3"> <div className="col-span-3">
<select <Select
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white" size="sm"
value={col.dataType} className="w-full"
onChange={(e) => { maxMenuHeight={200}
const dt = e.target.value as SqlDataType menuPortalTarget={document.body}
options={dataTypeOptions}
value={dataTypeOptions.filter((option) => option.value === col.dataType)}
onChange={(option) => {
if (!option) return
const dt = option.value as SqlDataType
updateColumn(col.id, 'dataType', dt) updateColumn(col.id, 'dataType', dt)
if (dt !== 'nvarchar') updateColumn(col.id, 'maxLength', '') if (dt !== 'nvarchar') updateColumn(col.id, 'maxLength', '')
else if (!col.maxLength) updateColumn(col.id, 'maxLength', '100') else if (!col.maxLength) updateColumn(col.id, 'maxLength', '100')
}} }}
> />
{DATA_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</select>
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<Input <Input
unstyle size="sm"
type="number" type="number"
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white text-center" className="w-full text-center"
placeholder="-" placeholder="-"
value={col.maxLength} value={col.maxLength}
disabled={col.dataType !== 'nvarchar'} disabled={col.dataType !== 'nvarchar'}
@ -2427,9 +2446,9 @@ const SqlTableDesignerDialog = ({
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Input <Input
unstyle size="sm"
type="text" type="text"
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white" className="w-full"
placeholder={translate('::Abp.Mailing.Default')} placeholder={translate('::Abp.Mailing.Default')}
value={col.defaultValue} value={col.defaultValue}
onChange={(e) => updateColumn(col.id, 'defaultValue', e.target.value)} onChange={(e) => updateColumn(col.id, 'defaultValue', e.target.value)}
@ -2558,10 +2577,10 @@ const SqlTableDesignerDialog = ({
{translate('::App.SqlQueryManager.EntityName')} <span className="text-red-500">*</span> {translate('::App.SqlQueryManager.EntityName')} <span className="text-red-500">*</span>
</label> </label>
<Input <Input
unstyle size="sm"
type="text" type="text"
disabled={isEditMode} // Entity name (and thus table name) cannot be changed in edit mode disabled={isEditMode} // Entity name (and thus table name) cannot be changed in edit mode
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white" className="w-full"
value={settings.entityName} value={settings.entityName}
onChange={(e) => onEntityNameChange(e.target.value)} onChange={(e) => onEntityNameChange(e.target.value)}
placeholder={translate('::App.SqlQueryManager.EntityNamePlaceholder')} placeholder={translate('::App.SqlQueryManager.EntityNamePlaceholder')}
@ -2574,10 +2593,10 @@ const SqlTableDesignerDialog = ({
{translate('::App.Listform.ListformField.TableName')} {translate('::App.Listform.ListformField.TableName')}
</label> </label>
<Input <Input
unstyle size="sm"
type="text" type="text"
readOnly readOnly
className="w-full px-3 py-2 text-sm border rounded bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 cursor-not-allowed" className="w-full bg-gray-50 cursor-not-allowed"
value={isEditMode ? (initialTableData?.tableName ?? '') : settings.tableName} value={isEditMode ? (initialTableData?.tableName ?? '') : settings.tableName}
placeholder={translate('::App.SqlQueryManager.TableNameAutoGenerated')} placeholder={translate('::App.SqlQueryManager.TableNameAutoGenerated')}
/> />
@ -2760,41 +2779,41 @@ const SqlTableDesignerDialog = ({
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> <label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
{translate('::App.SqlQueryManager.FkColumnInThisTable')} {translate('::App.SqlQueryManager.FkColumnInThisTable')}
</label> </label>
<select <Select
value={fkForm.fkColumnName} size="sm"
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))} className="w-full"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500" isClearable
> placeholder={translate('::App.Platform.Select')}
<option value="">{translate('::App.Platform.Select')}</option> menuPortalTarget={document.body}
{columns options={fkColumnOptions}
.filter((c) => c.columnName.trim()) value={fkColumnOptions.filter(
.map((c) => ( (option) => option.value === fkForm.fkColumnName,
<option key={c.id} value={c.columnName}> )}
{c.columnName} onChange={(option) =>
</option> setFkForm((f) => ({ ...f, fkColumnName: option?.value ?? '' }))
))} }
</select> />
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> <label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
{translate('::App.SqlQueryManager.TargetTable')} {translate('::App.SqlQueryManager.TargetTable')}
</label> </label>
<select <Select
value={fkForm.referencedTable} size="sm"
onChange={(e) => { className="w-full"
const val = e.target.value isClearable
placeholder={translate('::App.Platform.Select')}
menuPortalTarget={document.body}
options={referencedTableOptions}
value={referencedTableOptions.filter(
(option) => option.value === fkForm.referencedTable,
)}
onChange={(option) => {
const val = option?.value ?? ''
setFkForm((f) => ({ ...f, referencedTable: val, referencedColumn: '' })) setFkForm((f) => ({ ...f, referencedTable: val, referencedColumn: '' }))
loadTargetColumns(val) loadTargetColumns(val)
}} }}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500" />
>
<option value="">{translate('::App.Platform.Select')}</option>
{dbTables.map((t) => (
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
{t.tableName}
</option>
))}
</select>
</div> </div>
</div> </div>
@ -2805,29 +2824,21 @@ const SqlTableDesignerDialog = ({
? `${translate('::App.Platform.LoadingWithThreeDot')}` ? `${translate('::App.Platform.LoadingWithThreeDot')}`
: ''} : ''}
</label> </label>
<select <Select
value={fkForm.referencedColumn} size="sm"
onChange={(e) => setFkForm((f) => ({ ...f, referencedColumn: e.target.value }))} className="w-full"
disabled={targetColsLoading || targetTableColumns.length === 0} isClearable
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60" isDisabled={targetColsLoading || targetTableColumns.length === 0}
> placeholder={translate('::App.SqlQueryManager.SelectTargetTableFirst')}
<option value=""> menuPortalTarget={document.body}
{translate('::App.SqlQueryManager.SelectTargetTableFirst')} options={referencedColumnOptions}
</option> value={referencedColumnOptions.filter(
{targetTableColumns.map((col) => ( (option) => option.value === fkForm.referencedColumn,
<option )}
key={col} onChange={(option) =>
value={col} setFkForm((f) => ({ ...f, referencedColumn: option?.value ?? '' }))
disabled={ }
!targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase()) />
}
>
{targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
? `${col} (PK/UNIQUE)`
: `${col} (Not Key)`}
</option>
))}
</select>
{fkForm.referencedTable && {fkForm.referencedTable &&
!targetColsLoading && !targetColsLoading &&
targetTableKeyColumns.length === 0 && ( targetTableKeyColumns.length === 0 && (
@ -2843,43 +2854,43 @@ const SqlTableDesignerDialog = ({
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> <label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
{translate('::App.SqlQueryManager.CascadeUpdate')} {translate('::App.SqlQueryManager.CascadeUpdate')}
</label> </label>
<select <Select
value={fkForm.cascadeUpdate} size="sm"
onChange={(e) => className="w-full"
menuPortalTarget={document.body}
options={cascadeOptions}
value={cascadeOptions.filter(
(option) => option.value === fkForm.cascadeUpdate,
)}
onChange={(option) =>
option &&
setFkForm((f) => ({ setFkForm((f) => ({
...f, ...f,
cascadeUpdate: e.target.value as CascadeBehavior, cascadeUpdate: option.value as CascadeBehavior,
})) }))
} }
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500" />
>
{CASCADE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> <label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
{translate('::App.SqlQueryManager.CascadeDelete')} {translate('::App.SqlQueryManager.CascadeDelete')}
</label> </label>
<select <Select
value={fkForm.cascadeDelete} size="sm"
onChange={(e) => className="w-full"
menuPortalTarget={document.body}
options={cascadeOptions}
value={cascadeOptions.filter(
(option) => option.value === fkForm.cascadeDelete,
)}
onChange={(option) =>
option &&
setFkForm((f) => ({ setFkForm((f) => ({
...f, ...f,
cascadeDelete: e.target.value as CascadeBehavior, cascadeDelete: option.value as CascadeBehavior,
})) }))
} }
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500" />
>
{CASCADE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</div> </div>
</div> </div>
@ -2887,11 +2898,11 @@ const SqlTableDesignerDialog = ({
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<Input <Input
unstyle size="sm"
type="checkbox" type="checkbox"
checked={fkForm.isRequired} checked={fkForm.isRequired}
onChange={(e) => setFkForm((f) => ({ ...f, isRequired: e.target.checked }))} onChange={(e) => setFkForm((f) => ({ ...f, isRequired: e.target.checked }))}
className="w-4 h-4 text-indigo-600 rounded" className="w-4 text-indigo-600"
/> />
<span className="text-sm text-gray-700 dark:text-gray-300"> <span className="text-sm text-gray-700 dark:text-gray-300">
{translate('::App.Listform.ListformField.Required')} {translate('::App.Listform.ListformField.Required')}
@ -3135,14 +3146,14 @@ const SqlTableDesignerDialog = ({
{translate('::App.SqlQueryManager.IndexConstraintName')} {translate('::App.SqlQueryManager.IndexConstraintName')}
</label> </label>
<Input <Input
unstyle size="sm"
type="text" type="text"
value={indexForm.indexName} value={indexForm.indexName}
onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))} onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))}
placeholder={ placeholder={
buildIndexName(indexForm.indexType, indexForm.columns) || `PK_EntityName_Id` buildIndexName(indexForm.indexType, indexForm.columns) || `PK_EntityName_Id`
} }
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500" className="w-full"
/> />
</div> </div>
@ -3150,13 +3161,13 @@ const SqlTableDesignerDialog = ({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<Input <Input
unstyle size="sm"
type="checkbox" type="checkbox"
checked={indexForm.isClustered} checked={indexForm.isClustered}
onChange={(e) => onChange={(e) =>
setIndexForm((f) => ({ ...f, isClustered: e.target.checked })) setIndexForm((f) => ({ ...f, isClustered: e.target.checked }))
} }
className="w-4 h-4 text-indigo-600 rounded" className="w-4 text-indigo-600"
/> />
<span className="text-sm text-gray-700 dark:text-gray-300">Clustered</span> <span className="text-sm text-gray-700 dark:text-gray-300">Clustered</span>
</label> </label>
@ -3191,7 +3202,7 @@ const SqlTableDesignerDialog = ({
> >
<div className="col-span-1"> <div className="col-span-1">
<Input <Input
unstyle size="sm"
type="checkbox" type="checkbox"
checked={!!existing} checked={!!existing}
onChange={(e) => { onChange={(e) => {
@ -3220,7 +3231,7 @@ const SqlTableDesignerDialog = ({
}) })
} }
}} }}
className="w-4 h-4 text-indigo-600 rounded" className="w-4 text-indigo-600"
/> />
</div> </div>
<div className="col-span-7 text-sm font-mono text-gray-800 dark:text-gray-200"> <div className="col-span-7 text-sm font-mono text-gray-800 dark:text-gray-200">
@ -3228,23 +3239,26 @@ const SqlTableDesignerDialog = ({
</div> </div>
<div className="col-span-4"> <div className="col-span-4">
{existing && ( {existing && (
<select <Select
value={existing.order} size="sm"
onChange={(e) => className="w-full"
menuPortalTarget={document.body}
options={indexOrderOptions}
value={indexOrderOptions.filter(
(option) => option.value === existing.order,
)}
onChange={(option) =>
option &&
setIndexForm((f) => ({ setIndexForm((f) => ({
...f, ...f,
columns: f.columns.map((ic) => columns: f.columns.map((ic) =>
ic.columnName === col.columnName ic.columnName === col.columnName
? { ...ic, order: e.target.value as 'ASC' | 'DESC' } ? { ...ic, order: option.value }
: ic, : ic,
), ),
})) }))
} }
className="w-full px-2 py-0.5 text-xs border border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700 dark:text-white" />
>
<option value="ASC">ASC</option>
<option value="DESC">DESC</option>
</select>
)} )}
</div> </div>
</div> </div>

View file

@ -612,7 +612,7 @@ const SqlViewDesignerDialog = ({
</span> </span>
<Input <Input
unstyle unstyle
className={`${inputCls} w-[190px]`} className={`${inputCls} w-[320px]`}
placeholder="Vw_OrderSummary" placeholder="Vw_OrderSummary"
value={settings.viewName} value={settings.viewName}
disabled={isEditMode} disabled={isEditMode}

View file

@ -39,7 +39,7 @@ import { getList } from '@/services/form.service'
import type { GridDto } from '@/proxy/form/models' import type { GridDto } from '@/proxy/form/models'
import { getListForms } from '@/services/admin/list-form.service' import { getListForms } from '@/services/admin/list-form.service'
import { developerKitService } from '@/services/developerKit.service' import { developerKitService } from '@/services/developerKit.service'
import { Button, Notification, toast } from '@/components/ui' import { Button, Notification, Select, toast } from '@/components/ui'
import StyleModal from '@/components/codeLayout/StyleModal' import StyleModal from '@/components/codeLayout/StyleModal'
import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas' import VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
import { import {
@ -1164,22 +1164,20 @@ const PropertyEditor = ({
if (type === 'select' && options) { if (type === 'select' && options) {
const currentValue = String(value ?? '') const currentValue = String(value ?? '')
return ( return (
<select /* An empty value is not emitted, so the component keeps its own or its
className={inputClass} container's default spell that out instead of showing a blank row. */
value={currentValue} <Select
onChange={(event) => onChange(event.target.value)} size="xs"
> className="w-full"
{/* An empty value is not emitted, so the component keeps its own or its menuPortalTarget={window.document.body}
container's default spell that out instead of showing a blank row. */} options={options.map((option) => ({ value: option, label: option }))}
{!options.includes(currentValue) && ( value={
<option value="">{translate('::Abp.Mailing.Default')}</option> options.includes(currentValue)
)} ? { value: currentValue, label: currentValue }
{options.map((option) => ( : { value: '', label: translate('::Abp.Mailing.Default') }
<option key={option} value={option}> }
{option} onChange={(option) => onChange(option?.value ?? '')}
</option> />
))}
</select>
) )
} }
// An icon prop holds a name; the picker is what makes that name findable and // An icon prop holds a name; the picker is what makes that name findable and
@ -3513,29 +3511,33 @@ const VisualComponentDesigner = () => {
<span className="mb-1 block text-[10px] font-semibold text-slate-500"> <span className="mb-1 block text-[10px] font-semibold text-slate-500">
Koleksiyon Koleksiyon
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={selectOptionsBinding.path} className="w-full"
onChange={(event) => placeholder={translate(
'::App.DeveloperKitComponentDesigner.NoCollectionFound',
)}
menuPortalTarget={window.document.body}
options={selectCollectionPaths.map((path) => ({
value: path,
label:
path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
}))}
value={{
value: selectOptionsBinding.path,
label:
selectOptionsBinding.path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
}}
onChange={(option) =>
updateSelectedBindingDetails(optionDataProperty, { updateSelectedBindingDetails(optionDataProperty, {
path: event.target.value, path: option?.value ?? '',
labelPath: '', labelPath: '',
valuePath: '', valuePath: '',
}) })
} }
> />
{!selectCollectionPaths.length && (
<option value="">
{translate('::App.DeveloperKitComponentDesigner.NoCollectionFound')}
</option>
)}
{selectCollectionPaths.map((path) => (
<option key={path || '__root__'} value={path}>
{path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray')}
</option>
))}
</select>
</label> </label>
{selectCollectionSample && ( {selectCollectionSample && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
@ -3549,28 +3551,34 @@ const VisualComponentDesigner = () => {
<span className="mb-1 block text-[10px] font-semibold text-slate-500"> <span className="mb-1 block text-[10px] font-semibold text-slate-500">
{translate('::' + label)} {translate('::' + label)}
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={selectOptionsBinding[key] || ''} className="w-full"
onChange={(event) => isClearable
placeholder={translate(
selectColumnFields.length
? '::App.DeveloperKitComponentDesigner.SelectColumn'
: '::App.DeveloperKitComponentDesigner.ItemItself',
)}
menuPortalTarget={window.document.body}
options={selectColumnFields.map((field) => ({
value: field.path,
label: field.path,
}))}
value={
selectOptionsBinding[key]
? {
value: selectOptionsBinding[key] as string,
label: selectOptionsBinding[key] as string,
}
: null
}
onChange={(option) =>
updateSelectedBindingDetails(optionDataProperty, { updateSelectedBindingDetails(optionDataProperty, {
[key]: event.target.value, [key]: option?.value ?? '',
}) })
} }
> />
<option value="">
{translate(
selectColumnFields.length
? '::App.DeveloperKitComponentDesigner.SelectColumn'
: '::App.DeveloperKitComponentDesigner.ItemItself',
)}
</option>
{selectColumnFields.map((field) => (
<option key={field.path} value={field.path}>
{field.path}
</option>
))}
</select>
</label> </label>
))} ))}
</div> </div>
@ -3609,28 +3617,25 @@ const VisualComponentDesigner = () => {
</button> </button>
</span> </span>
))} ))}
<select <Select
className="rounded-md border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value="" className="min-w-[8rem]"
onChange={(event) => { placeholder={translate(
const next = event.target.value '::App.DeveloperKitComponentDesigner.AddColumn',
)}
menuPortalTarget={window.document.body}
options={selectColumnFields
.filter((field) => !selectOptionColumns.includes(field.path))
.map((field) => ({ value: field.path, label: field.path }))}
value={null}
onChange={(option) => {
const next = option?.value
if (!next || selectOptionColumns.includes(next)) return if (!next || selectOptionColumns.includes(next)) return
updateSelectedBindingDetails(optionDataProperty, { updateSelectedBindingDetails(optionDataProperty, {
columns: [...selectOptionColumns, next], columns: [...selectOptionColumns, next],
}) })
}} }}
> />
<option value="">
{translate('::App.DeveloperKitComponentDesigner.AddColumn')}
</option>
{selectColumnFields
.filter((field) => !selectOptionColumns.includes(field.path))
.map((field) => (
<option key={field.path} value={field.path}>
{field.path}
</option>
))}
</select>
</div> </div>
<p className="mt-1 text-[10px] leading-4 text-slate-500"> <p className="mt-1 text-[10px] leading-4 text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.ExtraColumnsHint')}{' '} {translate('::App.DeveloperKitComponentDesigner.ExtraColumnsHint')}{' '}
@ -3764,42 +3769,54 @@ const VisualComponentDesigner = () => {
</label> </label>
{lookup && ( {lookup && (
<div className="mt-2 space-y-1.5"> <div className="mt-2 space-y-1.5">
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={lookup.sourceId} className="w-full"
onChange={(event) => selectColumnLookupSource(column, event.target.value)} isClearable
> placeholder={translate(
<option value=""> '::App.DeveloperKitComponentDesigner.SelectEndpoint',
{translate('::App.DeveloperKitComponentDesigner.SelectEndpoint')} )}
</option> menuPortalTarget={window.document.body}
{bindableDataSources.map((source) => ( options={bindableDataSources.map((source) => ({
<option key={source.id} value={source.id}> value: source.id,
{source.name} label: source.name,
</option> }))}
))} value={bindableDataSources
</select> .filter((source) => source.id === lookup.sourceId)
.map((source) => ({ value: source.id, label: source.name }))}
onChange={(option) =>
selectColumnLookupSource(column, option?.value ?? '')
}
/>
{lookup.sourceId && collectionPaths.length > 1 && ( {lookup.sourceId && collectionPaths.length > 1 && (
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={lookup.path} className="w-full"
onChange={(event) => isClearable
maxMenuHeight={200}
placeholder={translate(
'::App.DeveloperKitComponentDesigner.SelectCollection',
)}
menuPortalTarget={window.document.body}
options={collectionPaths.map((path) => ({
value: path,
label:
path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
}))}
value={
lookup.path
? { value: lookup.path, label: lookup.path }
: null
}
onChange={(option) =>
writeColumnLookup(column, { writeColumnLookup(column, {
path: event.target.value, path: option?.value ?? '',
valueField: '', valueField: '',
textField: '', textField: '',
}) })
} }
> />
<option value="">
{translate('::App.DeveloperKitComponentDesigner.SelectCollection')}
</option>
{collectionPaths.map((path) => (
<option key={path || '__root__'} value={path}>
{path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray')}
</option>
))}
</select>
)} )}
{lookup.sourceId && ( {lookup.sourceId && (
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
@ -3813,26 +3830,29 @@ const VisualComponentDesigner = () => {
<span className="mb-0.5 block text-[9px] text-slate-400"> <span className="mb-0.5 block text-[9px] text-slate-400">
{translate(label)} {translate(label)}
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-1.5 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={lookup[field]} className="w-full"
onChange={(event) => isClearable
writeColumnLookup(column, { [field]: event.target.value }) maxMenuHeight={200}
} placeholder={
> rowFields.length
<option value="">
{rowFields.length
? translate('::App.DeveloperKitComponentDesigner.SelectField') ? translate('::App.DeveloperKitComponentDesigner.SelectField')
: translate( : translate(
'::App.DeveloperKitComponentDesigner.NoColumnInResponse', '::App.DeveloperKitComponentDesigner.NoColumnInResponse',
)} )
</option> }
{rowFields.map((path) => ( menuPortalTarget={window.document.body}
<option key={path} value={path}> options={rowFields.map((path) => ({ value: path, label: path }))}
{path} value={
</option> lookup[field]
))} ? { value: lookup[field], label: lookup[field] }
</select> : null
}
onChange={(option) =>
writeColumnLookup(column, { [field]: option?.value ?? '' })
}
/>
</label> </label>
))} ))}
</div> </div>
@ -3987,6 +4007,15 @@ const VisualComponentDesigner = () => {
const patchFilter = (id: string, updates: Partial<DesignerDataSourceFilter>) => const patchFilter = (id: string, updates: Partial<DesignerDataSourceFilter>) =>
writeFilters(filters.map((filter) => (filter.id === id ? { ...filter, ...updates } : filter))) writeFilters(filters.map((filter) => (filter.id === id ? { ...filter, ...updates } : filter)))
const filterSourceOptions = DESIGNER_FILTER_SOURCES.map((item) => ({
value: item,
label: translate(
`::App.DeveloperKitComponentDesigner.FilterSource${
item.charAt(0).toUpperCase() + item.slice(1)
}`,
),
}))
return ( return (
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800"> <div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
@ -4038,25 +4067,28 @@ const VisualComponentDesigner = () => {
> >
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{strictColumns ? ( {strictColumns ? (
<select <Select
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-1 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={filter.field || ''} className="min-w-0 flex-1"
onChange={(event) => patchFilter(filter.id, { field: event.target.value })} isClearable
> maxMenuHeight={200}
<option value=""> placeholder={translate(
{translate('::App.DeveloperKitComponentDesigner.FilterColumn')} '::App.DeveloperKitComponentDesigner.FilterColumn',
</option> )}
{/* A field configured before the list form changed is kept in menuPortalTarget={window.document.body}
the list, so switching forms does not silently blank it. */} options={(filter.field && !columns.includes(filter.field)
{(filter.field && !columns.includes(filter.field)
? [filter.field, ...columns] ? [filter.field, ...columns]
: columns : columns
).map((column) => ( ).map((column) => ({ value: column, label: column }))}
<option key={column} value={column}> value={
{column} filter.field
</option> ? { value: filter.field, label: filter.field }
))} : null
</select> }
onChange={(option) =>
patchFilter(filter.id, { field: option?.value ?? '' })
}
/>
) : ( ) : (
<Input <Input
unstyle unstyle
@ -4067,21 +4099,26 @@ const VisualComponentDesigner = () => {
onChange={(event) => patchFilter(filter.id, { field: event.target.value })} onChange={(event) => patchFilter(filter.id, { field: event.target.value })}
/> />
)} )}
<select <Select
className="w-24 rounded border border-slate-300 bg-white px-1 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={filter.operator} className="w-24"
onChange={(event) => maxMenuHeight={200}
menuPortalTarget={window.document.body}
options={DESIGNER_FILTER_OPERATORS.map((operator) => ({
value: operator,
label: FILTER_OPERATOR_LABELS[operator],
}))}
value={{
value: filter.operator,
label: FILTER_OPERATOR_LABELS[filter.operator],
}}
onChange={(option) =>
option &&
patchFilter(filter.id, { patchFilter(filter.id, {
operator: event.target.value as DesignerFilterOperator, operator: option.value as DesignerFilterOperator,
}) })
} }
> />
{DESIGNER_FILTER_OPERATORS.map((operator) => (
<option key={operator} value={operator}>
{FILTER_OPERATOR_LABELS[operator]}
</option>
))}
</select>
<button <button
className="rounded p-1 text-slate-400 hover:text-red-600" className="rounded p-1 text-slate-400 hover:text-red-600"
title={translate('::App.Platform.Delete')} title={translate('::App.Platform.Delete')}
@ -4095,26 +4132,23 @@ const VisualComponentDesigner = () => {
</div> </div>
{!valueless && ( {!valueless && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<select <Select
className="w-24 rounded border border-slate-300 bg-white px-1 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={filter.source} className="w-24"
onChange={(event) => maxMenuHeight={200}
menuPortalTarget={window.document.body}
options={filterSourceOptions}
value={filterSourceOptions.filter(
(option) => option.value === filter.source,
)}
onChange={(option) =>
option &&
patchFilter(filter.id, { patchFilter(filter.id, {
source: event.target.value as DesignerFilterSource, source: option.value as DesignerFilterSource,
value: '', value: '',
}) })
} }
> />
{DESIGNER_FILTER_SOURCES.map((item) => (
<option key={item} value={item}>
{translate(
`::App.DeveloperKitComponentDesigner.FilterSource${
item.charAt(0).toUpperCase() + item.slice(1)
}`,
)}
</option>
))}
</select>
{filter.source !== 'record' && ( {filter.source !== 'record' && (
<Input <Input
unstyle unstyle
@ -4135,24 +4169,28 @@ const VisualComponentDesigner = () => {
next to the source selector left neither of them usable. */} next to the source selector left neither of them usable. */}
{!valueless && filter.source === 'record' && ( {!valueless && filter.source === 'record' && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<select <Select
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-1 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={masterRef} className="min-w-0 flex-1"
onChange={(event) => isClearable
maxMenuHeight={200}
placeholder={translate(
'::App.DeveloperKitComponentDesigner.FilterMaster',
)}
menuPortalTarget={window.document.body}
options={sqlContainerRefs.map((item) => ({
value: item.ref,
label: item.ref,
}))}
value={masterRef ? { value: masterRef, label: masterRef } : null}
onChange={(option) =>
patchFilter(filter.id, { patchFilter(filter.id, {
value: masterColumn ? `${event.target.value}.${masterColumn}` : event.target.value, value: masterColumn
? `${option?.value ?? ''}.${masterColumn}`
: (option?.value ?? ''),
}) })
} }
> />
<option value="">
{translate('::App.DeveloperKitComponentDesigner.FilterMaster')}
</option>
{sqlContainerRefs.map((item) => (
<option key={item.ref} value={item.ref}>
{item.ref}
</option>
))}
</select>
<Input <Input
unstyle unstyle
className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900" className="min-w-0 flex-1 rounded border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
@ -4264,11 +4302,25 @@ const VisualComponentDesigner = () => {
<p className="mb-2 text-[10px] leading-4 text-slate-500"> <p className="mb-2 text-[10px] leading-4 text-slate-500">
{translate('::' + slot.description)} {translate('::' + slot.description)}
</p> </p>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={currentValue} className="w-full"
onChange={(event) => { isClearable
const sourceId = event.target.value maxMenuHeight={200}
placeholder={translate('::App.DeveloperKitComponentDesigner.NotDefined')}
menuPortalTarget={window.document.body}
options={options.map((source) => ({
value: source.id,
label: `${source.name} · ${source.url}`,
}))}
value={options
.filter((source) => source.id === currentValue)
.map((source) => ({
value: source.id,
label: `${source.name} · ${source.url}`,
}))}
onChange={(option) => {
const sourceId = option?.value ?? ''
updateSelectedProp(slot.property, sourceId) updateSelectedProp(slot.property, sourceId)
const source = document.dataSources.find((item) => item.id === sourceId) const source = document.dataSources.find((item) => item.id === sourceId)
if ( if (
@ -4279,16 +4331,7 @@ const VisualComponentDesigner = () => {
void testDataSource(source) void testDataSource(source)
} }
}} }}
> />
<option value="">
{translate('::App.DeveloperKitComponentDesigner.NotDefined')}
</option>
{options.map((source) => (
<option key={source.id} value={source.id}>
{source.name} · {source.url}
</option>
))}
</select>
{!options.length && ( {!options.length && (
<p className="mt-1.5 text-[10px] leading-4 text-amber-600"> <p className="mt-1.5 text-[10px] leading-4 text-amber-600">
{translate('::App.DeveloperKitComponentDesigner.NoEndpointForMethod', { {translate('::App.DeveloperKitComponentDesigner.NoEndpointForMethod', {
@ -4316,17 +4359,26 @@ const VisualComponentDesigner = () => {
<span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400"> <span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
Koleksiyon Koleksiyon
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={String(selectedNode.props.collectionPath ?? '')} className="w-full"
onChange={(event) => updateSelectedProp('collectionPath', event.target.value)} maxMenuHeight={200}
> menuPortalTarget={window.document.body}
{collectionPaths.map((path) => ( options={collectionPaths.map((path) => ({
<option key={path || '__root__'} value={path}> value: path,
{path || translate('::App.DeveloperKitComponentDesigner.ResponseItself')} label:
</option> path || translate('::App.DeveloperKitComponentDesigner.ResponseItself'),
))} }))}
</select> value={{
value: String(selectedNode.props.collectionPath ?? ''),
label:
String(selectedNode.props.collectionPath ?? '') ||
translate('::App.DeveloperKitComponentDesigner.ResponseItself'),
}}
onChange={(option) =>
updateSelectedProp('collectionPath', option?.value ?? '')
}
/>
</label> </label>
</div> </div>
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800"> <div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
@ -4336,17 +4388,17 @@ const VisualComponentDesigner = () => {
<p className="mb-2 text-[10px] leading-4 text-slate-500"> <p className="mb-2 text-[10px] leading-4 text-slate-500">
{translate('::App.DeveloperKitComponentDesigner.KeyParamHint')} {translate('::App.DeveloperKitComponentDesigner.KeyParamHint')}
</p> </p>
<select <Select
className="mb-2 w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={getSqlDataSourceKeySource(selectedNode)} className="mb-2 w-full"
onChange={(event) => updateSelectedProp('keySource', event.target.value)} maxMenuHeight={200}
> menuPortalTarget={window.document.body}
{SQL_DATA_SOURCE_KEY_SOURCES.map((item) => ( options={SQL_DATA_SOURCE_KEY_SOURCES}
<option key={item.value} value={item.value}> value={SQL_DATA_SOURCE_KEY_SOURCES.filter(
{item.label} (item) => item.value === getSqlDataSourceKeySource(selectedNode),
</option> )}
))} onChange={(option) => option && updateSelectedProp('keySource', option.value)}
</select> />
<Input <Input
unstyle unstyle
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900"
@ -4499,6 +4551,10 @@ const VisualComponentDesigner = () => {
const column = binding?.sourceId === sqlScopeNode.id ? binding.path : '' const column = binding?.sourceId === sqlScopeNode.id ? binding.path : ''
const rawDefault = selectedNode.props?.[SQL_DEFAULT_VALUE_PROP] const rawDefault = selectedNode.props?.[SQL_DEFAULT_VALUE_PROP]
const currentDefault = rawDefault === undefined || rawDefault === null ? '' : String(rawDefault) const currentDefault = rawDefault === undefined || rawDefault === null ? '' : String(rawDefault)
const sqlDefaultValueOptions = [
{ value: 'true', label: translate('::App.Platform.Yes') },
{ value: 'false', label: translate('::App.Platform.No') },
]
const columnListId = `sql-columns-${sqlScopeNode.id}` const columnListId = `sql-columns-${sqlScopeNode.id}`
// A picker gets a real date editor, unless it holds a token — `@today` is not // A picker gets a real date editor, unless it holds a token — `@today` is not
// a date the browser can render, and typing it needs a plain text field. // a date the browser can render, and typing it needs a plain text field.
@ -4552,18 +4608,24 @@ const VisualComponentDesigner = () => {
})} })}
</p> </p>
{sqlRecordProperty === 'checked' ? ( {sqlRecordProperty === 'checked' ? (
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] disabled:opacity-50 dark:border-slate-700 dark:bg-slate-900" size="xs"
disabled={!column} className="w-full"
value={currentDefault} isClearable
onChange={(event) => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, event.target.value)} isDisabled={!column}
> maxMenuHeight={200}
<option value=""> placeholder={translate(
{translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')} '::App.DeveloperKitComponentDesigner.NoDefaultValue',
</option> )}
<option value="true">{translate('::App.Platform.Yes')}</option> menuPortalTarget={window.document.body}
<option value="false">{translate('::App.Platform.No')}</option> options={sqlDefaultValueOptions}
</select> value={sqlDefaultValueOptions.filter(
(option) => option.value === currentDefault,
)}
onChange={(option) =>
updateSelectedProp(SQL_DEFAULT_VALUE_PROP, option?.value ?? '')
}
/>
) : ( ) : (
<Input <Input
unstyle unstyle
@ -4665,11 +4727,24 @@ const VisualComponentDesigner = () => {
{sqlScopeSource?.name} {sqlScopeSource?.name}
</div> </div>
) : ( ) : (
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2.5 py-2 text-xs dark:border-slate-700 dark:bg-slate-900" size="sm"
value={activeDataSource?.id || ''} className="w-full"
onChange={(event) => { isClearable
const sourceId = event.target.value maxMenuHeight={200}
placeholder={translate(
'::App.DeveloperKitComponentDesigner.SelectEndpoint',
)}
menuPortalTarget={window.document.body}
options={inspectorDataSources.map((source) => ({
value: source.id,
label: source.name,
}))}
value={inspectorDataSources
.filter((source) => source.id === (activeDataSource?.id || ''))
.map((source) => ({ value: source.id, label: source.name }))}
onChange={(option) => {
const sourceId = option?.value ?? ''
setDataPanelSourceId(sourceId) setDataPanelSourceId(sourceId)
if ( if (
(isOptionDataComponent(selectedNode?.type) || (isOptionDataComponent(selectedNode?.type) ||
@ -4703,20 +4778,7 @@ const VisualComponentDesigner = () => {
void testDataSource(source) void testDataSource(source)
} }
}} }}
> />
{/* Without this a browser paints the first option while the
value is still empty, which reads as a made choice. */}
{!activeDataSource && (
<option value="">
{translate('::App.DeveloperKitComponentDesigner.SelectEndpoint')}
</option>
)}
{inspectorDataSources.map((source) => (
<option key={source.id} value={source.id}>
{source.name}
</option>
))}
</select>
)} )}
</label> </label>
</div> </div>
@ -4733,24 +4795,30 @@ const VisualComponentDesigner = () => {
<span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400"> <span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
Koleksiyon Koleksiyon
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={tabularItemsBinding.path} className="w-full"
onChange={(event) => maxMenuHeight={200}
updateSelectedBindingDetails('items', { path: event.target.value }) placeholder={translate(
} '::App.DeveloperKitComponentDesigner.NoCollectionFound',
>
{!tabularCollectionPaths.length && (
<option value="">
{translate('::App.DeveloperKitComponentDesigner.NoCollectionFound')}
</option>
)} )}
{tabularCollectionPaths.map((path) => ( menuPortalTarget={window.document.body}
<option key={path || '__root__'} value={path}> options={tabularCollectionPaths.map((path) => ({
{path || translate('::App.DeveloperKitComponentDesigner.WholeResponseArray')} value: path,
</option> label:
))} path ||
</select> translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
}))}
value={{
value: tabularItemsBinding.path,
label:
tabularItemsBinding.path ||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
}}
onChange={(option) =>
updateSelectedBindingDetails('items', { path: option?.value ?? '' })
}
/>
</label> </label>
</div> </div>
)} )}
@ -4871,6 +4939,29 @@ const VisualComponentDesigner = () => {
choice === '__root__' || choice === '__root__' ||
selectableFields.some((field) => field.path === choice) selectableFields.some((field) => field.path === choice)
const bindingChoiceOptions = [
...(activeBindingSample !== undefined &&
(collection || !Array.isArray(activeBindingSample) || isActiveRepeatedSource)
? [
{
value: '__root__',
label: translate(
isActiveRepeatedSource
? '::App.DeveloperKitComponentDesigner.CurrentGridRow'
: collection
? '::App.DeveloperKitComponentDesigner.WholeCollection'
: '::App.DeveloperKitComponentDesigner.WholeResponse',
),
},
]
: []),
...(!knownChoice ? [{ value: choice, label: `${choice} (mevcut path)` }] : []),
...selectableFields.map((field) => ({
value: field.path,
label: `${field.path} · ${field.type}`,
})),
]
return ( return (
<label <label
key={property.name} key={property.name}
@ -4882,11 +4973,21 @@ const VisualComponentDesigner = () => {
{property.tsType || property.type} {property.tsType || property.type}
</span> </span>
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-2 py-2 text-[10px] dark:border-slate-700 dark:bg-slate-900" size="xs"
value={choice} className="w-full"
onChange={(event) => { isClearable
const nextChoice = event.target.value maxMenuHeight={200}
placeholder={translate(
'::App.DeveloperKitComponentDesigner.StaticValueNoBinding',
)}
menuPortalTarget={window.document.body}
options={bindingChoiceOptions}
value={bindingChoiceOptions.filter(
(option) => option.value === choice,
)}
onChange={(option) => {
const nextChoice = option?.value
if (!nextChoice) { if (!nextChoice) {
updateSelectedBinding(property.name, '') updateSelectedBinding(property.name, '')
return return
@ -4898,31 +4999,7 @@ const VisualComponentDesigner = () => {
getBindingPath(nextChoice, collection), getBindingPath(nextChoice, collection),
) )
}} }}
> />
<option value="">
{translate('::App.DeveloperKitComponentDesigner.StaticValueNoBinding')}
</option>
{activeBindingSample !== undefined &&
(collection ||
!Array.isArray(activeBindingSample) ||
isActiveRepeatedSource) && (
<option value="__root__">
{translate(
isActiveRepeatedSource
? '::App.DeveloperKitComponentDesigner.CurrentGridRow'
: collection
? '::App.DeveloperKitComponentDesigner.WholeCollection'
: '::App.DeveloperKitComponentDesigner.WholeResponse',
)}
</option>
)}
{!knownChoice && <option value={choice}>{choice} (mevcut path)</option>}
{selectableFields.map((field) => (
<option key={field.path} value={field.path}>
{field.path} · {field.type}
</option>
))}
</select>
{currentBinding?.sourceId && !foreignBinding && ( {currentBinding?.sourceId && !foreignBinding && (
<div className="mt-1.5 truncate font-mono text-[9px] text-emerald-600"> <div className="mt-1.5 truncate font-mono text-[9px] text-emerald-600">
{currentBindingSource?.name || currentBinding.sourceId}:{' '} {currentBindingSource?.name || currentBinding.sourceId}:{' '}
@ -6021,21 +6098,26 @@ const VisualComponentDesigner = () => {
<span className="mb-1.5 block text-[10px] font-semibold uppercase text-slate-500"> <span className="mb-1.5 block text-[10px] font-semibold uppercase text-slate-500">
Metot Metot
</span> </span>
<select <Select
className="w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-xs text-slate-800 outline-none focus:border-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100" size="sm"
value={catalogSourceEditor.draft.method} className="w-full"
onChange={(event) => maxMenuHeight={200}
menuPortalTarget={window.document.body}
options={DESIGNER_HTTP_METHODS.map((method) => ({
value: method,
label: method,
}))}
value={{
value: catalogSourceEditor.draft.method,
label: catalogSourceEditor.draft.method,
}}
onChange={(option) =>
option &&
updateCatalogSourceDraft({ updateCatalogSourceDraft({
method: event.target.value as DesignerHttpMethod, method: option.value as DesignerHttpMethod,
}) })
} }
> />
{DESIGNER_HTTP_METHODS.map((method) => (
<option key={method} value={method}>
{method}
</option>
))}
</select>
</label> </label>
</div> </div>
<label className="block"> <label className="block">

View file

@ -9,6 +9,7 @@ import { useState } from 'react'
import { FaCode, FaGripVertical, FaMagic, FaPlus, FaTrash } from 'react-icons/fa' import { FaCode, FaGripVertical, FaMagic, FaPlus, FaTrash } from 'react-icons/fa'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import { import {
GROUP_MODES, GROUP_MODES,
GROUP_MODE_LABEL, GROUP_MODE_LABEL,
@ -86,6 +87,19 @@ export function CriteriaGrid({
const orCount = orColumnCount(rows) const orCount = orColumnCount(rows)
const sourceOptions = sources.map((s) => ({
value: s.id,
label: s.alias || s.objectName,
}))
const groupModeOptions = GROUP_MODES.map((m) => ({
value: m,
label: GROUP_MODE_LABEL[m],
}))
const sortTypeOptions: { value: '' | 'ASC' | 'DESC'; label: string }[] = [
{ value: 'ASC', label: 'Asc' },
{ value: 'DESC', label: 'Desc' },
]
const cellCls = const cellCls =
'w-full rounded border border-transparent bg-transparent px-1 py-0.5 text-xs hover:border-gray-300 focus:border-blue-500 focus:bg-white dark:text-gray-100 dark:hover:border-gray-600 dark:focus:bg-gray-700' 'w-full rounded border border-transparent bg-transparent px-1 py-0.5 text-xs hover:border-gray-300 focus:border-blue-500 focus:bg-white dark:text-gray-100 dark:hover:border-gray-600 dark:focus:bg-gray-700'
@ -115,13 +129,13 @@ export function CriteriaGrid({
<thead className="sticky top-0 z-10 bg-gray-100 text-[10px] font-semibold uppercase tracking-wide text-gray-500 dark:bg-gray-800 dark:text-gray-300"> <thead className="sticky top-0 z-10 bg-gray-100 text-[10px] font-semibold uppercase tracking-wide text-gray-500 dark:bg-gray-800 dark:text-gray-300">
<tr> <tr>
<th className="w-6 border border-gray-200 px-1 py-1 dark:border-gray-700" /> <th className="w-6 border border-gray-200 px-1 py-1 dark:border-gray-700" />
<th className="min-w-[150px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"> <th className="min-w-[260px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.column} {labels.column}
</th> </th>
<th className="min-w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"> <th className="min-w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.alias} {labels.alias}
</th> </th>
<th className="min-w-[90px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"> <th className="min-w-[180px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.table} {labels.table}
</th> </th>
<th className="w-14 border border-gray-200 px-1 py-1 dark:border-gray-700"> <th className="w-14 border border-gray-200 px-1 py-1 dark:border-gray-700">
@ -132,7 +146,7 @@ export function CriteriaGrid({
{labels.groupBy} {labels.groupBy}
</th> </th>
)} )}
<th className="w-[86px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"> <th className="w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.sortType} {labels.sortType}
</th> </th>
<th className="w-[70px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"> <th className="w-[70px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
@ -201,18 +215,23 @@ export function CriteriaGrid({
onChange={(e) => onUpdateRow(row.id, { expression: e.target.value })} onChange={(e) => onUpdateRow(row.id, { expression: e.target.value })}
/> />
) : ( ) : (
<select <Select
className={cellCls} size="xs"
value={row.columnName} className="w-full"
onChange={(e) => onUpdateRow(row.id, { columnName: e.target.value })} isClearable
> placeholder="—"
<option value=""></option> menuPortalTarget={document.body}
{columnsOf(row.sourceId).map((c) => ( options={columnsOf(row.sourceId).map((c) => ({
<option key={c.columnName} value={c.columnName}> value: c.columnName ?? '',
{c.columnName} label: c.columnName ?? '',
</option> }))}
))} value={
</select> row.columnName ? { value: row.columnName, label: row.columnName } : null
}
onChange={(option) =>
onUpdateRow(row.id, { columnName: option?.value ?? '' })
}
/>
)} )}
</td> </td>
@ -230,19 +249,16 @@ export function CriteriaGrid({
{isExpression ? ( {isExpression ? (
<span className="px-1 text-[10px] italic text-gray-400">SQL</span> <span className="px-1 text-[10px] italic text-gray-400">SQL</span>
) : ( ) : (
<select <Select
className={cellCls} size="xs"
value={row.sourceId} className="w-full"
onChange={(e) => menuPortalTarget={document.body}
onUpdateRow(row.id, { sourceId: e.target.value, columnName: '' }) options={sourceOptions}
value={sourceOptions.filter((option) => option.value === row.sourceId)}
onChange={(option) =>
option && onUpdateRow(row.id, { sourceId: option.value, columnName: '' })
} }
> />
{sources.map((s) => (
<option key={s.id} value={s.id}>
{s.alias || s.objectName}
</option>
))}
</select>
)} )}
</td> </td>
@ -259,33 +275,36 @@ export function CriteriaGrid({
{grouped && ( {grouped && (
<td className="border border-gray-200 px-0.5 dark:border-gray-700"> <td className="border border-gray-200 px-0.5 dark:border-gray-700">
<select <Select
className={cellCls} size="xs"
value={row.groupMode} className="w-full"
disabled={isExpression} isDisabled={isExpression}
onChange={(e) => { menuPortalTarget={document.body}
const mode = e.target.value as GroupMode options={groupModeOptions}
value={groupModeOptions.filter((option) => option.value === row.groupMode)}
onChange={(option) => {
if (!option) return
const mode = option.value
onUpdateRow(row.id, { onUpdateRow(row.id, {
groupMode: mode, groupMode: mode,
...(mode === 'Where' ? { output: false } : {}), ...(mode === 'Where' ? { output: false } : {}),
}) })
}} }}
> />
{GROUP_MODES.map((m) => (
<option key={m} value={m}>
{GROUP_MODE_LABEL[m]}
</option>
))}
</select>
</td> </td>
)} )}
<td className="border border-gray-200 px-0.5 dark:border-gray-700"> <td className="border border-gray-200 px-0.5 dark:border-gray-700">
<select <Select
className={cellCls} size="xs"
value={row.sortType} className="w-full"
onChange={(e) => { isClearable
const sortType = e.target.value as '' | 'ASC' | 'DESC' placeholder="—"
menuPortalTarget={document.body}
options={sortTypeOptions}
value={sortTypeOptions.filter((option) => option.value === row.sortType)}
onChange={(option) => {
const sortType = (option?.value ?? '') as '' | 'ASC' | 'DESC'
onUpdateRow(row.id, { onUpdateRow(row.id, {
sortType, sortType,
sortOrder: sortType sortOrder: sortType
@ -293,11 +312,7 @@ export function CriteriaGrid({
: 0, : 0,
}) })
}} }}
> />
<option value=""></option>
<option value="ASC">Ascending</option>
<option value="DESC">Descending</option>
</select>
</td> </td>
<td className="border border-gray-200 px-0.5 dark:border-gray-700"> <td className="border border-gray-200 px-0.5 dark:border-gray-700">
@ -395,6 +410,11 @@ function FilterBuilder({
const [value2, setValue2] = useState('') const [value2, setValue2] = useState('')
const [mode, setMode] = useState<'literal' | 'expression'>('literal') const [mode, setMode] = useState<'literal' | 'expression'>('literal')
const modeOptions: { value: 'literal' | 'expression'; label: string }[] = [
{ value: 'literal', label: labels.builderLiteral },
{ value: 'expression', label: labels.builderExpression },
]
const needsValue = operator !== 'IS NULL' && operator !== 'IS NOT NULL' const needsValue = operator !== 'IS NULL' && operator !== 'IS NOT NULL'
const build = () => { const build = () => {
@ -418,17 +438,14 @@ function FilterBuilder({
className="absolute left-0 top-full z-50 mt-1 w-[230px] rounded-lg border border-gray-200 bg-white p-2 shadow-xl dark:border-gray-700 dark:bg-gray-800" className="absolute left-0 top-full z-50 mt-1 w-[230px] rounded-lg border border-gray-200 bg-white p-2 shadow-xl dark:border-gray-700 dark:bg-gray-800"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<select <Select
className="mb-1 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white" size="xs"
value={operator} className="mb-1 w-full"
onChange={(e) => setOperator(e.target.value)} menuPortalTarget={document.body}
> options={OPERATORS.map((op) => ({ value: op, label: op }))}
{OPERATORS.map((op) => ( value={{ value: operator, label: operator }}
<option key={op} value={op}> onChange={(option) => option && setOperator(option.value)}
{op} />
</option>
))}
</select>
{needsValue && ( {needsValue && (
<> <>
<Input <Input
@ -447,14 +464,14 @@ function FilterBuilder({
onChange={(e) => setValue2(e.target.value)} onChange={(e) => setValue2(e.target.value)}
/> />
)} )}
<select <Select
className="mb-1 w-full rounded border px-1 py-1 text-[11px] dark:border-gray-600 dark:bg-gray-700 dark:text-white" size="xs"
value={mode} className="mb-1 w-full"
onChange={(e) => setMode(e.target.value as 'literal' | 'expression')} menuPortalTarget={document.body}
> options={modeOptions}
<option value="literal">{labels.builderLiteral}</option> value={modeOptions.filter((option) => option.value === mode)}
<option value="expression">{labels.builderExpression}</option> onChange={(option) => option && setMode(option.value)}
</select> />
</> </>
)} )}
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">

View file

@ -25,6 +25,7 @@ import {
} from 'react-icons/fa' } from 'react-icons/fa'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
import { import {
BOX_FOOTER_HEIGHT, BOX_FOOTER_HEIGHT,
BOX_HEADER_HEIGHT, BOX_HEADER_HEIGHT,
@ -343,6 +344,15 @@ export function DiagramPane({
? sources.find((s) => s.id === activeJoin.sourceId) ? sources.find((s) => s.id === activeJoin.sourceId)
: undefined : undefined
const joinKindOptions = JOIN_KINDS.filter((k) => k !== 'CROSS').map((k) => ({
value: k,
label: k === 'INNER' ? 'INNER JOIN' : `${k} OUTER JOIN`,
}))
const joinOperatorOptions = JOIN_OPERATORS.map((op) => ({
value: op,
label: activeJoin ? `${activeJoin.leftColumn} ${op} ${activeJoin.rightColumn}` : op,
}))
return ( return (
<div <div
className="relative h-full overflow-auto rounded-lg border border-gray-200 bg-white [--grid-color:#edf1f6] dark:border-gray-700 dark:bg-gray-950 dark:[--grid-color:#243244]" className="relative h-full overflow-auto rounded-lg border border-gray-200 bg-white [--grid-color:#edf1f6] dark:border-gray-700 dark:bg-gray-950 dark:[--grid-color:#243244]"
@ -416,34 +426,33 @@ export function DiagramPane({
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400"> <div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
{labels.joinType} {labels.joinType}
</div> </div>
<select <Select
className="mb-1.5 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white" size="xs"
value={activeJoinSource.joinKind} className="mb-1.5 w-full"
onChange={(e) => menuPortalTarget={document.body}
onUpdateSource(activeJoinSource.id, { joinKind: e.target.value as JoinKind }) options={joinKindOptions}
value={joinKindOptions.filter(
(option) => option.value === activeJoinSource.joinKind,
)}
onChange={(option) =>
option && onUpdateSource(activeJoinSource.id, { joinKind: option.value })
} }
> />
{JOIN_KINDS.filter((k) => k !== 'CROSS').map((k) => ( <Select
<option key={k} value={k}> size="xs"
{k === 'INNER' ? 'INNER JOIN' : `${k} OUTER JOIN`} className="mb-1.5 w-full"
</option> menuPortalTarget={document.body}
))} options={joinOperatorOptions}
</select> value={joinOperatorOptions.filter(
<select (option) => option.value === activeJoin.operator,
className="mb-1.5 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white" )}
value={activeJoin.operator} onChange={(option) =>
onChange={(e) => option &&
onUpdateJoin(activeJoin.sourceId, activeJoin.conditionId, { onUpdateJoin(activeJoin.sourceId, activeJoin.conditionId, {
operator: e.target.value as JoinOperator, operator: option.value,
}) })
} }
> />
{JOIN_OPERATORS.map((op) => (
<option key={op} value={op}>
{`${activeJoin.leftColumn} ${op} ${activeJoin.rightColumn}`}
</option>
))}
</select>
<button <button
type="button" type="button"
className="w-full rounded px-1 py-1 text-left text-[11px] text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20" className="w-full rounded px-1 py-1 text-left text-[11px] text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"

View file

@ -388,7 +388,7 @@ function AnnouncementModalContent({ announcement, onClose, onLikeChange }: Annou
'::App.SocialWallPostItem.CommentPlaceholder', '::App.SocialWallPostItem.CommentPlaceholder',
)} )}
rows={1} rows={1}
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors" className="flex-1 resize-none px-2 py-1 text-sm rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
/> />
<Button <Button
size="sm" size="sm"

View file

@ -313,7 +313,7 @@ function EventModalContent({ event, onClose, onLikeChange }: EventModalProps) {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={translate('::App.Events.EventAttendance')} placeholder={translate('::App.Events.EventAttendance')}
rows={1} rows={1}
className="flex-1 resize-none px-2 py-1 text-sm rounded-xl bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors" className="flex-1 resize-none px-2 py-1 text-sm rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
/> />
<Button <Button
type="button" type="button"

View file

@ -2,6 +2,7 @@ import React, { useState } from 'react'
import { GridExtraFilterState } from './Utils' import { GridExtraFilterState } from './Utils'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
export function GridExtraFilterToolbar({ export function GridExtraFilterToolbar({
filters, filters,
@ -42,23 +43,27 @@ export function GridExtraFilterToolbar({
}) })
} }
const itemOptions: { value: string; label: string }[] = (fs.items ?? []).map(
(item: any) => ({ value: String(item.key), label: String(item.value) }),
)
return ( return (
<div key={fs.fieldName} className="flex items-center gap-2"> <div key={fs.fieldName} className="flex items-center gap-2">
<label>{fs.caption}</label> <label>{fs.caption}</label>
{fs.controlType === 'Select' ? ( {fs.controlType === 'Select' ? (
<select <Select
className="border rounded px-1.5 py-1.5 dark:bg-gray-700 dark:text-white" size="xs"
value={current?.value ?? ''} className="min-w-[10rem]"
onChange={(e) => handleSave(e.target.value)} isClearable={!fs.defaultValue}
> placeholder={translate('::App.Platform.Select')}
{!fs.defaultValue && <option value="">{translate('::App.Platform.Select')}</option>} menuPortalTarget={document.body}
{fs.items?.map((item: any) => ( options={itemOptions}
<option key={item.key} value={item.key}> value={itemOptions.filter(
{item.value} (option) => option.value === String(current?.value ?? ''),
</option> )}
))} onChange={(option) => handleSave(option?.value ?? '')}
</select> />
) : ( ) : (
<Input <Input
unstyle unstyle

View file

@ -38,6 +38,7 @@ import type { GridColumnData } from './GridColumnData'
import { flattenGridColumns } from './shared/columns' import { flattenGridColumns } from './shared/columns'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
@ -279,6 +280,11 @@ const TodoBoardContent = ({
[lookupText, options.statusExpr, translate], [lookupText, options.statusExpr, translate],
) )
const statusSelectOptions = statuses.map((status) => ({
value: status,
label: statusText(status),
}))
const clearDragState = useCallback(() => { const clearDragState = useCallback(() => {
dragActiveRef.current = false dragActiveRef.current = false
setDraggedKey(undefined) setDraggedKey(undefined)
@ -1000,23 +1006,21 @@ const TodoBoardContent = ({
{options.statusExpr && ( {options.statusExpr && (
<div> <div>
<label className={labelClass}>{translate('::App.Listform.ListformField.Status')}</label> <label className={labelClass}>{translate('::App.Listform.ListformField.Status')}</label>
<select <Select
className={inputClass} size="sm"
disabled={!canUpdate} isDisabled={!canUpdate}
value={String(draft[options.statusExpr] ?? '')} menuPortalTarget={document.body}
onChange={(event) => options={statusSelectOptions}
value={statusSelectOptions.filter(
(option) => option.value === String(draft[options.statusExpr!] ?? ''),
)}
onChange={(option) =>
setDraft((current) => ({ setDraft((current) => ({
...current, ...current,
[options.statusExpr!]: event.target.value, [options.statusExpr!]: option?.value ?? '',
})) }))
} }
> />
{statuses.map((status) => (
<option key={status} value={status}>
{statusText(status)}
</option>
))}
</select>
</div> </div>
)} )}
{options.dueDateExpr && ( {options.dueDateExpr && (

View file

@ -393,7 +393,7 @@ const MenuItemComponentBase: React.FC<MenuItemComponentProps> = ({
{isModalOpen && ( {isModalOpen && (
<Dialog <Dialog
isOpen={isModalOpen} isOpen={isModalOpen}
width={640} width={900}
onClose={() => setIsModalOpen(false)} onClose={() => setIsModalOpen(false)}
onRequestClose={() => setIsModalOpen(false)} onRequestClose={() => setIsModalOpen(false)}
> >
@ -497,6 +497,8 @@ const MenuItemComponentBase: React.FC<MenuItemComponentProps> = ({
{({ field, form }: FieldProps<SelectBoxOption>) => ( {({ field, form }: FieldProps<SelectBoxOption>) => (
<Select <Select
isClearable isClearable
maxMenuHeight={220}
menuPortalTarget={document.body}
field={field} field={field}
form={form} form={form}
options={permissions} options={permissions}

View file

@ -9,6 +9,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { useConfig } from '@/components/ui/ConfigProvider' import { useConfig } from '@/components/ui/ConfigProvider'
import { useForm } from '@/components/ui/Form/context' import { useForm } from '@/components/ui/Form/context'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
import Select from '@/components/ui/Select'
const menuService = new MenuService() const menuService = new MenuService()
@ -253,6 +254,11 @@ export function MenuAddDialog({
.sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => a.label.localeCompare(b.label))
}, [isEditMode, rawItems, editMenu]) }, [isEditMode, rawItems, editMenu])
const parentSelectOptions = parentOptions.map((option) => ({
value: option.code,
label: option.label,
}))
const handleSave = async () => { const handleSave = async () => {
if (!form.code.trim() || !form.menuTextEn.trim()) return if (!form.code.trim() || !form.menuTextEn.trim()) return
if (shortNameRequired && !form.shortName.trim()) return if (shortNameRequired && !form.shortName.trim()) return
@ -420,18 +426,19 @@ export function MenuAddDialog({
<div className="flex flex-col"> <div className="flex flex-col">
<label className={labelCls}>{translate('::App.Platform.MenuParent')}</label> <label className={labelCls}>{translate('::App.Platform.MenuParent')}</label>
{isEditMode ? ( {isEditMode ? (
<select <Select
value={form.parentCode} size="sm"
onChange={(e) => setForm((p) => ({ ...p, parentCode: e.target.value }))} isClearable
className={fieldCls} placeholder={translate('::App.WizardStep1.RootMenu')}
> menuPortalTarget={document.body}
<option value="">{translate('::App.WizardStep1.RootMenu')}</option> options={parentSelectOptions}
{parentOptions.map((option) => ( value={parentSelectOptions.filter(
<option key={option.code} value={option.code}> (option) => option.value === form.parentCode,
{option.label} )}
</option> onChange={(option) =>
))} setForm((p) => ({ ...p, parentCode: option?.value ?? '' }))
</select> }
/>
) : ( ) : (
<Input unstyle disabled value={form.parentCode} className={disabledCls} /> <Input unstyle disabled value={form.parentCode} className={disabledCls} />
)} )}