mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
8.3 KiB
261 lines
8.3 KiB
<script lang="ts" setup> |
|
const { form, formState, formElementsCategorized, isLoading, validateInfos } = useFormBuilderHelperOrThrow() |
|
|
|
const deepReference = (path: string): any => { |
|
return path.split('.').reduce((acc, key) => acc[key], formState.value) |
|
} |
|
|
|
const setFormState = (path: string, value: any) => { |
|
// update nested prop in formState |
|
const keys = path.split('.') |
|
const lastKey = keys.pop() |
|
|
|
if (!lastKey) return |
|
|
|
const target = keys.reduce((acc, key) => { |
|
if (!acc[key]) { |
|
acc[key] = {} |
|
} |
|
return acc[key] |
|
}, formState.value) |
|
target[lastKey] = value |
|
} |
|
</script> |
|
|
|
<template> |
|
<div class="nc-form-builder nc-scrollbar-thin relative"> |
|
<a-form ref="form" :model="formState" hide-required-mark layout="vertical" class="flex flex-col gap-4"> |
|
<template v-for="category in Object.keys(formElementsCategorized)" :key="category"> |
|
<div class="nc-form-section"> |
|
<div v-if="category !== FORM_BUILDER_NON_CATEGORIZED" class="nc-form-section-title">{{ category }}</div> |
|
<div class="nc-form-section-body"> |
|
<div class="flex flex-wrap"> |
|
<template v-for="field in formElementsCategorized[category]" :key="field.model"> |
|
<div |
|
v-if="field.type === FormBuilderInputType.Space" |
|
:style="`width:${+field.width || 100}%`" |
|
class="w-full" |
|
></div> |
|
<a-form-item |
|
v-else |
|
v-bind="validateInfos[field.model]" |
|
class="nc-form-item" |
|
:style="`width:${+field.width || 100}%`" |
|
:required="false" |
|
:data-testid="`nc-form-input-${field.model}`" |
|
> |
|
<template v-if="![FormBuilderInputType.Switch].includes(field.type)" #label> |
|
<div class="flex items-center gap-1"> |
|
<span>{{ field.label }}</span> |
|
<span v-if="field.required" class="text-red-500">*</span> |
|
<NcTooltip v-if="field.helpText && field.showHintAsTooltip"> |
|
<template #title> |
|
<div class="text-xs"> |
|
{{ field.helpText }} |
|
</div> |
|
</template> |
|
<GeneralIcon icon="info" class="text-gray-500 h-4" /> |
|
</NcTooltip> |
|
</div> |
|
</template> |
|
<template v-if="field.type === FormBuilderInputType.Input"> |
|
<a-input |
|
autocomplete="off" |
|
class="!w-full" |
|
:value="deepReference(field.model)" |
|
@update:value="setFormState(field.model, $event)" |
|
/> |
|
</template> |
|
<template v-else-if="field.type === FormBuilderInputType.Password"> |
|
<a-input-password |
|
readonly |
|
onfocus="this.removeAttribute('readonly');" |
|
onblur="this.setAttribute('readonly', true);" |
|
autocomplete="off" |
|
:value="deepReference(field.model)" |
|
@update:value="setFormState(field.model, $event)" |
|
/> |
|
</template> |
|
<template v-else-if="field.type === FormBuilderInputType.Select"> |
|
<NcSelect |
|
:value="deepReference(field.model)" |
|
:options="field.options" |
|
@update:value="setFormState(field.model, $event)" |
|
/> |
|
</template> |
|
<template v-else-if="field.type === FormBuilderInputType.Switch"> |
|
<div class="flex flex-col p-2" :class="field.border ? 'border-1 rounded-lg shadow' : ''"> |
|
<div class="flex items-center"> |
|
<NcSwitch :checked="!!deepReference(field.model)" @update:checked="setFormState(field.model, $event)" /> |
|
<span class="ml-[6px] font-bold">{{ field.label }}</span> |
|
<NcTooltip v-if="field.helpText"> |
|
<template #title> |
|
<div class="text-xs"> |
|
{{ field.helpText }} |
|
</div> |
|
</template> |
|
<GeneralIcon icon="info" class="text-gray-500 h-4 ml-1" /> |
|
</NcTooltip> |
|
</div> |
|
<div v-if="field.helpText && !field.showHintAsTooltip" class="w-full mt-1 pl-[35px]"> |
|
<div class="text-xs text-gray-500">{{ field.helpText }}</div> |
|
</div> |
|
</div> |
|
</template> |
|
<div |
|
v-if="field.helpText && field.type !== FormBuilderInputType.Switch && !field.showHintAsTooltip" |
|
class="w-full mt-1" |
|
> |
|
<div class="text-xs text-gray-500">{{ field.helpText }}</div> |
|
</div> |
|
</a-form-item> |
|
</template> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
</a-form> |
|
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15"> |
|
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000"> |
|
<a-spin size="large" /> |
|
</div> |
|
</general-overlay> |
|
</div> |
|
</template> |
|
|
|
<style lang="scss" scoped> |
|
.nc-form-item { |
|
margin-bottom: 12px; |
|
} |
|
|
|
:deep(.ant-collapse-header) { |
|
@apply !-mt-4 !p-0 flex items-center !cursor-default children:first:flex; |
|
} |
|
|
|
:deep(.ant-collapse-icon-position-right > .ant-collapse-item > .ant-collapse-header .ant-collapse-arrow) { |
|
@apply !right-0; |
|
} |
|
|
|
:deep(.ant-collapse-content-box) { |
|
@apply !px-0 !pb-0 !pt-3; |
|
} |
|
|
|
:deep(.ant-form-item-explain-error) { |
|
@apply !text-xs; |
|
} |
|
|
|
:deep(.ant-divider) { |
|
@apply m-0; |
|
} |
|
|
|
:deep(.ant-form-item-with-help .ant-form-item-explain) { |
|
@apply !min-h-0; |
|
} |
|
|
|
:deep(.ant-select .ant-select-selector .ant-select-selection-item) { |
|
@apply font-weight-400; |
|
} |
|
|
|
.nc-form-builder { |
|
:deep(.ant-input-affix-wrapper), |
|
:deep(.ant-input), |
|
:deep(.ant-select) { |
|
@apply !appearance-none border-solid rounded-md; |
|
} |
|
|
|
:deep(.ant-input-password) { |
|
input { |
|
@apply !border-none my-0; |
|
} |
|
} |
|
|
|
.nc-form-section { |
|
@apply flex flex-col gap-3; |
|
} |
|
|
|
.nc-form-section-title { |
|
@apply text-sm font-bold text-gray-800; |
|
} |
|
|
|
.nc-form-section-body { |
|
@apply flex flex-col gap-3; |
|
} |
|
|
|
:deep(.ant-form-item-label > label.ant-form-item-required:after) { |
|
@apply content-['*'] inline-block text-inherit text-red-500 ml-1; |
|
} |
|
|
|
:deep(.ant-form-item) { |
|
&.ant-form-item-has-error { |
|
&:not(:has(.ant-input-password)) .ant-input { |
|
&:not(:hover):not(:focus):not(:disabled) { |
|
@apply shadow-default; |
|
} |
|
|
|
&:hover:not(:focus):not(:disabled) { |
|
@apply shadow-hover; |
|
} |
|
|
|
&:focus { |
|
@apply shadow-error ring-0; |
|
} |
|
} |
|
|
|
.ant-input-number, |
|
.ant-input-affix-wrapper.ant-input-password { |
|
&:not(:hover):not(:focus-within):not(:disabled) { |
|
@apply shadow-default; |
|
} |
|
|
|
&:hover:not(:focus-within):not(:disabled) { |
|
@apply shadow-hover; |
|
} |
|
|
|
&:focus-within { |
|
@apply shadow-error ring-0; |
|
} |
|
} |
|
} |
|
|
|
&:not(.ant-form-item-has-error) { |
|
&:not(:has(.ant-input-password)) .ant-input { |
|
&:not(:hover):not(:focus):not(:disabled) { |
|
@apply shadow-default border-gray-200; |
|
} |
|
|
|
&:hover:not(:focus):not(:disabled) { |
|
@apply border-gray-200 shadow-hover; |
|
} |
|
|
|
&:focus { |
|
@apply shadow-selected ring-0; |
|
} |
|
} |
|
|
|
.ant-input-number, |
|
.ant-input-affix-wrapper.ant-input-password { |
|
&:not(:hover):not(:focus-within):not(:disabled) { |
|
@apply shadow-default border-gray-200; |
|
} |
|
|
|
&:hover:not(:focus-within):not(:disabled) { |
|
@apply border-gray-200 shadow-hover; |
|
} |
|
|
|
&:focus-within { |
|
@apply shadow-selected ring-0; |
|
} |
|
} |
|
} |
|
} |
|
|
|
:deep(.ant-row:not(.ant-form-item)) { |
|
@apply !-mx-1.5; |
|
& > .ant-col { |
|
@apply !px-1.5; |
|
} |
|
} |
|
} |
|
</style> |
|
|
|
<style lang="scss"></style>
|
|
|