mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
80 changed files with 1568 additions and 659 deletions
@ -1,5 +1,11 @@
|
||||
<script setup lang="ts"> |
||||
const route = useRoute() |
||||
|
||||
const disableBaseLayout = $computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form')) |
||||
</script> |
||||
|
||||
<template> |
||||
<NuxtLayout name="base"> |
||||
<NuxtLayout :name="disableBaseLayout ? false : 'base'"> |
||||
<NuxtPage /> |
||||
</NuxtLayout> |
||||
</template> |
||||
|
@ -1,69 +1,76 @@
|
||||
<script setup lang="ts"> |
||||
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports' |
||||
import { EditModeInj } from '~/context' |
||||
import { EditModeInj, inject } from '#imports' |
||||
|
||||
interface Props { |
||||
modelValue: number | string | null | undefined |
||||
} |
||||
|
||||
const { modelValue } = defineProps<Props>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
const props = defineProps<Props>() |
||||
const emits = defineEmits(['update:modelValue']) |
||||
|
||||
const editEnabled = inject(EditModeInj) |
||||
|
||||
const column = inject(ColumnInj) |
||||
|
||||
const percent = ref() |
||||
|
||||
const isEdited = ref(false) |
||||
|
||||
const percentType = computed(() => column?.value?.meta?.precision || 0) |
||||
|
||||
const percentStep = computed(() => getPercentStep(percentType.value)) |
||||
|
||||
const localState = computed({ |
||||
get: () => { |
||||
return renderPercent(modelValue, percentType.value, !isEdited.value) |
||||
}, |
||||
set: (val) => { |
||||
if (val === null) val = 0 |
||||
if (isValidPercent(val, column?.value?.meta?.negative)) { |
||||
percent.value = val / 100 |
||||
} |
||||
}, |
||||
}) |
||||
|
||||
function onKeyDown(evt: KeyboardEvent) { |
||||
isEdited.value = true |
||||
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault() |
||||
} |
||||
|
||||
function onBlur() { |
||||
if (isEdited.value) { |
||||
emit('update:modelValue', percent.value) |
||||
isEdited.value = false |
||||
} |
||||
} |
||||
|
||||
function onKeyDownEnter() { |
||||
if (isEdited.value) { |
||||
emit('update:modelValue', percent.value) |
||||
isEdited.value = false |
||||
} |
||||
} |
||||
const vModel = useVModel(props, 'modelValue', emits) |
||||
</script> |
||||
|
||||
<template> |
||||
<input |
||||
v-if="isEdited" |
||||
v-model="localState" |
||||
type="number" |
||||
:step="percentStep" |
||||
@keydown="onKeyDown" |
||||
@blur="onBlur" |
||||
@keydown.enter="onKeyDownEnter" |
||||
/> |
||||
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" /> |
||||
<span v-else>{{ localState }}</span> |
||||
<input v-if="editEnabled" v-model="vModel" type="number" /> |
||||
<span v-else>{{ vModel }}</span> |
||||
</template> |
||||
|
||||
<!-- <script setup lang="ts"> |
||||
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports' |
||||
import { EditModeInj } from '~/context' |
||||
interface Props { |
||||
modelValue: number | string | null | undefined |
||||
} |
||||
const { modelValue } = defineProps<Props>() |
||||
const emit = defineEmits(['update:modelValue']) |
||||
const editEnabled = inject(EditModeInj) |
||||
const column = inject(ColumnInj) |
||||
const percent = ref() |
||||
const isEdited = ref(false) |
||||
const percentType = computed(() => column?.value?.meta?.precision || 0) |
||||
const percentStep = computed(() => getPercentStep(percentType.value)) |
||||
const localState = computed({ |
||||
get: () => { |
||||
return renderPercent(modelValue, percentType.value, !isEdited.value) |
||||
}, |
||||
set: (val) => { |
||||
if (val === null) val = 0 |
||||
if (isValidPercent(val, column?.value?.meta?.negative)) { |
||||
percent.value = val / 100 |
||||
} |
||||
}, |
||||
}) |
||||
function onKeyDown(evt: KeyboardEvent) { |
||||
isEdited.value = true |
||||
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault() |
||||
} |
||||
function onBlur() { |
||||
if (isEdited.value) { |
||||
emit('update:modelValue', percent.value) |
||||
isEdited.value = false |
||||
} |
||||
} |
||||
function onKeyDownEnter() { |
||||
if (isEdited.value) { |
||||
emit('update:modelValue', percent.value) |
||||
isEdited.value = false |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<input |
||||
v-if="isEdited" |
||||
v-model="localState" |
||||
type="number" |
||||
:step="percentStep" |
||||
@keydown="onKeyDown" |
||||
@blur="onBlur" |
||||
@keydown.enter="onKeyDownEnter" |
||||
/> |
||||
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" /> |
||||
<span v-else>{{ localState }}</span> |
||||
</template> --> |
||||
|
@ -1,18 +1,20 @@
|
||||
<script setup lang="ts"> |
||||
import { inject, ref } from '#imports' |
||||
import { RightSidebarInj } from '~/context' |
||||
|
||||
const emits = defineEmits(['addRow']) |
||||
|
||||
const sidebarOpen = inject(RightSidebarInj, ref(true)) |
||||
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' }) |
||||
const isLocked = inject(IsLockedInj) |
||||
</script> |
||||
|
||||
<template> |
||||
<a-tooltip :placement="sidebarOpen ? 'bottomRight' : 'left'"> |
||||
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'"> |
||||
<template #title> {{ $t('activity.addRow') }} </template> |
||||
|
||||
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group"> |
||||
<MdiPlusOutline class="cursor-pointer group-hover:(!text-white)" @click="emits('addRow')" /> |
||||
<div |
||||
:class="{ 'hover:after:bg-primary/75 group': !isLocked, 'disabled-ring': isLocked }" |
||||
class="nc-sidebar-right-item nc-sidebar-add-row" |
||||
> |
||||
<MdiPlusOutline |
||||
:class="{ 'cursor-pointer group-hover:(!text-white)': !isLocked, 'disabled': isLocked }" |
||||
@click="!isLocked ? emits('addRow') : {}" |
||||
/> |
||||
</div> |
||||
</a-tooltip> |
||||
</template> |
||||
|
@ -0,0 +1,197 @@
|
||||
import useVuelidate from '@vuelidate/core' |
||||
import { minLength, required } from '@vuelidate/validators' |
||||
import { message } from 'ant-design-vue' |
||||
import type { ColumnType, FormType, LinkToAnotherRecordType, TableType, ViewType } from 'nocodb-sdk' |
||||
import { ErrorMessages, RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { SharedViewPasswordInj } from '~/context' |
||||
import { extractSdkResponseErrorMsg } from '~/utils' |
||||
import { useInjectionState, useMetas } from '#imports' |
||||
|
||||
const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((sharedViewId: string) => { |
||||
const progress = ref(false) |
||||
const notFound = ref(false) |
||||
const submitted = ref(false) |
||||
const passwordDlg = ref(false) |
||||
const password = ref<string | null>(null) |
||||
const secondsRemain = ref(0) |
||||
|
||||
provide(SharedViewPasswordInj, password) |
||||
|
||||
const sharedView = ref<ViewType>() |
||||
const sharedFormView = ref<FormType>() |
||||
const meta = ref<TableType>() |
||||
const columns = ref<(ColumnType & { required?: boolean; show?: boolean })[]>() |
||||
|
||||
const { $api } = useNuxtApp() |
||||
const { metas, setMeta } = useMetas() |
||||
const formState = ref({}) |
||||
|
||||
const { state: additionalState } = useProvideSmartsheetRowStore( |
||||
meta as Ref<TableType>, |
||||
ref({ |
||||
row: formState, |
||||
rowMeta: { new: true }, |
||||
oldRow: {}, |
||||
}), |
||||
) |
||||
|
||||
const formColumns = computed(() => |
||||
columns?.value?.filter((c) => c.show)?.filter((col) => !isVirtualCol(col) || col.uidt === UITypes.LinkToAnotherRecord), |
||||
) |
||||
const loadSharedView = async () => { |
||||
try { |
||||
const viewMeta = await $api.public.sharedViewMetaGet(sharedViewId, { |
||||
headers: { |
||||
'xc-password': password.value, |
||||
}, |
||||
}) |
||||
|
||||
passwordDlg.value = false |
||||
|
||||
sharedView.value = viewMeta |
||||
sharedFormView.value = viewMeta.view |
||||
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) { |
||||
passwordDlg.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, |
||||
} |
||||
} |
||||
} |
||||
|
||||
return obj |
||||
}) |
||||
|
||||
const v$ = useVuelidate( |
||||
validators, |
||||
computed(() => ({ localState: formState?.value, virtual: additionalState?.value })), |
||||
) |
||||
|
||||
const submitForm = async () => { |
||||
try { |
||||
if (!(await v$.value?.$validate())) { |
||||
return |
||||
} |
||||
|
||||
progress.value = true |
||||
const data: Record<string, any> = { ...(formState?.value ?? {}), ...(additionalState?.value || {}) } |
||||
const attachment: Record<string, any> = {} |
||||
|
||||
for (const col of metas?.value?.[sharedView?.value?.fk_model_id as string]?.columns ?? []) { |
||||
if (col.uidt === UITypes.Attachment) { |
||||
attachment[`_${col.title}`] = data[col.title!] |
||||
delete data[col.title!] |
||||
} |
||||
} |
||||
|
||||
await $api.public.dataCreate( |
||||
sharedView?.value?.uuid as string, |
||||
{ |
||||
data, |
||||
...attachment, |
||||
}, |
||||
{ |
||||
headers: { |
||||
'xc-password': password.value, |
||||
}, |
||||
}, |
||||
) |
||||
|
||||
submitted.value = true |
||||
progress.value = false |
||||
|
||||
await message.success(sharedFormView.value?.success_msg || 'Saved successfully.') |
||||
} catch (e: any) { |
||||
console.log(e) |
||||
await message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
progress.value = false |
||||
} |
||||
|
||||
/** reset form if show_blank_form is true */ |
||||
watch(submitted, (nextVal: boolean) => { |
||||
if (nextVal && sharedFormView.value?.show_blank_form) { |
||||
secondsRemain.value = 5 |
||||
const intvl = setInterval(() => { |
||||
secondsRemain.value = secondsRemain.value - 1 |
||||
if (secondsRemain.value < 0) { |
||||
submitted.value = false |
||||
clearInterval(intvl) |
||||
} |
||||
}, 1000) |
||||
} |
||||
|
||||
/** reset form state and validation */ |
||||
if (!nextVal) { |
||||
additionalState.value = {} |
||||
formState.value = {} |
||||
v$.value?.$reset() |
||||
} |
||||
}) |
||||
|
||||
return { |
||||
sharedView, |
||||
sharedFormView, |
||||
loadSharedView, |
||||
columns, |
||||
submitForm, |
||||
progress, |
||||
meta, |
||||
validators, |
||||
v$, |
||||
formColumns, |
||||
formState, |
||||
notFound, |
||||
password, |
||||
submitted, |
||||
secondsRemain, |
||||
passwordDlg, |
||||
} |
||||
}, '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 |
||||
} |
Loading…
Reference in new issue