<select/> -> <Select /> olarak değiştirildi.
This commit is contained in:
parent
7186cb0ab0
commit
4c09de9ad0
27 changed files with 1114 additions and 913 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
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 Input from '@/components/ui/Input'
|
||||
|
||||
|
|
@ -352,6 +352,11 @@ const StyleModal = ({
|
|||
const [search, setSearch] = useState('')
|
||||
const [customClass, setCustomClass] = useState('')
|
||||
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 classes = useMemo(() => {
|
||||
const source =
|
||||
|
|
@ -409,18 +414,15 @@ const StyleModal = ({
|
|||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<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"
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event.target.value as typeof category)}
|
||||
>
|
||||
<option value="__all__">{translate('::App.StyleModal.AllCategories')}</option>
|
||||
{Object.keys(STYLE_GROUPS).map((name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
menuPortalTarget={document.body}
|
||||
options={categoryOptions}
|
||||
value={categoryOptions.filter((option) => option.value === category)}
|
||||
onChange={(option) =>
|
||||
setCategory((option?.value ?? '__all__') as typeof category)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mode === 'class' && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Dialog } from '@/components/ui'
|
||||
import { Button, Dialog, Select } from '@/components/ui'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Editor, { type Monaco } from '@monaco-editor/react'
|
||||
import type * as monacoApi from 'monaco-editor'
|
||||
|
|
@ -276,18 +276,20 @@ function ScriptBuilderDialog({
|
|||
)
|
||||
}
|
||||
return (
|
||||
<select
|
||||
className={controlClass}
|
||||
value={options.includes(selectedValue) ? selectedValue : ''}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
>
|
||||
<option value="">{placeholder}</option>
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
isClearable
|
||||
placeholder={placeholder}
|
||||
menuPortalTarget={document.body}
|
||||
options={options.map((option) => ({ value: option, label: option }))}
|
||||
value={
|
||||
options.includes(selectedValue)
|
||||
? { value: selectedValue, label: selectedValue }
|
||||
: null
|
||||
}
|
||||
onChange={(option) => onChange(option?.value ?? '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -318,24 +320,21 @@ function ScriptBuilderDialog({
|
|||
</button>
|
||||
</span>
|
||||
))}
|
||||
<select
|
||||
className={`${controlClass} !h-8 !w-44`}
|
||||
value=""
|
||||
onChange={(event) => {
|
||||
const next = event.target.value
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-44"
|
||||
placeholder={translate('::App.ScriptBuilder.AddOption')}
|
||||
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
|
||||
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>
|
||||
)
|
||||
|
|
@ -357,17 +356,21 @@ function ScriptBuilderDialog({
|
|||
param.placeholder || translate('::App.ScriptBuilder.Choose'),
|
||||
)}
|
||||
{param.type === 'select' && (
|
||||
<select
|
||||
className={controlClass}
|
||||
value={currentValue}
|
||||
onChange={(event) => updateParam(rule.id, param.key, event.target.value)}
|
||||
>
|
||||
{param.choices?.map((choice) => (
|
||||
<option key={choice.value} value={choice.value}>
|
||||
{choice.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={(param.choices ?? []).map((choice) => ({
|
||||
value: choice.value,
|
||||
label: choice.label,
|
||||
}))}
|
||||
value={(param.choices ?? [])
|
||||
.filter((choice) => choice.value === currentValue)
|
||||
.map((choice) => ({ value: choice.value, label: choice.label }))}
|
||||
onChange={(option) =>
|
||||
option && updateParam(rule.id, param.key, option.value)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(param.type === 'text' || param.type === 'number') && (
|
||||
<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 operator = dialect.operators.find((item) => item.value === condition.operator)
|
||||
const kind =
|
||||
|
|
@ -401,16 +424,16 @@ function ScriptBuilderDialog({
|
|||
<span className="mb-1 block text-xs text-gray-500">
|
||||
{translate('::App.ScriptBuilder.Conjunction')}
|
||||
</span>
|
||||
<select
|
||||
className={controlClass}
|
||||
value={rule.join ?? 'and'}
|
||||
onChange={(event) =>
|
||||
updateRule(rule.id, { join: event.target.value as ScriptRule['join'] })
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
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>
|
||||
) : (
|
||||
<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">
|
||||
{translate('::App.ScriptBuilder.Comparison')}
|
||||
</span>
|
||||
<select
|
||||
className={controlClass}
|
||||
value={condition.operator}
|
||||
onChange={(event) => updateCondition(rule, index, { operator: event.target.value })}
|
||||
>
|
||||
{dialect.operators.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label.startsWith('App.') ? translate('::' + item.label) : item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={operatorOptions}
|
||||
value={operatorOptions.filter((option) => option.value === condition.operator)}
|
||||
onChange={(option) =>
|
||||
option && updateCondition(rule, index, { operator: option.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{operator?.needsSource && dialect.conditionKinds.length > 1 && (
|
||||
|
|
@ -445,19 +467,19 @@ function ScriptBuilderDialog({
|
|||
<span className="mb-1 block text-xs text-gray-500">
|
||||
{translate('::App.Listform.ListformField.SourceId')}
|
||||
</span>
|
||||
<select
|
||||
className={controlClass}
|
||||
value={condition.kind ?? dialect.conditionKinds[0]?.value ?? ''}
|
||||
onChange={(event) =>
|
||||
updateCondition(rule, index, { kind: event.target.value, source: '' })
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
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>
|
||||
)}
|
||||
|
||||
|
|
@ -602,17 +624,18 @@ function ScriptBuilderDialog({
|
|||
{dialect.triggers && (
|
||||
<label className="w-full md:w-56">
|
||||
<span className="mb-1 block text-xs text-gray-500">Ne zaman</span>
|
||||
<select
|
||||
className={controlClass}
|
||||
value={trigger ?? dialect.triggers[0].value}
|
||||
onChange={(event) => updateRule(rule.id, { trigger: event.target.value })}
|
||||
>
|
||||
{dialect.triggers.map((item) => (
|
||||
<option key={item.value} value={item.value} title={item.help}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={triggerOptions}
|
||||
value={triggerOptions.filter(
|
||||
(option) => option.value === (trigger ?? dialect.triggers?.[0]?.value),
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option && updateRule(rule.id, { trigger: option.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,9 @@ function SelectBase<
|
|||
placeholder: (provided) => ({ ...provided, margin: 0 }),
|
||||
singleValue: (provided) => ({ ...provided, margin: 0 }),
|
||||
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,
|
||||
}}
|
||||
theme={(theme) => ({
|
||||
|
|
|
|||
|
|
@ -999,18 +999,19 @@ const SqlDataSourceView = ({
|
|||
{rows.length > 1 && (
|
||||
<label className="flex items-center gap-1">
|
||||
{translate('::App.Platform.Row')}
|
||||
<select
|
||||
className="rounded border border-sky-300 bg-white px-1 py-0.5 text-[10px] dark:border-sky-800 dark:bg-slate-900"
|
||||
value={rowIndex}
|
||||
onChange={(event) => goToRow(Number(event.target.value) || 0)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{rows.map((_, index) => (
|
||||
<option key={index} value={index}>
|
||||
{index + 1}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<UiKit.Select
|
||||
size="xs"
|
||||
className="min-w-[4.5rem]"
|
||||
maxMenuHeight={200}
|
||||
menuPosition="fixed"
|
||||
menuPortalTarget={window.document.body}
|
||||
options={rows.map((_, index) => ({
|
||||
value: index,
|
||||
label: String(index + 1),
|
||||
}))}
|
||||
value={{ value: rowIndex, label: String(rowIndex + 1) }}
|
||||
onChange={(option) => goToRow(Number(option?.value) || 0)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
|
|
@ -50,6 +50,10 @@ function TablePickerModal({
|
|||
null,
|
||||
)
|
||||
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
|
||||
const pickerColumnOptions = pickerColumns.map((c) => ({
|
||||
value: c.columnName ?? '',
|
||||
label: c.columnName ?? '',
|
||||
}))
|
||||
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
|
||||
const [keyCol, setKeyCol] = useState('')
|
||||
const [nameCol, setNameCol] = useState('')
|
||||
|
|
@ -163,35 +167,31 @@ function TablePickerModal({
|
|||
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||
{translate('::App.ListFormFieldEdit.KeyColumn')}
|
||||
</label>
|
||||
<select
|
||||
value={keyCol}
|
||||
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"
|
||||
onChange={(e) => setKeyCol(e.target.value)}
|
||||
>
|
||||
<option value="">{translate('::App.Platform.Select')}</option>
|
||||
{pickerColumns.map((c) => (
|
||||
<option key={c.columnName} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={pickerColumnOptions}
|
||||
value={pickerColumnOptions.filter((option) => option.value === keyCol)}
|
||||
onChange={(option) => setKeyCol(option?.value ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||
{translate('::App.ListFormFieldEdit.NameColumn')}
|
||||
</label>
|
||||
<select
|
||||
value={nameCol}
|
||||
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"
|
||||
onChange={(e) => setNameCol(e.target.value)}
|
||||
>
|
||||
<option value="">{translate('::App.Platform.Select')}</option>
|
||||
{pickerColumns.map((c) => (
|
||||
<option key={c.columnName} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={pickerColumnOptions}
|
||||
value={pickerColumnOptions.filter((option) => option.value === nameCol)}
|
||||
onChange={(option) => setNameCol(option?.value ?? '')}
|
||||
/>
|
||||
</div>
|
||||
{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">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Dialog } from '@/components/ui'
|
||||
import { Button, Dialog, Select } from '@/components/ui'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import type { IconType } from 'react-icons'
|
||||
|
|
@ -61,6 +61,7 @@ type EditorOptionsBuilderDialogProps = {
|
|||
}
|
||||
|
||||
const leafTypes: LeafValueType[] = ['string', 'number', 'boolean', 'json']
|
||||
const leafTypeOptions = leafTypes.map((item) => ({ value: item, label: item }))
|
||||
|
||||
const PRESETS_KEY = '__presets'
|
||||
const UNMANAGED_KEY = '__unmanaged'
|
||||
|
|
@ -545,25 +546,22 @@ function EditorOptionsBuilderDialog({
|
|||
>
|
||||
{path}
|
||||
</span>
|
||||
<select
|
||||
className={`${controlClass} col-span-2`}
|
||||
value={type}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="sm"
|
||||
className="col-span-2"
|
||||
menuPortalTarget={document.body}
|
||||
options={leafTypeOptions}
|
||||
value={leafTypeOptions.filter((option) => option.value === type)}
|
||||
onChange={(option) =>
|
||||
setPath(
|
||||
path,
|
||||
textToLeaf(
|
||||
leafToText(current),
|
||||
event.target.value as LeafValueType,
|
||||
(option?.value ?? 'string') as LeafValueType,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{leafTypes.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
<Input
|
||||
unstyle
|
||||
className={`${controlClass} col-span-5`}
|
||||
|
|
@ -596,17 +594,16 @@ function EditorOptionsBuilderDialog({
|
|||
placeholder="path.to.option"
|
||||
onChange={(event) => setNewPath(event.target.value)}
|
||||
/>
|
||||
<select
|
||||
className={`${controlClass} col-span-2`}
|
||||
value={newType}
|
||||
onChange={(event) => setNewType(event.target.value as LeafValueType)}
|
||||
>
|
||||
{leafTypes.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="col-span-2"
|
||||
menuPortalTarget={document.body}
|
||||
options={leafTypeOptions}
|
||||
value={leafTypeOptions.filter((option) => option.value === newType)}
|
||||
onChange={(option) =>
|
||||
setNewType((option?.value ?? 'string') as LeafValueType)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
unstyle
|
||||
className={`${controlClass} col-span-5`}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { coerceNumber, coerceSize, leafToText } from './jsonUtils'
|
|||
import type { OptionSpec } from './optionSpecs'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
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'
|
||||
|
|
@ -64,19 +65,24 @@ const BooleanControl = ({
|
|||
? 'false'
|
||||
: ''
|
||||
|
||||
const booleanOptions = [
|
||||
{ value: 'true', label: 'true' },
|
||||
{ value: 'false', label: 'false' },
|
||||
]
|
||||
|
||||
return (
|
||||
<select
|
||||
className={controlClass}
|
||||
value={current}
|
||||
onChange={(event) => {
|
||||
if (!event.target.value) return onChange(undefined)
|
||||
onChange(event.target.value === 'true')
|
||||
<Select
|
||||
size="sm"
|
||||
isClearable
|
||||
placeholder={translate('::App.ListFormEditorOptions.Undefined')}
|
||||
menuPortalTarget={document.body}
|
||||
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 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 (
|
||||
<select
|
||||
className={controlClass}
|
||||
value={current}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value
|
||||
<Select
|
||||
size="sm"
|
||||
isClearable
|
||||
placeholder={translate('::App.ListFormEditorOptions.Undefined')}
|
||||
menuPortalTarget={document.body}
|
||||
options={choiceOptions}
|
||||
value={choiceOptions.filter((option) => option.value === current)}
|
||||
onChange={(option) => {
|
||||
const raw = option?.value
|
||||
if (!raw) return onChange(undefined)
|
||||
const choice = spec.choices?.find((item) => String(item.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>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const triggerLabels: { value: RuleTrigger; label: string; help: string }[
|
|||
label: 'App.ScriptBuilderOpen.OpenLabel',
|
||||
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. */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Dialog } from '@/components/ui'
|
||||
import { Button, Dialog, Select } from '@/components/ui'
|
||||
import type { SelectBoxOption } from '@/types/shared'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import {
|
||||
|
|
@ -230,6 +230,10 @@ function SortableItem({
|
|||
null,
|
||||
)
|
||||
const [pickerColumns, setPickerColumns] = useState<DatabaseColumnDto[]>([])
|
||||
const pickerColumnOptions = pickerColumns.map((c) => ({
|
||||
value: c.columnName ?? '',
|
||||
label: c.columnName ?? '',
|
||||
}))
|
||||
const [isLoadingPickerColumns, setIsLoadingPickerColumns] = useState(false)
|
||||
const [pickerKeyCol, setPickerKeyCol] = useState('')
|
||||
const [pickerNameCol, setPickerNameCol] = useState('')
|
||||
|
|
@ -321,17 +325,17 @@ function SortableItem({
|
|||
<span className="text-[10px] text-gray-400 font-medium">
|
||||
{translate('::App.WizardStep3.EditorType')}
|
||||
</span>
|
||||
<select
|
||||
value={item.editorType}
|
||||
onChange={(e) => onEditorTypeChange(e.target.value)}
|
||||
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"
|
||||
>
|
||||
{columnEditorTypeListOptions.map((et) => (
|
||||
<option key={et.value} value={et.value}>
|
||||
{et.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full text-xs"
|
||||
maxMenuHeight={200}
|
||||
menuPortalTarget={document.body}
|
||||
options={columnEditorTypeListOptions}
|
||||
value={columnEditorTypeListOptions.filter(
|
||||
(option) => option.value === item.editorType,
|
||||
)}
|
||||
onChange={(option) => option && onEditorTypeChange(option.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{translate('::App.WizardStep3.LookupDataSourceType')}
|
||||
</span>
|
||||
<select
|
||||
value={item.lookupDataSourceType}
|
||||
onChange={(e) =>
|
||||
onLookupDataSourceTypeChange(e.target.value as unknown as UiLookupDataSourceTypeEnum)
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full text-xs"
|
||||
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 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">
|
||||
{translate('::App.ListFormFieldEdit.KeyColumn')}
|
||||
</label>
|
||||
<select
|
||||
value={pickerKeyCol}
|
||||
onChange={(e) => setPickerKeyCol(e.target.value)}
|
||||
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"
|
||||
>
|
||||
<option value="">{translate('::App.Platform.Select')}</option>
|
||||
{pickerColumns.map((c) => (
|
||||
<option key={c.columnName} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full text-xs"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={pickerColumnOptions}
|
||||
value={pickerColumnOptions.filter(
|
||||
(option) => option.value === pickerKeyCol,
|
||||
)}
|
||||
onChange={(option) => setPickerKeyCol(option?.value ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10px] font-medium text-gray-500 dark:text-gray-400">
|
||||
{translate('::App.ListFormFieldEdit.NameColumn')}
|
||||
</label>
|
||||
<select
|
||||
value={pickerNameCol}
|
||||
onChange={(e) => setPickerNameCol(e.target.value)}
|
||||
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"
|
||||
>
|
||||
<option value="">{translate('::App.Platform.Select')}</option>
|
||||
{pickerColumns.map((c) => (
|
||||
<option key={c.columnName} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full text-xs"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={pickerColumnOptions}
|
||||
value={pickerColumnOptions.filter(
|
||||
(option) => option.value === pickerNameCol,
|
||||
)}
|
||||
onChange={(option) => setPickerNameCol(option?.value ?? '')}
|
||||
/>
|
||||
</div>
|
||||
{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">
|
||||
|
|
@ -651,17 +657,17 @@ function SortableItem({
|
|||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-gray-400">{translate('::App.WizardStep3.Span')}</span>
|
||||
<select
|
||||
value={item.colSpan}
|
||||
onChange={(e) => onColSpanChange(Number(e.target.value))}
|
||||
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"
|
||||
>
|
||||
{Array.from({ length: groupColCount }, (_, i) => i + 1).map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-20 text-xs"
|
||||
menuPortalTarget={document.body}
|
||||
options={Array.from({ length: groupColCount }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: String(i + 1),
|
||||
}))}
|
||||
value={{ value: item.colSpan, label: String(item.colSpan) }}
|
||||
onChange={(option) => option && onColSpanChange(Number(option.value))}
|
||||
/>
|
||||
</div>
|
||||
<label
|
||||
className="flex items-center gap-1 cursor-pointer ml-auto"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import classNames from 'classnames'
|
||||
import Select from '@/components/ui/Select'
|
||||
import { FaChevronRight, FaChevronDown } from 'react-icons/fa'
|
||||
import Container from '@/components/shared/Container'
|
||||
import { Button, Checkbox, Dialog, Input, Menu, toast } from '@/components/ui'
|
||||
|
|
@ -480,6 +481,10 @@ function RolesPermission({
|
|||
const [copyDialogOpen, setCopyDialogOpen] = useState(false)
|
||||
const [copyDialogRole, setCopyDialogRole] = useState('')
|
||||
|
||||
const copyRoleOptions = roleList
|
||||
.filter((role) => role !== name)
|
||||
.map((role) => ({ value: role, label: role }))
|
||||
|
||||
// Fetch all roles for select (except current)
|
||||
useEffect(() => {
|
||||
async function fetchRoles() {
|
||||
|
|
@ -571,20 +576,16 @@ function RolesPermission({
|
|||
>
|
||||
<h5 className="mb-2">{translate('::AbpIdentity.Roles.CopyPermissions')}</h5>
|
||||
<div className="mb-4">
|
||||
<select
|
||||
className="border rounded px-2 py-1 w-full"
|
||||
value={copyDialogRole}
|
||||
onChange={(e) => setCopyDialogRole(e.target.value)}
|
||||
>
|
||||
<option value="">{translate('::App.Platform.Select')}</option>
|
||||
{roleList
|
||||
.filter((role) => role !== name)
|
||||
.map((role) => (
|
||||
<option key={role} value={role}>
|
||||
{role}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={copyRoleOptions}
|
||||
value={copyRoleOptions.filter((option) => option.value === copyDialogRole)}
|
||||
onChange={(option) => setCopyDialogRole(option?.value ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="plain" onClick={() => setCopyDialogOpen(false)}>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/
|
|||
import { Button } from '@/components/ui'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
interface ChatPanelProps {
|
||||
user: { id: string; name: string; role: string }
|
||||
|
|
@ -50,6 +51,11 @@ const ChatPanel = ({
|
|||
|
||||
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 (
|
||||
<div className="h-full bg-white flex flex-col text-gray-900">
|
||||
{/* Header */}
|
||||
|
|
@ -125,21 +131,18 @@ const ChatPanel = ({
|
|||
</div>
|
||||
|
||||
{messageMode === 'private' && (
|
||||
<select
|
||||
value={selectedRecipient?.id || ''}
|
||||
onChange={(e) => {
|
||||
const recipient = availableRecipients.find((p) => p.id === e.target.value)
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
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)
|
||||
}}
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import PageTitle from '@/components/shared/PageTitle'
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { VideoroomDto } from '@/proxy/videoroom/models'
|
||||
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'
|
||||
|
||||
export interface RoomProps {
|
||||
|
|
@ -45,6 +45,21 @@ const RoomList = () => {
|
|||
const { user } = useStoreState((state) => state.auth)
|
||||
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 = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
|
|
@ -733,80 +748,70 @@ const RoomList = () => {
|
|||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{translate('::App.VideoRoom.DefaultMicrophoneState')}
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultMicrophoneState}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
menuPortalTarget={document.body}
|
||||
options={microphoneStateOptions}
|
||||
value={microphoneStateOptions.filter(
|
||||
(option) =>
|
||||
option.value === videoroom.settingsDto?.defaultMicrophoneState,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setVideoroom({
|
||||
...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 className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{translate('::App.VideoRoom.DefaultCameraState')}
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultCameraState}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
menuPortalTarget={document.body}
|
||||
options={cameraStateOptions}
|
||||
value={cameraStateOptions.filter(
|
||||
(option) => option.value === videoroom.settingsDto?.defaultCameraState,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setVideoroom({
|
||||
...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 className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{translate('::App.VideoRoom.DefaultLayout')}
|
||||
</label>
|
||||
<select
|
||||
value={videoroom.settingsDto?.defaultLayout}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
menuPortalTarget={document.body}
|
||||
options={layoutOptions}
|
||||
value={layoutOptions.filter(
|
||||
(option) => option.value === videoroom.settingsDto?.defaultLayout,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setVideoroom({
|
||||
...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>
|
||||
|
||||
<label className="flex items-center space-x-3">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
|||
import { usePermission } from '@/utils/hooks/usePermission'
|
||||
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
const ComponentManager: React.FC = () => {
|
||||
const {
|
||||
|
|
@ -45,6 +46,12 @@ const ComponentManager: React.FC = () => {
|
|||
const activeComponents = components?.filter((c) => c.isActive).length || 0
|
||||
const inactiveComponents = totalComponents - activeComponents
|
||||
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 canCreate = checkPermission(COMPONENT_PERMISSION.CREATE)
|
||||
const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE)
|
||||
|
|
@ -143,19 +150,13 @@ const ComponentManager: React.FC = () => {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<FaFilter className="w-5 h-5 text-slate-500 dark:text-gray-400" />
|
||||
<select
|
||||
value={filterActive}
|
||||
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"
|
||||
onChange={(e) => setFilterActive(e.target.value as 'all' | 'active' | 'inactive')}
|
||||
>
|
||||
<option value="all">{translate('::App.ComponentFilter.All')}</option>
|
||||
<option value="active">
|
||||
{translate('::App.EntityFilter.Active')}
|
||||
</option>
|
||||
<option value="inactive">
|
||||
{translate('::App.EntityFilter.Inactive')}
|
||||
</option>
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="min-w-[9rem]"
|
||||
options={filterActiveOptions}
|
||||
value={filterActiveOptions.filter((o) => o.value === filterActive)}
|
||||
onChange={(option) => setFilterActive(option?.value ?? 'all')}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
|
|
|
|||
|
|
@ -18,9 +18,20 @@ import PageTitle from '@/components/shared/PageTitle'
|
|||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
const DynamicServiceManager: React.FC = () => {
|
||||
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 [isLoading, setIsLoading] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
|
@ -140,22 +151,12 @@ const DynamicServiceManager: React.FC = () => {
|
|||
</div>
|
||||
<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" />
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) =>
|
||||
setFilterStatus(e.target.value as 'all' | 'Success' | 'Failed' | 'Pending')
|
||||
}
|
||||
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>
|
||||
<Select
|
||||
className="w-full lg:w-48"
|
||||
options={filterStatusOptions}
|
||||
value={filterStatusOptions.filter((o) => o.value === filterStatus)}
|
||||
onChange={(option) => setFilterStatus(option?.value ?? 'all')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full sm:w-auto">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
FaTable,
|
||||
FaPlus,
|
||||
FaEye,
|
||||
FaProjectDiagram,
|
||||
FaCog,
|
||||
FaCode,
|
||||
FaDatabase,
|
||||
|
|
@ -748,7 +749,7 @@ const SqlObjectExplorer = ({
|
|||
closeCtx()
|
||||
}}
|
||||
>
|
||||
<FaEye className="text-purple-500" /> Design View
|
||||
<FaProjectDiagram className="text-purple-500" /> Design View
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { getDataSources } from '@/services/data-source.service'
|
||||
import type { DataSourceDto } from '@/proxy/data-source'
|
||||
|
|
@ -272,6 +272,11 @@ const SqlQueryManager = () => {
|
|||
`[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]`
|
||||
const getSafePgFullName = (schemaName: string, objectName: string) =>
|
||||
`"${escapePgIdentifier(schemaName)}"."${escapePgIdentifier(objectName)}"`
|
||||
const dataSourceOptions = state.dataSources.map((ds) => ({
|
||||
value: ds.code ?? '',
|
||||
label: ds.code ?? '',
|
||||
}))
|
||||
|
||||
const selectedDataSourceType = state.dataSources.find(
|
||||
(item) => item.code === state.selectedDataSource,
|
||||
)?.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-wrap items-center gap-2 sm:gap-3">
|
||||
<FaDatabase className="text-lg text-blue-500" />
|
||||
<select
|
||||
className="border border-gray-300 rounded px-2 py-1 max-w-full dark:bg-gray-700 dark:border-gray-600"
|
||||
disabled={state.dataSources.length === 0}
|
||||
value={state.selectedDataSource || ''}
|
||||
onChange={(e) => {
|
||||
const ds = state.dataSources.find((d) => d.code === e.target.value)
|
||||
<Select
|
||||
size="sm"
|
||||
className="min-w-[12rem] max-w-full"
|
||||
isDisabled={state.dataSources.length === 0}
|
||||
menuPortalTarget={document.body}
|
||||
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)
|
||||
}}
|
||||
>
|
||||
{state.dataSources.map((ds) => (
|
||||
<option key={ds.code} value={ds.code}>
|
||||
{ds.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
<DbMigrateButton />
|
||||
{/* Seed dosyalari (configs/seeds) File Manager uzerinden yonetilir. */}
|
||||
<Link to={ROUTES_ENUM.protected.admin.files} target="_blank">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'
|
||||
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 {
|
||||
FaPlus,
|
||||
FaTrash,
|
||||
|
|
@ -1336,6 +1336,28 @@ const SqlTableDesignerDialog = ({
|
|||
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
||||
const [targetTableColumns, setTargetTableColumns] = 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 [indexes, setIndexes] = useState<TableIndex[]>([])
|
||||
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
|
||||
|
|
@ -2374,11 +2396,9 @@ const SqlTableDesignerDialog = ({
|
|||
>
|
||||
<div className="col-span-4">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="text"
|
||||
className={`w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
|
||||
isDuplicate ? 'border-red-400' : ''
|
||||
}`}
|
||||
className={isDuplicate ? 'w-full border-red-400' : 'w-full'}
|
||||
placeholder={translate('::App.SqlQueryManager.ColumnNamePlaceholder')}
|
||||
value={col.columnName}
|
||||
onChange={(e) => updateColumn(col.id, 'columnName', e.target.value)}
|
||||
|
|
@ -2391,28 +2411,27 @@ const SqlTableDesignerDialog = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<select
|
||||
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
value={col.dataType}
|
||||
onChange={(e) => {
|
||||
const dt = e.target.value as SqlDataType
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
maxMenuHeight={200}
|
||||
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)
|
||||
if (dt !== 'nvarchar') updateColumn(col.id, 'maxLength', '')
|
||||
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 className="col-span-1">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
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="-"
|
||||
value={col.maxLength}
|
||||
disabled={col.dataType !== 'nvarchar'}
|
||||
|
|
@ -2427,9 +2446,9 @@ const SqlTableDesignerDialog = ({
|
|||
</div>
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
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')}
|
||||
value={col.defaultValue}
|
||||
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>
|
||||
</label>
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="text"
|
||||
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}
|
||||
onChange={(e) => onEntityNameChange(e.target.value)}
|
||||
placeholder={translate('::App.SqlQueryManager.EntityNamePlaceholder')}
|
||||
|
|
@ -2574,10 +2593,10 @@ const SqlTableDesignerDialog = ({
|
|||
{translate('::App.Listform.ListformField.TableName')}
|
||||
</label>
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="text"
|
||||
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}
|
||||
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">
|
||||
{translate('::App.SqlQueryManager.FkColumnInThisTable')}
|
||||
</label>
|
||||
<select
|
||||
value={fkForm.fkColumnName}
|
||||
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
||||
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>
|
||||
{columns
|
||||
.filter((c) => c.columnName.trim())
|
||||
.map((c) => (
|
||||
<option key={c.id} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
isClearable
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={fkColumnOptions}
|
||||
value={fkColumnOptions.filter(
|
||||
(option) => option.value === fkForm.fkColumnName,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setFkForm((f) => ({ ...f, fkColumnName: option?.value ?? '' }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
{translate('::App.SqlQueryManager.TargetTable')}
|
||||
</label>
|
||||
<select
|
||||
value={fkForm.referencedTable}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
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: '' }))
|
||||
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>
|
||||
|
||||
|
|
@ -2805,29 +2824,21 @@ const SqlTableDesignerDialog = ({
|
|||
? ` — ${translate('::App.Platform.LoadingWithThreeDot')}`
|
||||
: ''}
|
||||
</label>
|
||||
<select
|
||||
value={fkForm.referencedColumn}
|
||||
onChange={(e) => setFkForm((f) => ({ ...f, referencedColumn: e.target.value }))}
|
||||
disabled={targetColsLoading || targetTableColumns.length === 0}
|
||||
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"
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.SqlQueryManager.SelectTargetTableFirst')}
|
||||
</option>
|
||||
{targetTableColumns.map((col) => (
|
||||
<option
|
||||
key={col}
|
||||
value={col}
|
||||
disabled={
|
||||
!targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
|
||||
}
|
||||
>
|
||||
{targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
|
||||
? `${col} (PK/UNIQUE)`
|
||||
: `${col} (Not Key)`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
isClearable
|
||||
isDisabled={targetColsLoading || targetTableColumns.length === 0}
|
||||
placeholder={translate('::App.SqlQueryManager.SelectTargetTableFirst')}
|
||||
menuPortalTarget={document.body}
|
||||
options={referencedColumnOptions}
|
||||
value={referencedColumnOptions.filter(
|
||||
(option) => option.value === fkForm.referencedColumn,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setFkForm((f) => ({ ...f, referencedColumn: option?.value ?? '' }))
|
||||
}
|
||||
/>
|
||||
{fkForm.referencedTable &&
|
||||
!targetColsLoading &&
|
||||
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">
|
||||
{translate('::App.SqlQueryManager.CascadeUpdate')}
|
||||
</label>
|
||||
<select
|
||||
value={fkForm.cascadeUpdate}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={cascadeOptions}
|
||||
value={cascadeOptions.filter(
|
||||
(option) => option.value === fkForm.cascadeUpdate,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option &&
|
||||
setFkForm((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>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
{translate('::App.SqlQueryManager.CascadeDelete')}
|
||||
</label>
|
||||
<select
|
||||
value={fkForm.cascadeDelete}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={cascadeOptions}
|
||||
value={cascadeOptions.filter(
|
||||
(option) => option.value === fkForm.cascadeDelete,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option &&
|
||||
setFkForm((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>
|
||||
|
||||
|
|
@ -2887,11 +2898,11 @@ const SqlTableDesignerDialog = ({
|
|||
<div className="flex items-center gap-6">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="checkbox"
|
||||
checked={fkForm.isRequired}
|
||||
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">
|
||||
{translate('::App.Listform.ListformField.Required')}
|
||||
|
|
@ -3135,14 +3146,14 @@ const SqlTableDesignerDialog = ({
|
|||
{translate('::App.SqlQueryManager.IndexConstraintName')}
|
||||
</label>
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="text"
|
||||
value={indexForm.indexName}
|
||||
onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))}
|
||||
placeholder={
|
||||
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>
|
||||
|
||||
|
|
@ -3150,13 +3161,13 @@ const SqlTableDesignerDialog = ({
|
|||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="checkbox"
|
||||
checked={indexForm.isClustered}
|
||||
onChange={(e) =>
|
||||
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>
|
||||
</label>
|
||||
|
|
@ -3191,7 +3202,7 @@ const SqlTableDesignerDialog = ({
|
|||
>
|
||||
<div className="col-span-1">
|
||||
<Input
|
||||
unstyle
|
||||
size="sm"
|
||||
type="checkbox"
|
||||
checked={!!existing}
|
||||
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 className="col-span-7 text-sm font-mono text-gray-800 dark:text-gray-200">
|
||||
|
|
@ -3228,23 +3239,26 @@ const SqlTableDesignerDialog = ({
|
|||
</div>
|
||||
<div className="col-span-4">
|
||||
{existing && (
|
||||
<select
|
||||
value={existing.order}
|
||||
onChange={(e) =>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={indexOrderOptions}
|
||||
value={indexOrderOptions.filter(
|
||||
(option) => option.value === existing.order,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option &&
|
||||
setIndexForm((f) => ({
|
||||
...f,
|
||||
columns: f.columns.map((ic) =>
|
||||
ic.columnName === col.columnName
|
||||
? { ...ic, order: e.target.value as 'ASC' | 'DESC' }
|
||||
? { ...ic, order: option.value }
|
||||
: 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>
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ const SqlViewDesignerDialog = ({
|
|||
</span>
|
||||
<Input
|
||||
unstyle
|
||||
className={`${inputCls} w-[190px]`}
|
||||
className={`${inputCls} w-[320px]`}
|
||||
placeholder="Vw_OrderSummary"
|
||||
value={settings.viewName}
|
||||
disabled={isEditMode}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ import { getList } from '@/services/form.service'
|
|||
import type { GridDto } from '@/proxy/form/models'
|
||||
import { getListForms } from '@/services/admin/list-form.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 VisualCanvas, { DESIGNER_DRAG_TYPE } from '@/components/visualDesigner/VisualCanvas'
|
||||
import {
|
||||
|
|
@ -1164,22 +1164,20 @@ const PropertyEditor = ({
|
|||
if (type === 'select' && options) {
|
||||
const currentValue = String(value ?? '')
|
||||
return (
|
||||
<select
|
||||
className={inputClass}
|
||||
value={currentValue}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
>
|
||||
{/* An empty value is not emitted, so the component keeps its own or its
|
||||
container's default — spell that out instead of showing a blank row. */}
|
||||
{!options.includes(currentValue) && (
|
||||
<option value="">{translate('::Abp.Mailing.Default')}</option>
|
||||
)}
|
||||
{options.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/* An empty value is not emitted, so the component keeps its own or its
|
||||
container's default — spell that out instead of showing a blank row. */
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
menuPortalTarget={window.document.body}
|
||||
options={options.map((option) => ({ value: option, label: option }))}
|
||||
value={
|
||||
options.includes(currentValue)
|
||||
? { value: currentValue, label: currentValue }
|
||||
: { value: '', label: translate('::Abp.Mailing.Default') }
|
||||
}
|
||||
onChange={(option) => onChange(option?.value ?? '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// 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">
|
||||
Koleksiyon
|
||||
</span>
|
||||
<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"
|
||||
value={selectOptionsBinding.path}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
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, {
|
||||
path: event.target.value,
|
||||
path: option?.value ?? '',
|
||||
labelPath: '',
|
||||
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>
|
||||
{selectCollectionSample && (
|
||||
<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">
|
||||
{translate('::' + label)}
|
||||
</span>
|
||||
<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"
|
||||
value={selectOptionsBinding[key] || ''}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
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, {
|
||||
[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>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -3609,28 +3617,25 @@ const VisualComponentDesigner = () => {
|
|||
</button>
|
||||
</span>
|
||||
))}
|
||||
<select
|
||||
className="rounded-md border border-slate-300 bg-white px-2 py-1 text-[10px] dark:border-slate-700 dark:bg-slate-900"
|
||||
value=""
|
||||
onChange={(event) => {
|
||||
const next = event.target.value
|
||||
<Select
|
||||
size="xs"
|
||||
className="min-w-[8rem]"
|
||||
placeholder={translate(
|
||||
'::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
|
||||
updateSelectedBindingDetails(optionDataProperty, {
|
||||
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>
|
||||
<p className="mt-1 text-[10px] leading-4 text-slate-500">
|
||||
{translate('::App.DeveloperKitComponentDesigner.ExtraColumnsHint')}{' '}
|
||||
|
|
@ -3764,42 +3769,54 @@ const VisualComponentDesigner = () => {
|
|||
</label>
|
||||
{lookup && (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
<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"
|
||||
value={lookup.sourceId}
|
||||
onChange={(event) => selectColumnLookupSource(column, event.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.DeveloperKitComponentDesigner.SelectEndpoint')}
|
||||
</option>
|
||||
{bindableDataSources.map((source) => (
|
||||
<option key={source.id} value={source.id}>
|
||||
{source.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.SelectEndpoint',
|
||||
)}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={bindableDataSources.map((source) => ({
|
||||
value: source.id,
|
||||
label: source.name,
|
||||
}))}
|
||||
value={bindableDataSources
|
||||
.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 && (
|
||||
<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"
|
||||
value={lookup.path}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
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, {
|
||||
path: event.target.value,
|
||||
path: option?.value ?? '',
|
||||
valueField: '',
|
||||
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 && (
|
||||
<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">
|
||||
{translate(label)}
|
||||
</span>
|
||||
<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"
|
||||
value={lookup[field]}
|
||||
onChange={(event) =>
|
||||
writeColumnLookup(column, { [field]: event.target.value })
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
{rowFields.length
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
maxMenuHeight={200}
|
||||
placeholder={
|
||||
rowFields.length
|
||||
? translate('::App.DeveloperKitComponentDesigner.SelectField')
|
||||
: translate(
|
||||
'::App.DeveloperKitComponentDesigner.NoColumnInResponse',
|
||||
)}
|
||||
</option>
|
||||
{rowFields.map((path) => (
|
||||
<option key={path} value={path}>
|
||||
{path}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={rowFields.map((path) => ({ value: path, label: path }))}
|
||||
value={
|
||||
lookup[field]
|
||||
? { value: lookup[field], label: lookup[field] }
|
||||
: null
|
||||
}
|
||||
onChange={(option) =>
|
||||
writeColumnLookup(column, { [field]: option?.value ?? '' })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -3987,6 +4007,15 @@ const VisualComponentDesigner = () => {
|
|||
const patchFilter = (id: string, updates: Partial<DesignerDataSourceFilter>) =>
|
||||
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 (
|
||||
<div className="rounded-lg border border-slate-200 p-3 dark:border-slate-800">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
|
|
@ -4038,25 +4067,28 @@ const VisualComponentDesigner = () => {
|
|||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{strictColumns ? (
|
||||
<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"
|
||||
value={filter.field || ''}
|
||||
onChange={(event) => patchFilter(filter.id, { field: event.target.value })}
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.DeveloperKitComponentDesigner.FilterColumn')}
|
||||
</option>
|
||||
{/* A field configured before the list form changed is kept in
|
||||
the list, so switching forms does not silently blank it. */}
|
||||
{(filter.field && !columns.includes(filter.field)
|
||||
<Select
|
||||
size="xs"
|
||||
className="min-w-0 flex-1"
|
||||
isClearable
|
||||
maxMenuHeight={200}
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.FilterColumn',
|
||||
)}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={(filter.field && !columns.includes(filter.field)
|
||||
? [filter.field, ...columns]
|
||||
: columns
|
||||
).map((column) => (
|
||||
<option key={column} value={column}>
|
||||
{column}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
).map((column) => ({ value: column, label: column }))}
|
||||
value={
|
||||
filter.field
|
||||
? { value: filter.field, label: filter.field }
|
||||
: null
|
||||
}
|
||||
onChange={(option) =>
|
||||
patchFilter(filter.id, { field: option?.value ?? '' })
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
unstyle
|
||||
|
|
@ -4067,21 +4099,26 @@ const VisualComponentDesigner = () => {
|
|||
onChange={(event) => patchFilter(filter.id, { field: event.target.value })}
|
||||
/>
|
||||
)}
|
||||
<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"
|
||||
value={filter.operator}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-24"
|
||||
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, {
|
||||
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
|
||||
className="rounded p-1 text-slate-400 hover:text-red-600"
|
||||
title={translate('::App.Platform.Delete')}
|
||||
|
|
@ -4095,26 +4132,23 @@ const VisualComponentDesigner = () => {
|
|||
</div>
|
||||
{!valueless && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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"
|
||||
value={filter.source}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-24"
|
||||
maxMenuHeight={200}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={filterSourceOptions}
|
||||
value={filterSourceOptions.filter(
|
||||
(option) => option.value === filter.source,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option &&
|
||||
patchFilter(filter.id, {
|
||||
source: event.target.value as DesignerFilterSource,
|
||||
source: option.value as DesignerFilterSource,
|
||||
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' && (
|
||||
<Input
|
||||
unstyle
|
||||
|
|
@ -4135,24 +4169,28 @@ const VisualComponentDesigner = () => {
|
|||
next to the source selector left neither of them usable. */}
|
||||
{!valueless && filter.source === 'record' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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"
|
||||
value={masterRef}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="xs"
|
||||
className="min-w-0 flex-1"
|
||||
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, {
|
||||
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
|
||||
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"
|
||||
|
|
@ -4264,11 +4302,25 @@ const VisualComponentDesigner = () => {
|
|||
<p className="mb-2 text-[10px] leading-4 text-slate-500">
|
||||
{translate('::' + slot.description)}
|
||||
</p>
|
||||
<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"
|
||||
value={currentValue}
|
||||
onChange={(event) => {
|
||||
const sourceId = event.target.value
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
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)
|
||||
const source = document.dataSources.find((item) => item.id === sourceId)
|
||||
if (
|
||||
|
|
@ -4279,16 +4331,7 @@ const VisualComponentDesigner = () => {
|
|||
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 && (
|
||||
<p className="mt-1.5 text-[10px] leading-4 text-amber-600">
|
||||
{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">
|
||||
Koleksiyon
|
||||
</span>
|
||||
<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"
|
||||
value={String(selectedNode.props.collectionPath ?? '')}
|
||||
onChange={(event) => updateSelectedProp('collectionPath', event.target.value)}
|
||||
>
|
||||
{collectionPaths.map((path) => (
|
||||
<option key={path || '__root__'} value={path}>
|
||||
{path || translate('::App.DeveloperKitComponentDesigner.ResponseItself')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
maxMenuHeight={200}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={collectionPaths.map((path) => ({
|
||||
value: path,
|
||||
label:
|
||||
path || translate('::App.DeveloperKitComponentDesigner.ResponseItself'),
|
||||
}))}
|
||||
value={{
|
||||
value: String(selectedNode.props.collectionPath ?? ''),
|
||||
label:
|
||||
String(selectedNode.props.collectionPath ?? '') ||
|
||||
translate('::App.DeveloperKitComponentDesigner.ResponseItself'),
|
||||
}}
|
||||
onChange={(option) =>
|
||||
updateSelectedProp('collectionPath', option?.value ?? '')
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<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">
|
||||
{translate('::App.DeveloperKitComponentDesigner.KeyParamHint')}
|
||||
</p>
|
||||
<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"
|
||||
value={getSqlDataSourceKeySource(selectedNode)}
|
||||
onChange={(event) => updateSelectedProp('keySource', event.target.value)}
|
||||
>
|
||||
{SQL_DATA_SOURCE_KEY_SOURCES.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="mb-2 w-full"
|
||||
maxMenuHeight={200}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={SQL_DATA_SOURCE_KEY_SOURCES}
|
||||
value={SQL_DATA_SOURCE_KEY_SOURCES.filter(
|
||||
(item) => item.value === getSqlDataSourceKeySource(selectedNode),
|
||||
)}
|
||||
onChange={(option) => option && updateSelectedProp('keySource', option.value)}
|
||||
/>
|
||||
<Input
|
||||
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"
|
||||
|
|
@ -4499,6 +4551,10 @@ const VisualComponentDesigner = () => {
|
|||
const column = binding?.sourceId === sqlScopeNode.id ? binding.path : ''
|
||||
const rawDefault = selectedNode.props?.[SQL_DEFAULT_VALUE_PROP]
|
||||
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}`
|
||||
// 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.
|
||||
|
|
@ -4552,18 +4608,24 @@ const VisualComponentDesigner = () => {
|
|||
})}
|
||||
</p>
|
||||
{sqlRecordProperty === 'checked' ? (
|
||||
<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"
|
||||
disabled={!column}
|
||||
value={currentDefault}
|
||||
onChange={(event) => updateSelectedProp(SQL_DEFAULT_VALUE_PROP, event.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoDefaultValue')}
|
||||
</option>
|
||||
<option value="true">{translate('::App.Platform.Yes')}</option>
|
||||
<option value="false">{translate('::App.Platform.No')}</option>
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
isDisabled={!column}
|
||||
maxMenuHeight={200}
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.NoDefaultValue',
|
||||
)}
|
||||
menuPortalTarget={window.document.body}
|
||||
options={sqlDefaultValueOptions}
|
||||
value={sqlDefaultValueOptions.filter(
|
||||
(option) => option.value === currentDefault,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
updateSelectedProp(SQL_DEFAULT_VALUE_PROP, option?.value ?? '')
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
unstyle
|
||||
|
|
@ -4665,11 +4727,24 @@ const VisualComponentDesigner = () => {
|
|||
{sqlScopeSource?.name}
|
||||
</div>
|
||||
) : (
|
||||
<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"
|
||||
value={activeDataSource?.id || ''}
|
||||
onChange={(event) => {
|
||||
const sourceId = event.target.value
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
isClearable
|
||||
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)
|
||||
if (
|
||||
(isOptionDataComponent(selectedNode?.type) ||
|
||||
|
|
@ -4703,20 +4778,7 @@ const VisualComponentDesigner = () => {
|
|||
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>
|
||||
</div>
|
||||
|
|
@ -4733,24 +4795,30 @@ const VisualComponentDesigner = () => {
|
|||
<span className="mb-1 block text-[10px] font-semibold uppercase tracking-wider text-slate-400">
|
||||
Koleksiyon
|
||||
</span>
|
||||
<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"
|
||||
value={tabularItemsBinding.path}
|
||||
onChange={(event) =>
|
||||
updateSelectedBindingDetails('items', { path: event.target.value })
|
||||
}
|
||||
>
|
||||
{!tabularCollectionPaths.length && (
|
||||
<option value="">
|
||||
{translate('::App.DeveloperKitComponentDesigner.NoCollectionFound')}
|
||||
</option>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
maxMenuHeight={200}
|
||||
placeholder={translate(
|
||||
'::App.DeveloperKitComponentDesigner.NoCollectionFound',
|
||||
)}
|
||||
{tabularCollectionPaths.map((path) => (
|
||||
<option key={path || '__root__'} value={path}>
|
||||
{path || translate('::App.DeveloperKitComponentDesigner.WholeResponseArray')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
menuPortalTarget={window.document.body}
|
||||
options={tabularCollectionPaths.map((path) => ({
|
||||
value: path,
|
||||
label:
|
||||
path ||
|
||||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
|
||||
}))}
|
||||
value={{
|
||||
value: tabularItemsBinding.path,
|
||||
label:
|
||||
tabularItemsBinding.path ||
|
||||
translate('::App.DeveloperKitComponentDesigner.WholeResponseArray'),
|
||||
}}
|
||||
onChange={(option) =>
|
||||
updateSelectedBindingDetails('items', { path: option?.value ?? '' })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -4871,6 +4939,29 @@ const VisualComponentDesigner = () => {
|
|||
choice === '__root__' ||
|
||||
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 (
|
||||
<label
|
||||
key={property.name}
|
||||
|
|
@ -4882,11 +4973,21 @@ const VisualComponentDesigner = () => {
|
|||
{property.tsType || property.type}
|
||||
</span>
|
||||
</span>
|
||||
<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"
|
||||
value={choice}
|
||||
onChange={(event) => {
|
||||
const nextChoice = event.target.value
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
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) {
|
||||
updateSelectedBinding(property.name, '')
|
||||
return
|
||||
|
|
@ -4898,31 +4999,7 @@ const VisualComponentDesigner = () => {
|
|||
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 && (
|
||||
<div className="mt-1.5 truncate font-mono text-[9px] text-emerald-600">
|
||||
{currentBindingSource?.name || currentBinding.sourceId}:{' '}
|
||||
|
|
@ -6021,21 +6098,26 @@ const VisualComponentDesigner = () => {
|
|||
<span className="mb-1.5 block text-[10px] font-semibold uppercase text-slate-500">
|
||||
Metot
|
||||
</span>
|
||||
<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"
|
||||
value={catalogSourceEditor.draft.method}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="sm"
|
||||
className="w-full"
|
||||
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({
|
||||
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>
|
||||
</div>
|
||||
<label className="block">
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useState } from 'react'
|
|||
import { FaCode, FaGripVertical, FaMagic, FaPlus, FaTrash } from 'react-icons/fa'
|
||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
import {
|
||||
GROUP_MODES,
|
||||
GROUP_MODE_LABEL,
|
||||
|
|
@ -86,6 +87,19 @@ export function CriteriaGrid({
|
|||
|
||||
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 =
|
||||
'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">
|
||||
<tr>
|
||||
<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}
|
||||
</th>
|
||||
<th className="min-w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
|
||||
{labels.alias}
|
||||
</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}
|
||||
</th>
|
||||
<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}
|
||||
</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}
|
||||
</th>
|
||||
<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 })}
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
className={cellCls}
|
||||
value={row.columnName}
|
||||
onChange={(e) => onUpdateRow(row.id, { columnName: e.target.value })}
|
||||
>
|
||||
<option value="">—</option>
|
||||
{columnsOf(row.sourceId).map((c) => (
|
||||
<option key={c.columnName} value={c.columnName}>
|
||||
{c.columnName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
placeholder="—"
|
||||
menuPortalTarget={document.body}
|
||||
options={columnsOf(row.sourceId).map((c) => ({
|
||||
value: c.columnName ?? '',
|
||||
label: c.columnName ?? '',
|
||||
}))}
|
||||
value={
|
||||
row.columnName ? { value: row.columnName, label: row.columnName } : null
|
||||
}
|
||||
onChange={(option) =>
|
||||
onUpdateRow(row.id, { columnName: option?.value ?? '' })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
|
||||
|
|
@ -230,19 +249,16 @@ export function CriteriaGrid({
|
|||
{isExpression ? (
|
||||
<span className="px-1 text-[10px] italic text-gray-400">SQL</span>
|
||||
) : (
|
||||
<select
|
||||
className={cellCls}
|
||||
value={row.sourceId}
|
||||
onChange={(e) =>
|
||||
onUpdateRow(row.id, { sourceId: e.target.value, columnName: '' })
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
menuPortalTarget={document.body}
|
||||
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>
|
||||
|
||||
|
|
@ -259,33 +275,36 @@ export function CriteriaGrid({
|
|||
|
||||
{grouped && (
|
||||
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
|
||||
<select
|
||||
className={cellCls}
|
||||
value={row.groupMode}
|
||||
disabled={isExpression}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value as GroupMode
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isDisabled={isExpression}
|
||||
menuPortalTarget={document.body}
|
||||
options={groupModeOptions}
|
||||
value={groupModeOptions.filter((option) => option.value === row.groupMode)}
|
||||
onChange={(option) => {
|
||||
if (!option) return
|
||||
const mode = option.value
|
||||
onUpdateRow(row.id, {
|
||||
groupMode: mode,
|
||||
...(mode === 'Where' ? { output: false } : {}),
|
||||
})
|
||||
}}
|
||||
>
|
||||
{GROUP_MODES.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{GROUP_MODE_LABEL[m]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
|
||||
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
|
||||
<select
|
||||
className={cellCls}
|
||||
value={row.sortType}
|
||||
onChange={(e) => {
|
||||
const sortType = e.target.value as '' | 'ASC' | 'DESC'
|
||||
<Select
|
||||
size="xs"
|
||||
className="w-full"
|
||||
isClearable
|
||||
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, {
|
||||
sortType,
|
||||
sortOrder: sortType
|
||||
|
|
@ -293,11 +312,7 @@ export function CriteriaGrid({
|
|||
: 0,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<option value="">—</option>
|
||||
<option value="ASC">Ascending</option>
|
||||
<option value="DESC">Descending</option>
|
||||
</select>
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
|
||||
|
|
@ -395,6 +410,11 @@ function FilterBuilder({
|
|||
const [value2, setValue2] = useState('')
|
||||
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 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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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"
|
||||
value={operator}
|
||||
onChange={(e) => setOperator(e.target.value)}
|
||||
>
|
||||
{OPERATORS.map((op) => (
|
||||
<option key={op} value={op}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="mb-1 w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={OPERATORS.map((op) => ({ value: op, label: op }))}
|
||||
value={{ value: operator, label: operator }}
|
||||
onChange={(option) => option && setOperator(option.value)}
|
||||
/>
|
||||
{needsValue && (
|
||||
<>
|
||||
<Input
|
||||
|
|
@ -447,14 +464,14 @@ function FilterBuilder({
|
|||
onChange={(e) => setValue2(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<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"
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value as 'literal' | 'expression')}
|
||||
>
|
||||
<option value="literal">{labels.builderLiteral}</option>
|
||||
<option value="expression">{labels.builderExpression}</option>
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="mb-1 w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={modeOptions}
|
||||
value={modeOptions.filter((option) => option.value === mode)}
|
||||
onChange={(option) => option && setMode(option.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from 'react-icons/fa'
|
||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
import {
|
||||
BOX_FOOTER_HEIGHT,
|
||||
BOX_HEADER_HEIGHT,
|
||||
|
|
@ -343,6 +344,15 @@ export function DiagramPane({
|
|||
? sources.find((s) => s.id === activeJoin.sourceId)
|
||||
: 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 (
|
||||
<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]"
|
||||
|
|
@ -416,34 +426,33 @@ export function DiagramPane({
|
|||
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
|
||||
{labels.joinType}
|
||||
</div>
|
||||
<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"
|
||||
value={activeJoinSource.joinKind}
|
||||
onChange={(e) =>
|
||||
onUpdateSource(activeJoinSource.id, { joinKind: e.target.value as JoinKind })
|
||||
<Select
|
||||
size="xs"
|
||||
className="mb-1.5 w-full"
|
||||
menuPortalTarget={document.body}
|
||||
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) => (
|
||||
<option key={k} value={k}>
|
||||
{k === 'INNER' ? 'INNER JOIN' : `${k} OUTER JOIN`}
|
||||
</option>
|
||||
))}
|
||||
</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"
|
||||
value={activeJoin.operator}
|
||||
onChange={(e) =>
|
||||
/>
|
||||
<Select
|
||||
size="xs"
|
||||
className="mb-1.5 w-full"
|
||||
menuPortalTarget={document.body}
|
||||
options={joinOperatorOptions}
|
||||
value={joinOperatorOptions.filter(
|
||||
(option) => option.value === activeJoin.operator,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
option &&
|
||||
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
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -388,7 +388,7 @@ function AnnouncementModalContent({ announcement, onClose, onLikeChange }: Annou
|
|||
'::App.SocialWallPostItem.CommentPlaceholder',
|
||||
)}
|
||||
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
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ function EventModalContent({ event, onClose, onLikeChange }: EventModalProps) {
|
|||
onKeyDown={handleKeyDown}
|
||||
placeholder={translate('::App.Events.EventAttendance')}
|
||||
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
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from 'react'
|
|||
import { GridExtraFilterState } from './Utils'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
export function GridExtraFilterToolbar({
|
||||
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 (
|
||||
<div key={fs.fieldName} className="flex items-center gap-2">
|
||||
<label>{fs.caption}</label>
|
||||
|
||||
{fs.controlType === 'Select' ? (
|
||||
<select
|
||||
className="border rounded px-1.5 py-1.5 dark:bg-gray-700 dark:text-white"
|
||||
value={current?.value ?? ''}
|
||||
onChange={(e) => handleSave(e.target.value)}
|
||||
>
|
||||
{!fs.defaultValue && <option value="">{translate('::App.Platform.Select')}</option>}
|
||||
{fs.items?.map((item: any) => (
|
||||
<option key={item.key} value={item.key}>
|
||||
{item.value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="xs"
|
||||
className="min-w-[10rem]"
|
||||
isClearable={!fs.defaultValue}
|
||||
placeholder={translate('::App.Platform.Select')}
|
||||
menuPortalTarget={document.body}
|
||||
options={itemOptions}
|
||||
value={itemOptions.filter(
|
||||
(option) => option.value === String(current?.value ?? ''),
|
||||
)}
|
||||
onChange={(option) => handleSave(option?.value ?? '')}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
unstyle
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import type { GridColumnData } from './GridColumnData'
|
|||
import { flattenGridColumns } from './shared/columns'
|
||||
import { useStoreState } from '@/store/store'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
|
|
@ -279,6 +280,11 @@ const TodoBoardContent = ({
|
|||
[lookupText, options.statusExpr, translate],
|
||||
)
|
||||
|
||||
const statusSelectOptions = statuses.map((status) => ({
|
||||
value: status,
|
||||
label: statusText(status),
|
||||
}))
|
||||
|
||||
const clearDragState = useCallback(() => {
|
||||
dragActiveRef.current = false
|
||||
setDraggedKey(undefined)
|
||||
|
|
@ -1000,23 +1006,21 @@ const TodoBoardContent = ({
|
|||
{options.statusExpr && (
|
||||
<div>
|
||||
<label className={labelClass}>{translate('::App.Listform.ListformField.Status')}</label>
|
||||
<select
|
||||
className={inputClass}
|
||||
disabled={!canUpdate}
|
||||
value={String(draft[options.statusExpr] ?? '')}
|
||||
onChange={(event) =>
|
||||
<Select
|
||||
size="sm"
|
||||
isDisabled={!canUpdate}
|
||||
menuPortalTarget={document.body}
|
||||
options={statusSelectOptions}
|
||||
value={statusSelectOptions.filter(
|
||||
(option) => option.value === String(draft[options.statusExpr!] ?? ''),
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
[options.statusExpr!]: event.target.value,
|
||||
[options.statusExpr!]: option?.value ?? '',
|
||||
}))
|
||||
}
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{statusText(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{options.dueDateExpr && (
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ const MenuItemComponentBase: React.FC<MenuItemComponentProps> = ({
|
|||
{isModalOpen && (
|
||||
<Dialog
|
||||
isOpen={isModalOpen}
|
||||
width={640}
|
||||
width={900}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onRequestClose={() => setIsModalOpen(false)}
|
||||
>
|
||||
|
|
@ -497,6 +497,8 @@ const MenuItemComponentBase: React.FC<MenuItemComponentProps> = ({
|
|||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||
<Select
|
||||
isClearable
|
||||
maxMenuHeight={220}
|
||||
menuPortalTarget={document.body}
|
||||
field={field}
|
||||
form={form}
|
||||
options={permissions}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
|||
import { useConfig } from '@/components/ui/ConfigProvider'
|
||||
import { useForm } from '@/components/ui/Form/context'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Select from '@/components/ui/Select'
|
||||
|
||||
const menuService = new MenuService()
|
||||
|
||||
|
|
@ -253,6 +254,11 @@ export function MenuAddDialog({
|
|||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
}, [isEditMode, rawItems, editMenu])
|
||||
|
||||
const parentSelectOptions = parentOptions.map((option) => ({
|
||||
value: option.code,
|
||||
label: option.label,
|
||||
}))
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!form.code.trim() || !form.menuTextEn.trim()) return
|
||||
if (shortNameRequired && !form.shortName.trim()) return
|
||||
|
|
@ -420,18 +426,19 @@ export function MenuAddDialog({
|
|||
<div className="flex flex-col">
|
||||
<label className={labelCls}>{translate('::App.Platform.MenuParent')}</label>
|
||||
{isEditMode ? (
|
||||
<select
|
||||
value={form.parentCode}
|
||||
onChange={(e) => setForm((p) => ({ ...p, parentCode: e.target.value }))}
|
||||
className={fieldCls}
|
||||
>
|
||||
<option value="">{translate('::App.WizardStep1.RootMenu')}</option>
|
||||
{parentOptions.map((option) => (
|
||||
<option key={option.code} value={option.code}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
size="sm"
|
||||
isClearable
|
||||
placeholder={translate('::App.WizardStep1.RootMenu')}
|
||||
menuPortalTarget={document.body}
|
||||
options={parentSelectOptions}
|
||||
value={parentSelectOptions.filter(
|
||||
(option) => option.value === form.parentCode,
|
||||
)}
|
||||
onChange={(option) =>
|
||||
setForm((p) => ({ ...p, parentCode: option?.value ?? '' }))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Input unstyle disabled value={form.parentCode} className={disabledCls} />
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue