Browse Source

feat(gui-v2): add shared form view

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/3188/head
Pranav C 2 years ago
parent
commit
2446b04de1
  1. 170
      packages/nc-gui-v2/components/shared-view/Form.vue
  2. 1
      packages/nc-gui-v2/composables/index.ts
  3. 160
      packages/nc-gui-v2/composables/useSharedFormViewStore.ts
  4. 100
      packages/nc-gui-v2/package-lock.json
  5. 2
      packages/nc-gui-v2/package.json
  6. 44
      packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue

170
packages/nc-gui-v2/components/shared-view/Form.vue

@ -1,12 +1,9 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { FieldsInj, MetaInj } from '#imports'
import { ref, useProvideSmartsheetRowStore } from '#imports'
import { useSharedFormStoreOrThrow } from '#imports'
const fields = inject(FieldsInj, ref([]))
const meta = inject(MetaInj)
const { sharedView } = useSharedView()
const formState = ref(fields.value.reduce((a, v) => ({ ...a, [v.title]: undefined }), {}))
const { sharedView, submitForm, v$, formState, notFound, formColumns, meta } = useSharedFormStoreOrThrow()
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
@ -15,67 +12,124 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = fields.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string,
any
>
columnObj = formColumns.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)
}
useSmartsheetStoreOrThrow()
useProvideSmartsheetRowStore(meta, formState)
const formRef = ref()
const { state: additionalState } = useProvideSmartsheetRowStore(
meta,
ref({
row: formState,
rowMeta: { new: true },
oldRow: {},
}),
)
</script>
<template>
<div class="flex flex-col my-4 space-y-2 mx-32 items-center">
<div class="flex w-2/3 flex-col mt-10">
<div class="flex flex-col items-start px-14 py-8 bg-gray-50 rounded-md w-full">
<a-typography-title class="border-b-1 border-gray-100 w-full pb-3 nc-share-form-title" :level="1">
{{ sharedView.view.heading }}
</a-typography-title>
<a-typography class="pl-1 text-sm nc-share-form-desc">{{ sharedView.view.subheading }}</a-typography>
</div>
<div class="bg-primary/100 !h-[100vh] overflow-auto w-100 flex flex-col">
<div>
<img src="~/assets/img/icons/512x512-trans.png" width="30" class="mx-4 mt-2" />
</div>
<div class="m-4 mt-2 bg-white rounded p-2 flex-1">
<a-alert v-if="notFound" type="warning" class="mx-auto mt-10 max-w-[300px]" message="Not found"> </a-alert>
<a-form ref="formRef" :model="formState" class="mt-8 pb-12 mb-8 px-3 bg-gray-50 rounded-md">
<div v-for="(field, index) in fields" :key="index" class="flex flex-col mt-4 px-10 pt-6 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<template v-if="submitted">
<div class="lex justify-center">
<div v-if="sharedView" style="min-width: 350px">
<a-alert type="success" outlined>
<span class="title">{{ sharedView.success_msg || 'Successfully submitted form data' }}</span>
</a-alert>
<p v-if="sharedView.show_blank_form" class="text-xs text-gray-500 text-center">
New form will be loaded after {{ secondsRemain }} seconds
</p>
<div v-if="sharedView.submit_another_form" class="text-center">
<v-btn color="primary" @click="submitted = false"> Submit Another Form</v-btn>
</div>
</div>
<a-form-item
v-if="isVirtualCol(field)"
class="ma-0 gap-0 pa-0"
:class="`nc-form-field-${field.title}`"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[field.title]" class="nc-input" :column="field" />
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:class="`nc-form-field-${field.title}`"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetCell v-model="formState[field.title]" class="nc-input" :column="field" :edit-enabled="true" />
</a-form-item>
</div>
</a-form>
</template>
<div v-if="sharedView" class="">
<a-row class="justify-center">
<a-col :md="20">
<div>
<div class="h-full ma-0 rounded-b-0">
<div
class="nc-form-wrapper pb-10 rounded shadow-xl"
style="background: linear-gradient(180deg, #dbdbdb 0, #dbdbdb 200px, white 200px)"
>
<div class="mt-10 flex items-center justify-center flex-col">
<div class="nc-form-banner backgroundColor darken-1 flex-column justify-center d-flex">
<div class="flex items-center justify-center grow h-[100px]">
<img src="~/assets/img/icon.png" width="50" class="mx-4" />
<span class="text-4xl font-weight-bold">NocoDB</span>
</div>
</div>
</div>
<div class="mx-auto nc-form bg-white shadow-lg pa-2 mb-10 max-w-[600px] mx-auto rounded">
<h2 class="mt-4 text-4xl font-weight-bold text-left mx-4 mb-3 px-1">
{{ sharedView.view.heading }}
</h2>
<div class="text-lg text-left mx-4 py-2 px-1 text-gray-500">
{{ sharedView.view.subheading }}
</div>
<div class="h-100">
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col mt-4 px-4 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div>
<div v-if="isVirtualCol(field)" class="mt-0">
<SmartsheetVirtualCell v-model="formState[field.title]" class="mt-0 nc-input" :column="field" />
<div
v-if="
v$.virtual?.$dirty && (!v$.virtual?.[col.title]?.required || !v$.virtual?.[col.title]?.minLength)
"
class="text-xs text-red-500"
>
Field is required.
</div>
</div>
<div v-else class="mt-0">
<SmartsheetCell
v-model="formState[field.title]"
class="nc-input"
:column="field"
:edit-enabled="true"
/>
<template v-if="v$.localState.$dirty && v$.localState?.[field.title]">
<div v-for="error of v$.localState[field.title].$errors" :key="error" class="text-xs text-red-500">
{{ error.$message }}
</div>
</template>
</div>
</div>
<div class="text-center my-9">
<a-button type="primary" size="large" @click="submitForm(formState, additionalState)"> Submit</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</div>
</template>
@ -84,4 +138,8 @@ const formRef = ref()
.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;
}
.nc-form-wrapper {
@apply my-0 mx-auto max-w-[800px];
}
</style>

