mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
42 changed files with 590 additions and 408 deletions
@ -1,168 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
import { useSharedFormStoreOrThrow } from '#imports' |
||||
|
||||
const { |
||||
sharedFormView, |
||||
submitForm, |
||||
v$, |
||||
formState, |
||||
notFound, |
||||
formColumns, |
||||
submitted, |
||||
secondsRemain, |
||||
passwordDlg, |
||||
password, |
||||
loadSharedView, |
||||
} = useSharedFormStoreOrThrow() |
||||
|
||||
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 = 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) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="bg-primary !h-[100vh] overflow-auto w-full 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> |
||||
|
||||
<template v-else-if="submitted"> |
||||
<div class="flex justify-center"> |
||||
<div v-if="sharedFormView" style="min-width: 350px" class="mt-3"> |
||||
<a-alert type="success" outlined :message="sharedFormView.success_msg || 'Successfully submitted form data'"> |
||||
</a-alert> |
||||
<p v-if="sharedFormView.show_blank_form" class="text-xs text-gray-500 text-center my-4"> |
||||
New form will be loaded after {{ secondsRemain }} seconds |
||||
</p> |
||||
<div v-if="sharedFormView.submit_another_form" class="text-center"> |
||||
<a-button type="primary" @click="submitted = false"> Submit Another Form</a-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<div v-else-if="sharedFormView" class=""> |
||||
<a-row class="justify-center"> |
||||
<a-col :md="20"> |
||||
<div> |
||||
<div class="h-full m-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-col justify-center flex"> |
||||
<div class="flex items-center justify-center flex-1 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 p-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"> |
||||
{{ sharedFormView.heading }} |
||||
</h2> |
||||
|
||||
<div class="text-lg text-left mx-4 py-2 px-1 text-gray-500"> |
||||
{{ sharedFormView.subheading }} |
||||
</div> |
||||
<div class="h-full"> |
||||
<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 class="mt-0 nc-input" :column="field" /> |
||||
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div> |
||||
<template v-if="v$.virtual.$dirty && v$.virtual?.[field.title]"> |
||||
<div v-for="error of v$.virtual[field.title].$errors" :key="error" class="text-xs text-red-500"> |
||||
{{ error.$message }} |
||||
</div> |
||||
</template> |
||||
</div> |
||||
<div v-else class="mt-0"> |
||||
<SmartsheetCell |
||||
v-model="formState[field.title]" |
||||
class="nc-input" |
||||
:column="field" |
||||
:edit-enabled="true" |
||||
/> |
||||
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div> |
||||
<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> |
||||
|
||||
<a-modal |
||||
v-model:visible="passwordDlg" |
||||
:closable="false" |
||||
width="28rem" |
||||
centered |
||||
:footer="null" |
||||
:mask-closable="false" |
||||
@close="passwordDlg = false" |
||||
> |
||||
<div class="w-full flex flex-col"> |
||||
<a-typography-title :level="4">This shared view is protected</a-typography-title> |
||||
<a-form ref="formRef" :model="{ password }" class="mt-2" @finish="loadSharedView"> |
||||
<a-form-item name="password" :rules="[{ required: true, message: 'Password is required' }]"> |
||||
<a-input-password v-model:value="password" placeholder="Enter password" /> |
||||
</a-form-item> |
||||
<a-button type="primary" html-type="submit">Unlock</a-button> |
||||
</a-form> |
||||
</div> |
||||
</a-modal> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.nc-input { |
||||
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex items-center border-solid border-1 border-primary; |
||||
} |
||||
|
||||
.nc-form-wrapper { |
||||
@apply my-0 mx-auto max-w-[800px]; |
||||
} |
||||
</style> |
@ -0,0 +1,17 @@
|
||||
<template> |
||||
<a-dropdown :trigger="['click']"> |
||||
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> |
||||
<div class="flex gap-2 items-center"> |
||||
<MdiDownload class="group-hover:text-accent text-gray-500" /> |
||||
<span class="text-capitalize !text-sm font-weight-normal">Download</span> |
||||
<MdiMenuDown class="text-grey" /> |
||||
</div> |
||||
</a-button> |
||||
|
||||
<template #overlay> |
||||
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded"> |
||||
<SmartsheetToolbarExportSubActions /> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
</template> |
@ -0,0 +1,80 @@
|
||||
<script setup lang="ts"> |
||||
import { ExportTypes } from 'nocodb-sdk' |
||||
import FileSaver from 'file-saver' |
||||
import * as XLSX from 'xlsx' |
||||
import { message } from 'ant-design-vue' |
||||
|
||||
const isPublicView = inject(IsPublicInj, ref(false)) |
||||
const fields = inject(FieldsInj, ref([])) |
||||
|
||||
const { project } = useProject() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
|
||||
const meta = inject(MetaInj) |
||||
|
||||
const selectedView = inject(ActiveViewInj) |
||||
|
||||
const exportFile = async (exportType: ExportTypes) => { |
||||
let offset = 0 |
||||
let c = 1 |
||||
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob' |
||||
|
||||
try { |
||||
while (!isNaN(offset) && offset > -1) { |
||||
let res |
||||
if (isPublicView.value) { |
||||
const { exportFile: sharedViewExportFile } = useSharedView() |
||||
res = await sharedViewExportFile(fields.value, offset, exportType, responseType) |
||||
} else { |
||||
res = await $api.dbViewRow.export( |
||||
'noco', |
||||
project?.value.title as string, |
||||
meta?.value.title as string, |
||||
selectedView?.value.title as string, |
||||
exportType, |
||||
{ |
||||
responseType, |
||||
query: { |
||||
offset, |
||||
}, |
||||
} as any, |
||||
) |
||||
} |
||||
const { data, headers } = res |
||||
if (exportType === ExportTypes.EXCEL) { |
||||
const workbook = XLSX.read(data, { type: 'base64' }) |
||||
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`) |
||||
} else if (exportType === ExportTypes.CSV) { |
||||
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) |
||||
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`) |
||||
} |
||||
offset = +headers['nc-export-offset'] |
||||
if (offset > -1) { |
||||
message.info('Downloading more files') |
||||
} else { |
||||
message.success('Successfully exported all table data') |
||||
} |
||||
} |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-menu-item> |
||||
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as CSV --> |
||||
{{ $t('activity.downloadCSV') }} |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item> |
||||
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as XLSX --> |
||||
{{ $t('activity.downloadExcel') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</template> |
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts"> |
||||
import { ActiveViewInj, viewIcons } from '#imports' |
||||
|
||||
const selectedView = inject(ActiveViewInj) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex gap-2 items-center ml-2 mr-2 pr-4 pb-1 py-0.5 border-r-1 border-gray-100"> |
||||
<component |
||||
:is="viewIcons[selectedView?.type].icon" |
||||
v-if="selectedView?.type" |
||||
class="nc-view-icon group-hover:hidden" |
||||
:style="{ color: viewIcons[selectedView?.type].color }" |
||||
/> |
||||
<span class="!text-sm font-medium max-w-36 overflow-ellipsis overflow-hidden whitespace-nowrap">{{ |
||||
selectedView?.title |
||||
}}</span> |
||||
</div> |
||||
</template> |
@ -0,0 +1,174 @@
|
||||
<script setup lang="ts"> |
||||
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
import { useSharedFormStoreOrThrow } from '#imports' |
||||
|
||||
const { |
||||
sharedFormView, |
||||
submitForm, |
||||
v$, |
||||
formState, |
||||
notFound, |
||||
formColumns, |
||||
submitted, |
||||
secondsRemain, |
||||
passwordDlg, |
||||
password, |
||||
loadSharedView, |
||||
isLoading, |
||||
} = useSharedFormStoreOrThrow() |
||||
|
||||
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 = formColumns.value?.find((c) => c.id === columnObj.colOptions.fk_child_column_id) as Record<string, any> |
||||
} |
||||
|
||||
return !!(required || (columnObj && columnObj.rqd && !columnObj.cdf)) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="nc-form-view md:bg-primary bg-opacity-5 h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin" |
||||
> |
||||
<div |
||||
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full lg:max-w-1/2 max-w-500px mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)" |
||||
> |
||||
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" /> |
||||
|
||||
<h1 class="prose-2xl font-bold self-center my-4">{{ sharedFormView.heading }}</h1> |
||||
|
||||
<h2 v-if="sharedFormView.subheading" class="prose-lg text-gray-500 self-center">{{ sharedFormView.subheading }}</h2> |
||||
|
||||
<a-alert v-if="notFound" type="warning" class="my-4 text-center" message="Not found" /> |
||||
|
||||
<template v-else-if="submitted"> |
||||
<div class="flex justify-center"> |
||||
<div v-if="sharedFormView" class="min-w-350px mt-3"> |
||||
<a-alert |
||||
type="success" |
||||
class="my-4 text-center" |
||||
outlined |
||||
:message="sharedFormView.success_msg || 'Successfully submitted form data'" |
||||
/> |
||||
|
||||
<p v-if="sharedFormView.show_blank_form" class="text-xs text-gray-500 text-center my-4"> |
||||
New form will be loaded after {{ secondsRemain }} seconds |
||||
</p> |
||||
|
||||
<div v-if="sharedFormView.submit_another_form" class="text-center"> |
||||
<a-button type="primary" @click="submitted = false"> Submit Another Form</a-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<template v-else-if="sharedFormView"> |
||||
<div class="nc-form-wrapper"> |
||||
<div class="nc-form h-full max-w-3/4 mx-auto"> |
||||
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col my-6 gap-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 class="mt-0 nc-input" :column="field" /> |
||||
|
||||
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div> |
||||
|
||||
<template v-if="v$.virtual.$dirty && v$.virtual?.[field.title]"> |
||||
<div v-for="error of v$.virtual[field.title].$errors" :key="error" class="text-xs text-red-500"> |
||||
{{ error.$message }} |
||||
</div> |
||||
</template> |
||||
</div> |
||||
|
||||
<div v-else class="mt-0"> |
||||
<SmartsheetCell v-model="formState[field.title]" class="nc-input" :column="field" :edit-enabled="true" /> |
||||
|
||||
<div v-if="field.description" class="text-gray-500 text-[10px] mb-2 ml-1">{{ field.description }}</div> |
||||
|
||||
<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"> |
||||
<button type="submit" class="submit" @click="submitForm"> |
||||
{{ $t('general.submit') }} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<a-modal |
||||
v-model:visible="passwordDlg" |
||||
:closable="false" |
||||
width="28rem" |
||||
centered |
||||
:footer="null" |
||||
:mask-closable="false" |
||||
@close="passwordDlg = false" |
||||
> |
||||
<div class="w-full flex flex-col"> |
||||
<a-typography-title :level="4">This shared view is protected</a-typography-title> |
||||
|
||||
<a-form ref="formRef" :model="{ password }" class="mt-2" @finish="loadSharedView"> |
||||
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"> |
||||
<a-input-password v-model:value="password" :placeholder="$t('msg.info.signUp.enterPassword')" /> |
||||
</a-form-item> |
||||
|
||||
<!-- todo: i18n --> |
||||
<a-button type="primary" html-type="submit">Unlock</a-button> |
||||
</a-form> |
||||
</div> |
||||
</a-modal> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss"> |
||||
.nc-form-view { |
||||
.nc-input { |
||||
@apply w-full rounded p-2 min-h-[40px] flex items-center border-solid border-1 border-primary; |
||||
} |
||||
|
||||
.submit { |
||||
@apply z-1 relative color-transition rounded p-3 text-white shadow-sm; |
||||
|
||||
&::after { |
||||
@apply rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary; |
||||
content: ''; |
||||
z-index: -1; |
||||
} |
||||
|
||||
&:hover::after { |
||||
@apply transform scale-110 ring ring-accent; |
||||
} |
||||
|
||||
&:active::after { |
||||
@apply ring ring-accent; |
||||
} |
||||
} |
||||
} |
||||
</style> |
Loading…
Reference in new issue