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.
680 lines
22 KiB
680 lines
22 KiB
<script setup lang="ts"> |
|
import Draggable from 'vuedraggable' |
|
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk' |
|
import { message } from 'ant-design-vue' |
|
import type { Permission } from '~/composables/useUIPermission/rolePermissions' |
|
import { |
|
ActiveViewInj, |
|
IsFormInj, |
|
MetaInj, |
|
computed, |
|
extractSdkResponseErrorMsg, |
|
inject, |
|
onClickOutside, |
|
provide, |
|
reactive, |
|
ref, |
|
useDebounceFn, |
|
useGlobal, |
|
useNuxtApp, |
|
useUIPermission, |
|
useViewColumns, |
|
useViewData, |
|
watch, |
|
} from '#imports' |
|
|
|
provide(IsFormInj, ref(true)) |
|
|
|
// todo: generate hideCols based on default values |
|
const hiddenCols = ['created_at', 'updated_at'] |
|
|
|
const state = useGlobal() |
|
|
|
const formRef = ref() |
|
|
|
const { $api, $e } = useNuxtApp() |
|
|
|
const { isUIAllowed } = useUIPermission() |
|
|
|
const formState: Record<any, any> = reactive({}) |
|
|
|
const secondsRemain = ref(0) |
|
|
|
const isEditable = isUIAllowed('editFormView' as Permission) |
|
|
|
const meta = inject(MetaInj) |
|
|
|
const view = inject(ActiveViewInj) |
|
|
|
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any) |
|
|
|
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, false, async () => { |
|
await loadFormView() |
|
setFormData() |
|
}) |
|
|
|
const columns = computed(() => meta?.value?.columns || []) |
|
|
|
const localColumns = ref<Record<string, any>[]>([]) |
|
|
|
const hiddenColumns = ref<Record<string, any>[]>([]) |
|
|
|
const draggableRef = ref() |
|
|
|
const systemFieldsIds = ref<Record<string, any>[]>([]) |
|
|
|
const showColumnDropdown = ref(false) |
|
|
|
const moved = ref(false) |
|
|
|
const drag = ref(false) |
|
|
|
const emailMe = ref(false) |
|
|
|
const submitted = ref(false) |
|
|
|
const activeRow = ref('') |
|
|
|
function updateView() { |
|
if ((formViewData.value?.subheading?.length || 0) > 255) { |
|
message.error('Data too long for Form Description') |
|
return |
|
} |
|
|
|
updateFormView(formViewData.value) |
|
} |
|
|
|
async function submitForm() { |
|
try { |
|
await formRef.value?.validateFields() |
|
} catch (e: any) { |
|
e.errorFields.map((f: Record<string, any>) => message.error(f.errors.join(','))) |
|
return |
|
} |
|
|
|
await insertRow(formState) |
|
|
|
submitted.value = true |
|
} |
|
|
|
function isDbRequired(column: Record<string, any>) { |
|
if (hiddenCols.includes(column.fk_column_id)) { |
|
return false |
|
} |
|
|
|
let isRequired = |
|
// confirm column is not virtual |
|
(!isVirtualCol(column) && |
|
// column required / not null |
|
column.rqd && |
|
// column default value |
|
!column.cdf && |
|
// confirm it's not foreign key |
|
!columns.value.some( |
|
(c: Record<string, any>) => |
|
c.uidt === UITypes.LinkToAnotherRecord && |
|
c?.colOptions?.type === RelationTypes.BELONGS_TO && |
|
column.fk_column_id === c.colOptions.fk_child_column_id, |
|
)) || |
|
// primary column |
|
(column.pk && !column.ai && !column.cdf) |
|
if (column.uidt === UITypes.LinkToAnotherRecord && column.colOptions.type === RelationTypes.BELONGS_TO) { |
|
const col = columns.value.find((c: Record<string, any>) => c.id === column.colOptions.fk_child_column_id) as Record< |
|
string, |
|
any |
|
> |
|
if (col.rqd && !col.default) { |
|
isRequired = true |
|
} |
|
} |
|
|
|
return isRequired |
|
} |
|
|
|
function onMove(event: any) { |
|
const { newIndex, element, oldIndex } = event.added || event.moved || event.removed |
|
|
|
if (event.added) { |
|
element.show = true |
|
} |
|
|
|
if (event.removed) { |
|
element.show = false |
|
saveOrUpdate(element, oldIndex) |
|
} else { |
|
if (!localColumns.value.length || localColumns.value.length === 1) { |
|
element.order = 1 |
|
} else if (localColumns.value.length - 1 === newIndex) { |
|
element.order = (localColumns.value[newIndex - 1]?.order || 0) + 1 |
|
} else if (newIndex === 0) { |
|
element.order = (localColumns.value[1]?.order || 0) / 2 |
|
} else { |
|
element.order = ((localColumns.value[newIndex - 1]?.order || 0) + (localColumns.value[newIndex + 1].order || 0)) / 2 |
|
} |
|
saveOrUpdate(element, newIndex) |
|
} |
|
|
|
$e('a:form-view:reorder') |
|
} |
|
|
|
function hideColumn(idx: number) { |
|
if (isDbRequired(localColumns.value[idx]) || localColumns.value[idx].required) { |
|
message.info("Required field can't be moved") |
|
return |
|
} |
|
|
|
saveOrUpdate( |
|
{ |
|
...localColumns.value[idx], |
|
show: false, |
|
}, |
|
idx, |
|
) |
|
;(localColumns.value[idx] as any).show = false |
|
|
|
$e('a:form-view:hide-columns') |
|
} |
|
|
|
async function addAllColumns() { |
|
for (const col of (formColumnData as Record<string, any>)?.value) { |
|
if (!systemFieldsIds.value.includes(col.fk_column_id)) { |
|
col.show = true |
|
} |
|
} |
|
await showAll(systemFieldsIds.value) |
|
$e('a:form-view:add-all') |
|
} |
|
|
|
async function removeAllColumns() { |
|
for (const col of (formColumnData as Record<string, any>)?.value) { |
|
if (isDbRequired(col) || !!col.required) { |
|
continue |
|
} |
|
col.show = false |
|
} |
|
await hideAll( |
|
(localColumns as Record<string, any>)?.value |
|
.filter((f: Record<string, any>) => isDbRequired(f) || !!f.required) |
|
.map((f: Record<string, any>) => f.fk_column_id), |
|
) |
|
$e('a:form-view:remove-all') |
|
} |
|
|
|
async function checkSMTPStatus() { |
|
if (emailMe.value) { |
|
const emailPluginActive = await $api.plugin.status('SMTP') |
|
if (!emailPluginActive) { |
|
emailMe.value = false |
|
message.info('Please activate SMTP plugin in App store for enabling email notification') |
|
} |
|
} |
|
} |
|
|
|
function setFormData() { |
|
const col = (formColumnData as Record<string, any>)?.value |
|
|
|
formViewData.value = { |
|
...formViewData.value, |
|
submit_another_form: !!(formViewData?.value?.submit_another_form ?? 0), |
|
// todo: show_blank_form missing from FormType |
|
show_blank_form: !!((formViewData?.value as any)?.show_blank_form ?? 0), |
|
} as any |
|
|
|
{ |
|
// email me |
|
let data: Record<string, boolean> = {} |
|
try { |
|
data = JSON.parse(formViewData.value?.email || '') || {} |
|
} catch (e) { |
|
// noop |
|
} |
|
|
|
data[state.user.value?.email as string] = emailMe.value |
|
|
|
formViewData.value!.email = JSON.stringify(data) |
|
|
|
checkSMTPStatus() |
|
} |
|
|
|
localColumns.value = col |
|
.filter( |
|
(f: Record<string, any>) => f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula, |
|
) |
|
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order) |
|
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) })) |
|
|
|
systemFieldsIds.value = getSystemColumns(col).map((c: Record<string, any>) => c.fk_column_id) |
|
|
|
hiddenColumns.value = col.filter( |
|
(f: Record<string, any>) => !f.show && !systemFieldsIds.value.includes(f.fk_column_id) && f.uidt !== UITypes.Formula, |
|
) |
|
} |
|
|
|
function isRequired(_columnObj: Record<string, any>, required = false) { |
|
let columnObj = _columnObj |
|
if ( |
|
columnObj.uidt === UITypes.LinkToAnotherRecord && |
|
columnObj.colOptions && |
|
columnObj.colOptions.type === RelationTypes.BELONGS_TO |
|
) { |
|
columnObj = columns.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record< |
|
string, |
|
any |
|
> |
|
} |
|
|
|
return required || (columnObj && columnObj.rqd && !columnObj.cdf) |
|
} |
|
|
|
function onEmailChange() { |
|
updateView() |
|
checkSMTPStatus() |
|
} |
|
|
|
async function submitCallback() { |
|
await loadFormView() |
|
setFormData() |
|
showColumnDropdown.value = false |
|
} |
|
|
|
const updateColMeta = useDebounceFn(async (col: Record<string, any>) => { |
|
if (col.id) { |
|
try { |
|
await $api.dbView.formColumnUpdate(col.id, col) |
|
} catch (e: any) { |
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
} |
|
} |
|
}, 250) |
|
|
|
watch(submitted, (v) => { |
|
if (v && (formViewData?.value as any)?.show_blank_form) { |
|
secondsRemain.value = 5 |
|
const intvl = setInterval(() => { |
|
if (--secondsRemain.value < 0) { |
|
submitted.value = false |
|
clearInterval(intvl) |
|
} |
|
}, 1000) |
|
} |
|
}) |
|
|
|
function handleMouseUp(col: Record<string, any>) { |
|
if (!moved.value) { |
|
const index = localColumns.value.length |
|
col.order = (index ? localColumns.value[index - 1].order : 0) + 1 |
|
col.show = true |
|
saveOrUpdate(col, index) |
|
} |
|
} |
|
|
|
onClickOutside(draggableRef, () => { |
|
activeRow.value = '' |
|
}) |
|
|
|
onMounted(async () => { |
|
await loadFormView() |
|
setFormData() |
|
}) |
|
</script> |
|
|
|
<template> |
|
<a-row v-if="submitted" class="h-full"> |
|
<a-col :span="24"> |
|
<div v-if="formViewData" class="align-center justify-center text-center mt-2"> |
|
<a-alert type="success"> |
|
<template #message> |
|
<div class="text-center">{{ formViewData.success_msg || 'Successfully submitted form data' }}</div> |
|
</template> |
|
</a-alert> |
|
<div v-if="formViewData.show_blank_form" class="text-gray-400 mt-4"> |
|
New form will be loaded after {{ secondsRemain }} seconds |
|
</div> |
|
<div v-if="formViewData.submit_another_form" class="text-center mt-4"> |
|
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form</a-button> |
|
</div> |
|
</div> |
|
</a-col> |
|
</a-row> |
|
<a-row v-else class="h-full flex"> |
|
<a-col |
|
v-if="isEditable" |
|
:span="8" |
|
class="bg-[#f7f7f7] shadow-md pa-5 h-full overflow-auto scrollbar-thin-primary nc-form-left-drawer" |
|
> |
|
<div class="flex"> |
|
<div class="flex flex-row flex-1 text-lg"> |
|
<span> |
|
<!-- Fields --> |
|
{{ $t('objects.fields') }} |
|
</span> |
|
</div> |
|
<div class="flex flex-row"> |
|
<div class="cursor-pointer mr-2"> |
|
<span |
|
v-if="hiddenColumns.length" |
|
class="mr-2 nc-form-add-all" |
|
style="border-bottom: 2px solid rgb(218, 218, 218)" |
|
@click="addAllColumns" |
|
> |
|
<!-- Add all --> |
|
{{ $t('general.addAll') }} |
|
</span> |
|
<span |
|
v-if="localColumns.length" |
|
class="ml-2 nc-form-remove-all" |
|
style="border-bottom: 2px solid rgb(218, 218, 218)" |
|
@click="removeAllColumns" |
|
> |
|
<!-- Remove all --> |
|
{{ $t('general.removeAll') }} |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
<Draggable :list="hiddenColumns" draggable=".item" group="form-inputs" @start="drag = true" @end="drag = false"> |
|
<template #item="{ element }"> |
|
<a-card |
|
size="small" |
|
class="ma-0 pa-0 cursor-pointer item mb-2" |
|
@mousedown="moved = false" |
|
@mousemove="moved = false" |
|
@mouseup="handleMouseUp(element)" |
|
> |
|
<div class="flex"> |
|
<div class="flex flex-row flex-1"> |
|
<SmartsheetHeaderVirtualCell |
|
v-if="isVirtualCol(element)" |
|
:column="{ ...element, title: element.label || element.title }" |
|
:required="isRequired(element, element.required)" |
|
:hide-menu="true" |
|
/> |
|
<SmartsheetHeaderCell |
|
v-else |
|
class="w-full" |
|
:column="{ ...element, title: element.label || element.title }" |
|
:required="isRequired(element, element.required)" |
|
:hide-menu="true" |
|
/> |
|
</div> |
|
<div class="flex flex-row"> |
|
<mdi-drag-vertical class="flex flex-1" /> |
|
</div> |
|
</div> |
|
</a-card> |
|
</template> |
|
<template #footer> |
|
<div class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide"> |
|
<!-- Drag and drop fields here to hide --> |
|
{{ $t('msg.info.dragDropHide') }} |
|
</div> |
|
<a-dropdown v-model:visible="showColumnDropdown" :trigger="['click']"> |
|
<a-button type="link" class="w-full caption mt-2" size="large" @click.stop="showColumnDropdown = true"> |
|
<div class="flex items-center prose-sm justify-center text-gray-400"> |
|
<mdi-plus /> |
|
<!-- Add new field to this table --> |
|
{{ $t('activity.addField') }} |
|
</div> |
|
</a-button> |
|
<template #overlay> |
|
<SmartsheetColumnEditOrAddProvider |
|
v-if="showColumnDropdown" |
|
@submit="submitCallback" |
|
@cancel="showColumnDropdown = false" |
|
@click.stop |
|
@keydown.stop |
|
/> |
|
</template> |
|
</a-dropdown> |
|
</template> |
|
</Draggable> |
|
</a-col> |
|
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary"> |
|
<div class="h-[200px] !bg-[#dbdad7]"> |
|
<!-- for future implementation of cover image --> |
|
</div> |
|
<a-card |
|
class="h-full ma-0 rounded-b-0 pa-4" |
|
:body-style="{ |
|
maxWidth: '700px', |
|
margin: '0 auto', |
|
marginTop: '-200px', |
|
}" |
|
> |
|
<a-form ref="formRef" :model="formState" class="nc-form"> |
|
<a-card class="rounded ma-2 py-10 px-5"> |
|
<!-- Header --> |
|
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0"> |
|
<a-input |
|
v-model:value="formViewData.heading" |
|
class="w-full text-bold text-h3" |
|
size="large" |
|
hide-details |
|
placeholder="Form Title" |
|
:bordered="false" |
|
@blur="updateView" |
|
@keydown.enter="updateView" |
|
/> |
|
</a-form-item> |
|
|
|
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div> |
|
|
|
<!-- Sub Header --> |
|
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0"> |
|
<a-input |
|
v-model:value="formViewData.subheading" |
|
class="w-full" |
|
size="large" |
|
hide-details |
|
:placeholder="$t('msg.info.formDesc')" |
|
:bordered="false" |
|
:disabled="!isEditable" |
|
@blur="updateView" |
|
@click="updateView" |
|
/> |
|
</a-form-item> |
|
|
|
<div v-else class="ml-3 mb-5 w-full text-bold text-h3">{{ formViewData.subheading }}</div> |
|
|
|
<Draggable |
|
ref="draggableRef" |
|
:list="localColumns" |
|
item-key="fk_column_id" |
|
draggable=".item" |
|
group="form-inputs" |
|
class="h-100" |
|
@change="onMove($event)" |
|
@start="drag = true" |
|
@end="drag = false" |
|
> |
|
<template #item="{ element, index }"> |
|
<div |
|
class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3" |
|
:class="`nc-form-drag-${element.title.replaceAll(' ', '')}`" |
|
@click="activeRow = element.title" |
|
> |
|
<div class="flex"> |
|
<div class="flex flex-1"> |
|
<div class="flex flex-row"> |
|
<mdi-drag-vertical class="flex flex-1" /> |
|
</div> |
|
<SmartsheetHeaderVirtualCell |
|
v-if="isVirtualCol(element)" |
|
:column="{ ...element, title: element.label || element.title }" |
|
:required="isRequired(element, element.required)" |
|
:hide-menu="true" |
|
/> |
|
<SmartsheetHeaderCell |
|
v-else |
|
:column="{ ...element, title: element.label || element.title }" |
|
:required="isRequired(element, element.required)" |
|
:hide-menu="true" |
|
/> |
|
</div> |
|
<div v-if="isUIAllowed('editFormView') && !isRequired(element, element.required)" class="flex"> |
|
<mdi-eye-off-outline class="opacity-0 nc-field-remove-icon" @click.stop="hideColumn(index)" /> |
|
</div> |
|
</div> |
|
|
|
<a-form-item |
|
v-if="isVirtualCol(element)" |
|
class="ma-0 gap-0 pa-0" |
|
:name="element.title" |
|
:rules="[{ required: element.required, message: `${element.title} is required` }]" |
|
> |
|
<SmartsheetVirtualCell |
|
v-model="formState[element.title]" |
|
class="nc-input" |
|
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`" |
|
:column="element" |
|
/> |
|
</a-form-item> |
|
|
|
<a-form-item |
|
v-else |
|
class="ma-0 gap-0 pa-0" |
|
:name="element.title" |
|
:rules="[{ required: element.required, message: `${element.title} is required` }]" |
|
> |
|
<SmartsheetCell |
|
v-model="formState[element.title]" |
|
class="nc-input" |
|
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`" |
|
:column="element" |
|
:edit-enabled="true" |
|
/> |
|
</a-form-item> |
|
|
|
<div v-if="activeRow === element.title"> |
|
<a-form-item class="my-0 w-1/2"> |
|
<a-input |
|
v-model:value="element.label" |
|
size="small" |
|
class="form-meta-input !bg-[#dbdbdb] nc-form-input-label" |
|
:placeholder="$t('msg.info.formInput')" |
|
@change="updateColMeta(element)" |
|
> |
|
</a-input> |
|
</a-form-item> |
|
|
|
<a-form-item class="mt-2 mb-0 w-1/2"> |
|
<a-input |
|
v-model:value="element.description" |
|
size="small" |
|
class="form-meta-input !bg-[#dbdbdb] text-sm nc-form-input-help-text" |
|
:placeholder="$t('msg.info.formHelpText')" |
|
@change="updateColMeta(element)" |
|
/> |
|
</a-form-item> |
|
|
|
<div class="items-center flex"> |
|
<span class="text-sm text-gray-500 mr-2 nc-form-input-required">{{ $t('general.required') }}</span> |
|
|
|
<a-switch v-model:checked="element.required" size="small" class="my-2" @change="updateColMeta(element)" /> |
|
</div> |
|
</div> |
|
|
|
<span class="text-gray-500">{{ element.description }}</span> |
|
</div> |
|
</template> |
|
|
|
<template #footer> |
|
<div |
|
v-if="!localColumns.length" |
|
class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center" |
|
> |
|
Drag and drop fields here to add |
|
</div> |
|
</template> |
|
</Draggable> |
|
|
|
<div class="justify-center flex mt-10"> |
|
<a-button |
|
class="flex items-center gap-2 !bg-primary text-white rounded nc-form-submit" |
|
size="large" |
|
@click="submitForm" |
|
> |
|
<!-- Submit --> |
|
{{ $t('general.submit') }} |
|
</a-button> |
|
</div> |
|
</a-card> |
|
</a-form> |
|
|
|
<div v-if="isEditable" class="mx-10 px-10"> |
|
<!-- After form is submitted --> |
|
<div class="text-gray-500 mt-4 mb-2"> |
|
{{ $t('msg.info.afterFormSubmitted') }} |
|
</div> |
|
|
|
<!-- Show this message --> |
|
<label class="text-gray-600 text-bold"> {{ $t('msg.info.showMessage') }}: </label> |
|
<a-textarea |
|
v-model:value="formViewData.success_msg" |
|
:rows="3" |
|
hide-details |
|
class="nc-form-after-submit-msg" |
|
@change="updateView" |
|
/> |
|
|
|
<!-- Other options --> |
|
<div class="mt-4"> |
|
<div class="my-4"> |
|
<!-- Show "Submit Another Form" button --> |
|
<a-switch |
|
v-model:checked="formViewData.submit_another_form" |
|
v-t="[`a:form-view:submit-another-form`]" |
|
size="small" |
|
class="nc-form-checkbox-submit-another-form" |
|
@change="updateView" |
|
/> |
|
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span> |
|
</div> |
|
|
|
<div class="my-4"> |
|
<!-- Show a blank form after 5 seconds --> |
|
<a-switch |
|
v-model:checked="formViewData.show_blank_form" |
|
v-t="[`a:form-view:show-blank-form`]" |
|
size="small" |
|
class="nc-form-checkbox-show-blank-form" |
|
@change="updateView" |
|
/> |
|
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span> |
|
</div> |
|
|
|
<div class="my-4"> |
|
<a-switch |
|
v-model:checked="emailMe" |
|
v-t="[`a:form-view:email-me`]" |
|
size="small" |
|
class="nc-form-checkbox-send-email" |
|
@change="onEmailChange" |
|
/> |
|
<!-- Email me at <email> --> |
|
<span class="ml-4"> |
|
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ state.user.value?.email }}</span> |
|
</span> |
|
</div> |
|
</div> |
|
</div> |
|
</a-card> |
|
</a-col> |
|
</a-row> |
|
</template> |
|
|
|
<style scoped lang="scss"> |
|
.nc-editable:hover { |
|
.nc-field-remove-icon { |
|
@apply opacity-100; |
|
} |
|
} |
|
|
|
.nc-input { |
|
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary; |
|
} |
|
|
|
.form-meta-input::placeholder { |
|
@apply text-[#3d3d3d] italic; |
|
} |
|
</style>
|
|
|