1
packages/nc-gui-v2/composables/index.ts

@ -21,3 +21,4 @@ export * from './useColumnCreateStore'
export * from './useSmartsheetStore'
export * from './useLTARStore'
export * from './useExpandedFormStore'
export * from './useSharedFormViewStore'

160
packages/nc-gui-v2/composables/useSharedFormViewStore.ts

@ -0,0 +1,160 @@
import useVuelidate from '@vuelidate/core'
import { minLength, required } from '@vuelidate/validators'
import { message } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg } from '~/utils'
const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState(() => {
const progress = ref(false)
const notFound = ref(false)
const showPasswordModal = ref(false)
const submitted = ref(false)
const password = ref(null)
// todo: type
const sharedView = ref<any>()
const meta = ref<TableType>()
const columns = ref<(ColumnType & { required?: boolean })[]>()
const { $api } = useNuxtApp()
const { metas, setMeta } = useMetas()
const formState = ref({})
const formColumns = computed(
() =>
columns?.value
?.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<ColumnType & { required: boolean }>((c: ColumnType & { required?: boolean }) => ({
...c,
required: !!(c.required || 0),
})) ?? [],
)
const loadSharedView = async (viewId: string) => {
try {
// todo: swagget type correction
const viewMeta: any = await $api.public.sharedViewMetaGet(viewId)
sharedView.value = viewMeta
meta.value = viewMeta.model
columns.value = viewMeta.model.columns
setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
} catch (e: any) {
if (e.response && e.response.status === 404) {
notFound.value = true
} else if ((await extractSdkResponseErrorMsg(e)) === ErrorMessages.INVALID_SHARED_VIEW_PASSWORD) {
showPasswordModal.value = true
}
}
}
const validators = computed(() => {
const obj: any = {
localState: {},
virtual: {},
}
for (const column of formColumns?.value ?? []) {
if (
!isVirtualCol(column) &&
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || (column as any).required)
) {
obj.localState[column.title!] = { required }
} else if (
column.uidt === UITypes.LinkToAnotherRecord &&
column.colOptions &&
(column.colOptions as LinkToAnotherRecordType).type === RelationTypes.BELONGS_TO
) {
const col = columns.value?.find((c) => c.id === (column?.colOptions as LinkToAnotherRecordType)?.fk_child_column_id)
if ((col && col.rqd && !col.cdf) || column.required) {
if (col) {
obj.virtual[column.title!] = { required }
}
}
} else if (isVirtualCol(column) && column.required) {
obj.virtual[column.title!] = {
minLength: minLength(1),
required,
}
}
}
console.log(obj)
return obj
})
const v$ = useVuelidate(validators, { localState: formState, virtual: {} })
const submitForm = async (formState: Record<string, any>, additionalState: Record<string, any>) => {
try {
// if (this.$v.localState.$invalid || this.$v.virtual.$invalid) {
// this.$toast.error('Provide values of all required field').goAway(3000);
// return;
// }
if (!(await v$.value?.$validate())) {
return
}
progress.value = true
const data = { ...formState, ...additionalState }
const attachment: Record<string, any> = {}
for (const col of metas?.value?.[sharedView?.value?.fk_model_id]?.columns ?? []) {
if (col.uidt === UITypes.Attachment) {
attachment[`_${col.title}`] = data[col.title!]
delete data[col.title!]
}
}
await $api.public.dataCreate(
sharedView?.value?.uuid,
{
data,
...attachment,
},
{
// todo: add password support
// headers: { 'xc-password': password },
},
)
progress.value = false
await message.success(sharedView.value.success_msg || 'Saved successfully.')
} catch (e: any) {
console.log(e)
await message.error(await extractSdkResponseErrorMsg(e))
}
progress.value = false
}
return {
sharedView,
loadSharedView,
columns,
submitForm,
progress,
meta,
validators,
v$,
formColumns,
formState,
notFound,
password,
submitted
}
}, 'expanded-form-store')
export { useProvideSharedFormStore }
export function useSharedFormStoreOrThrow() {
const sharedFormStore = useSharedFormStore()
if (sharedFormStore == null) throw new Error('Please call `useProvideSharedFormStore` on the appropriate parent component')
return sharedFormStore
}

100
packages/nc-gui-v2/package-lock.json generated

@ -6,6 +6,8 @@
"": {
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",
@ -2862,6 +2864,72 @@
"vue": "^3.0.1"
}
},
"node_modules/@vuelidate/core": {
"version": "2.0.0-alpha.44",
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.44.tgz",
"integrity": "sha512-3DlCe3E0RRXbB+OfPacUetKhLmXzmnjeHkzjnbkc03p06mKm6h9pXR5pd6Mv4s4tus4sieuKDb2YWNmKK6rQeA==",
"dependencies": {
"vue-demi": "^0.13.4"
}
},
"node_modules/@vuelidate/core/node_modules/vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuelidate/validators": {
"version": "2.0.0-alpha.31",
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz",
"integrity": "sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ==",
"dependencies": {
"vue-demi": "^0.13.4"
}
},
"node_modules/@vuelidate/validators/node_modules/vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuetify/loader-shared": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-1.5.0.tgz",
@ -17366,6 +17434,38 @@
"dev": true,
"requires": {}
},
"@vuelidate/core": {
"version": "2.0.0-alpha.44",
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.0-alpha.44.tgz",
"integrity": "sha512-3DlCe3E0RRXbB+OfPacUetKhLmXzmnjeHkzjnbkc03p06mKm6h9pXR5pd6Mv4s4tus4sieuKDb2YWNmKK6rQeA==",
"requires": {
"vue-demi": "^0.13.4"
},
"dependencies": {
"vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"requires": {}
}
}
},
"@vuelidate/validators": {
"version": "2.0.0-alpha.31",
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.0-alpha.31.tgz",
"integrity": "sha512-+MFA9nZ7Y9zCpq383/voPDk/hiAmu6KqiJJhLOYB/FmrUPVoyKnuKnI9Bwiq8ok9GZlVkI8BnIrKPKGj9QpwiQ==",
"requires": {
"vue-demi": "^0.13.4"
},
"dependencies": {
"vue-demi": {
"version": "0.13.8",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.8.tgz",
"integrity": "sha512-Vy1zbZhCOdsmvGR6tJhAvO5vhP7eiS8xkbYQSoVa7o6KlIy3W8Rc53ED4qI4qpeRDjv3mLfXSEpYU6Yq4pgXRg==",
"requires": {}
}
}
},
"@vuetify/loader-shared": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-1.5.0.tgz",

2
packages/nc-gui-v2/package.json

@ -12,6 +12,8 @@
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vuelidate/core": "^2.0.0-alpha.44",
"@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",

44
packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue

@ -1,39 +1,45 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReloadViewDataHookInj, useRoute } from '#imports'
import type { TableType } from 'nocodb-sdk/build/main'
import { useProvideSharedFormStore } from '~/composables/useSharedFormViewStore'
import { ActiveViewInj, FieldsInj, IsFormInj, IsPublicInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { createEventHook, definePageMeta, provide, ref, useProvideSmartsheetStore, useRoute } from '#imports'
definePageMeta({
requiresAuth: false,
layout: 'false',
})
const route = useRoute()
const reloadEventHook = createEventHook<void>()
const { sharedView, loadSharedView, meta, formColumns } = useSharedView()
await loadSharedView(route.params.viewId as string)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, formColumns)
provide(IsPublicInj, ref(true))
const { loadSharedView, sharedView, meta, columns, notFound, formColumns } = useProvideSharedFormStore()
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
await loadSharedView(route.params.viewId as string)
if (!notFound.value) {
provide(ReloadViewDataHookInj, reloadEventHook)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, formColumns)
provide(IsPublicInj, ref(true))
provide(IsFormInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
}
</script>
<template>
<NuxtLayout id="content" class="flex">
<div class="nc-container flex flex-col h-full mt-2 px-6">
<SharedViewForm />
</div>
</NuxtLayout>
<!-- <NuxtLayout name="empty"> -->
<SharedViewForm />
<!-- </NuxtLayout> -->
</template>
<style scoped>
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
:global(.ant-layout-header) {
@apply !hidden;
}
:global(.nc-main-container) {
@apply !h-auto;
}
</style>

Loading…
Cancel
Save