mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
82 changed files with 3075 additions and 760 deletions
@ -1,78 +1,196 @@
|
||||
<script lang="ts" setup> |
||||
import type { Select as AntSelect } from 'ant-design-vue' |
||||
import type { SelectOptionType } from 'nocodb-sdk' |
||||
import { computed, inject } from '#imports' |
||||
import { ColumnInj, EditModeInj } from '~/context' |
||||
import { ActiveCellInj, ColumnInj } from '~/context' |
||||
import MdiCloseCircle from '~icons/mdi/close-circle' |
||||
|
||||
interface Props { |
||||
modelValue: string | null |
||||
modelValue: string | string[] | undefined |
||||
} |
||||
|
||||
const { modelValue } = defineProps<Props>() |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const { isMysql } = useProject() |
||||
|
||||
const column = inject(ColumnInj) |
||||
const isForm = inject<boolean>('isForm', false) |
||||
const editEnabled = inject(EditModeInj, ref(false)) |
||||
// const isForm = inject<boolean>('isForm', false) |
||||
// const editEnabled = inject(EditModeInj, ref(false)) |
||||
const active = inject(ActiveCellInj, ref(false)) |
||||
|
||||
const options = computed(() => column?.value?.dtxp?.split(',').map((v) => v.replace(/\\'/g, "'").replace(/^'|'$/g, '')) || []) |
||||
const selectedIds = ref<string[]>([]) |
||||
const aselect = ref<typeof AntSelect>() |
||||
const isOpen = ref(false) |
||||
|
||||
const localState = computed({ |
||||
get() { |
||||
return modelValue?.match(/(?:[^',]|\\')+(?='?(?:,|$))/g)?.map((v: string) => v.replace(/\\'/g, "'")) |
||||
}, |
||||
set(val?: string[]) { |
||||
emit('update:modelValue', val?.filter((v) => options.value.includes(v)).join(',')) |
||||
const options = computed(() => { |
||||
if (column?.value.colOptions) { |
||||
const opts = column.value.colOptions |
||||
? column.value.colOptions.options.filter((el: SelectOptionType) => el.title !== '') || [] |
||||
: [] |
||||
for (const op of opts.filter((el: SelectOptionType) => el.order === null)) { |
||||
op.title = op.title.replace(/^'/, '').replace(/'$/, '') |
||||
} |
||||
return opts |
||||
} |
||||
return [] |
||||
}) |
||||
|
||||
const vModel = computed({ |
||||
get: () => selectedIds.value.map((el) => options.value.find((op: SelectOptionType) => op.id === el).title), |
||||
set: (val) => emit('update:modelValue', val.length === 0 ? null : val.join(',')), |
||||
}) |
||||
|
||||
const selectedTitles = computed(() => |
||||
modelValue |
||||
? typeof modelValue === 'string' |
||||
? isMysql |
||||
? modelValue.split(',').sort((a, b) => { |
||||
const opa = options.value.find((el: SelectOptionType) => el.title === a) |
||||
const opb = options.value.find((el: SelectOptionType) => el.title === b) |
||||
if (opa && opb) { |
||||
return opa.order - opb.order |
||||
} |
||||
return 0 |
||||
}) |
||||
: modelValue.split(',') |
||||
: modelValue |
||||
: [], |
||||
) |
||||
|
||||
const handleKeys = (e: KeyboardEvent) => { |
||||
switch (e.key) { |
||||
case 'Escape': |
||||
e.preventDefault() |
||||
isOpen.value = false |
||||
break |
||||
case 'Enter': |
||||
e.stopPropagation() |
||||
break |
||||
} |
||||
} |
||||
|
||||
const handleClose = (e: MouseEvent) => { |
||||
if (aselect.value && !aselect.value.$el.contains(e.target)) { |
||||
isOpen.value = false |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
selectedIds.value = selectedTitles.value.map((el) => { |
||||
return options.value.find((op: SelectOptionType) => op.title === el).id |
||||
}) |
||||
}) |
||||
|
||||
useEventListener(document, 'click', handleClose) |
||||
|
||||
watch( |
||||
() => modelValue, |
||||
(_n, _o) => { |
||||
selectedIds.value = selectedTitles.value.map((el) => { |
||||
return options.value.find((op: SelectOptionType) => op.title === el).id |
||||
}) |
||||
}, |
||||
) |
||||
|
||||
watch(isOpen, (n, _o) => { |
||||
if (n === false) { |
||||
aselect.value.blur() |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<!-- |
||||
<v-select |
||||
v-model="localState" |
||||
:items="options" |
||||
hide-details |
||||
:clearable="!column.rqd" |
||||
variation="outlined" |
||||
multiple |
||||
/> |
||||
--> |
||||
|
||||
<v-combobox |
||||
v-model="localState" |
||||
:items="options" |
||||
multiple |
||||
chips |
||||
flat |
||||
dense |
||||
solo |
||||
hide-details |
||||
deletable-chips |
||||
class="text-center mt-0" |
||||
<a-select |
||||
ref="aselect" |
||||
v-model:value="vModel" |
||||
mode="multiple" |
||||
class="w-full" |
||||
placeholder="Select an option" |
||||
:bordered="false" |
||||
show-arrow |
||||
:show-search="false" |
||||
:open="isOpen" |
||||
@keydown="handleKeys" |
||||
@click="isOpen = !isOpen" |
||||
> |
||||
<!-- <template #selection="data"> --> |
||||
<!-- <v-chip --> |
||||
<!-- :key="data.item" --> |
||||
<!-- small --> |
||||
<!-- class="ma-1 " --> |
||||
<!-- :color="colors[setValues.indexOf(data.item) % colors.length]" --> |
||||
<!-- @click:close="data.parent.selectItem(data.item)" --> |
||||
<!-- > --> |
||||
<!-- {{ data.item }} --> |
||||
<!-- </v-chip> --> |
||||
<!-- </template> --> |
||||
|
||||
<!-- <template #item="{item}"> --> |
||||
<!-- <v-chip small :color="colors[setValues.indexOf(item) % colors.length]"> --> |
||||
<!-- {{ item }} --> |
||||
<!-- </v-chip> --> |
||||
<!-- </template> --> |
||||
<!-- <template #append> --> |
||||
<!-- <v-icon small class="mt-2"> --> |
||||
<!-- mdi-menu-down --> |
||||
<!-- </v-icon> --> |
||||
<!-- </template> --> |
||||
</v-combobox> |
||||
<a-select-option v-for="op of options" :key="op.id" :value="op.title" @click.stop> |
||||
<a-tag class="rounded-tag" :color="op.color"> |
||||
<span class="text-slate-500">{{ op.title }}</span> |
||||
</a-tag> |
||||
</a-select-option> |
||||
<template #tagRender="{ value: val, onClose }"> |
||||
<a-tag |
||||
v-if="options.find((el: SelectOptionType) => el.title === val)" |
||||
class="rounded-tag" |
||||
:style="{ display: 'flex', alignItems: 'center' }" |
||||
:color="options.find((el: SelectOptionType) => el.title === val).color" |
||||
:closable="active && (vModel.length > 1 || !column?.rqd)" |
||||
:close-icon="h(MdiCloseCircle, { class: ['ms-close-icon'] })" |
||||
@close="onClose" |
||||
> |
||||
<span class="text-slate-500">{{ val }}</span> |
||||
</a-tag> |
||||
</template> |
||||
</a-select> |
||||
</template> |
||||
|
||||
<style scoped></style> |
||||
<style scoped> |
||||
.ms-close-icon { |
||||
color: rgba(0, 0, 0, 0.25); |
||||
cursor: pointer; |
||||
display: flex; |
||||
font-size: 12px; |
||||
font-style: normal; |
||||
height: 12px; |
||||
line-height: 1; |
||||
text-align: center; |
||||
text-transform: none; |
||||
transition: color 0.3s ease, opacity 0.15s ease; |
||||
width: 12px; |
||||
z-index: 1; |
||||
margin-right: -6px; |
||||
margin-left: 3px; |
||||
} |
||||
.ms-close-icon:before { |
||||
display: block; |
||||
} |
||||
.ms-close-icon:hover { |
||||
color: rgba(0, 0, 0, 0.45); |
||||
} |
||||
.rounded-tag { |
||||
padding: 0px 12px; |
||||
border-radius: 12px; |
||||
} |
||||
:deep(.ant-tag) { |
||||
@apply "rounded-tag"; |
||||
} |
||||
:deep(.ant-tag-close-icon) { |
||||
@apply "text-slate-500"; |
||||
} |
||||
</style> |
||||
<!-- |
||||
/** |
||||
* @copyright Copyright (c) 2021, Xgene Cloud Ltd |
||||
* |
||||
* @author Naveen MR <oof1lab@gmail.com> |
||||
* @author Pranav C Balan <pranavxc@gmail.com> |
||||
* |
||||
* @license GNU AGPL version 3 or any later version |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU Affero General Public License as |
||||
* published by the Free Software Foundation, either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU Affero General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU Affero General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
* |
||||
*/ |
||||
--> |
||||
|
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup> |
||||
import { Chrome } from '@ckpack/vue-color' |
||||
import { enumColor } from '@/utils' |
||||
|
||||
interface Props { |
||||
modelValue: string | any |
||||
colors?: string[] |
||||
rowSize?: number |
||||
advanced?: Boolean |
||||
pickButton?: Boolean |
||||
} |
||||
|
||||
const props = withDefaults(defineProps<Props>(), { |
||||
modelValue: () => enumColor.light[0], |
||||
colors: () => enumColor.light.concat(enumColor.dark), |
||||
rowSize: () => 10, |
||||
advanced: () => true, |
||||
pickButton: () => false, |
||||
}) |
||||
|
||||
const emit = defineEmits(['update:modelValue']) |
||||
|
||||
const vModel = computed({ |
||||
get: () => props.modelValue, |
||||
set: (val) => { |
||||
emit('update:modelValue', val.hex ? val.hex : val || null) |
||||
}, |
||||
}) |
||||
|
||||
const picked = ref(props.modelValue || enumColor.light[0]) |
||||
|
||||
const selectColor = (color: any) => { |
||||
picked.value = color.hex ? color.hex : color |
||||
vModel.value = color.hex ? color.hex : color |
||||
} |
||||
|
||||
const compare = (colorA: String, colorB: String) => { |
||||
if ((typeof colorA === 'string' || colorA instanceof String) && (typeof colorB === 'string' || colorB instanceof String)) { |
||||
return colorA.toLowerCase() === colorB.toLowerCase() |
||||
} |
||||
return false |
||||
} |
||||
|
||||
watch(picked, (n, _o) => { |
||||
if (!props.pickButton) { |
||||
vModel.value = n.hex ? n.hex : n |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="color-picker"> |
||||
<div v-for="colId in Math.ceil(props.colors.length / props.rowSize)" :key="colId" class="color-picker-row"> |
||||
<button |
||||
v-for="(color, i) in colors.slice((colId - 1) * rowSize, colId * rowSize)" |
||||
:key="`color-${colId}-${i}`" |
||||
class="color-selector" |
||||
:class="compare(picked, color) ? 'selected' : ''" |
||||
:style="{ 'background-color': `${color}` }" |
||||
@click="selectColor(color)" |
||||
> |
||||
{{ compare(picked, color) ? '✓' : '' }} |
||||
</button> |
||||
</div> |
||||
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false"> |
||||
<a-collapse accordion ghost expand-icon-position="right"> |
||||
<a-collapse-panel key="1" header="Advanced" class=""> |
||||
<a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)"> |
||||
Pick Color |
||||
</a-button> |
||||
<div class="flex justify-center py-4"> |
||||
<Chrome v-model="picked" class="!w-full !shadow-none" /> |
||||
</div> |
||||
</a-collapse-panel> |
||||
</a-collapse> |
||||
</a-card> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.color-picker { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
flex-direction: column; |
||||
background: white; |
||||
padding: 10px; |
||||
} |
||||
.color-picker-row { |
||||
display: flex; |
||||
flex-direction: row; |
||||
} |
||||
.color-selector { |
||||
position: relative; |
||||
height: 32px; |
||||
width: 32px; |
||||
margin: 10px 5px; |
||||
border-radius: 5px; |
||||
-webkit-text-stroke-width: 1px; |
||||
-webkit-text-stroke-color: white; |
||||
} |
||||
.color-selector:hover { |
||||
filter: brightness(90%); |
||||
-webkit-filter: brightness(90%); |
||||
} |
||||
.color-selector.selected { |
||||
filter: brightness(90%); |
||||
-webkit-filter: brightness(90%); |
||||
} |
||||
</style> |
@ -0,0 +1,112 @@
|
||||
<script setup lang="ts"> |
||||
import Draggable from 'vuedraggable' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
import { useColumnCreateStoreOrThrow } from '#imports' |
||||
import { enumColor } from '@/utils' |
||||
import MdiDragIcon from '~icons/mdi/drag-vertical' |
||||
import MdiArrowDownDropCircle from '~icons/mdi/arrow-down-drop-circle' |
||||
import MdiClose from '~icons/mdi/close' |
||||
import MdiPlusIcon from '~icons/mdi/plus' |
||||
|
||||
const { formState, setAdditionalValidations } = useColumnCreateStoreOrThrow() |
||||
|
||||
let options = $ref<any[]>([]) |
||||
const colorMenus = $ref<any>({}) |
||||
const colors = $ref(enumColor.light) |
||||
const inputs = ref() |
||||
|
||||
const validators = { |
||||
'colOptions.options': [ |
||||
{ |
||||
validator: (_: any, _opt: any) => { |
||||
return new Promise<void>((resolve, reject) => { |
||||
for (const opt of options) { |
||||
if (!opt.title.length) { |
||||
return reject(new Error("Select options can't be null")) |
||||
} |
||||
if (formState.value.uidt === UITypes.MultiSelect && opt.title.includes(',')) { |
||||
return reject(new Error("MultiSelect columns can't have commas(',')")) |
||||
} |
||||
if (options.filter((el) => el.title === opt.title).length !== 1) { |
||||
return reject(new Error("Select options can't have duplicates")) |
||||
} |
||||
} |
||||
resolve() |
||||
}) |
||||
}, |
||||
}, |
||||
], |
||||
} |
||||
|
||||
setAdditionalValidations({ |
||||
...validators, |
||||
}) |
||||
|
||||
const getNextColor = () => { |
||||
let tempColor = colors[0] |
||||
if (options.length && options[options.length - 1].color) { |
||||
const lastColor = colors.indexOf(options[options.length - 1].color) |
||||
tempColor = colors[(lastColor + 1) % colors.length] |
||||
} |
||||
return tempColor |
||||
} |
||||
|
||||
const addNewOption = () => { |
||||
const tempOption = { |
||||
title: '', |
||||
color: getNextColor(), |
||||
} |
||||
options.push(tempOption) |
||||
} |
||||
|
||||
const removeOption = (index: number) => { |
||||
options.splice(index, 1) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
if (!formState.value.colOptions?.options) { |
||||
formState.value.colOptions = { |
||||
options: [], |
||||
} |
||||
} |
||||
options = formState.value.colOptions.options |
||||
// Support for older options |
||||
for (const op of options.filter((el) => el.order === null)) { |
||||
op.title = op.title.replace(/^'/, '').replace(/'$/, '') |
||||
} |
||||
}) |
||||
|
||||
// focus last created input |
||||
watch(inputs, () => { |
||||
if (inputs.value?.$el) { |
||||
inputs.value.$el.focus() |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="w-full"> |
||||
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon"> |
||||
<template #item="{ element, index }"> |
||||
<div class="flex py-1 align-center"> |
||||
<MdiDragIcon small class="nc-child-draggable-icon handle" /> |
||||
<a-dropdown v-model:visible="colorMenus[index]" :trigger="['click']"> |
||||
<template #overlay> |
||||
<GeneralColorPicker v-model="element.color" :pick-button="true" @update:model-value="colorMenus[index] = false" /> |
||||
</template> |
||||
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" /> |
||||
</a-dropdown> |
||||
<a-input ref="inputs" v-model:value="element.title" class="caption" /> |
||||
<MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" /> |
||||
</div> |
||||
</template> |
||||
<template #footer> |
||||
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()"> |
||||
<div class="flex align-center"><MdiPlusIcon /><span class="flex-auto">Add option</span></div> |
||||
</a-button> |
||||
</template> |
||||
</Draggable> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup> |
||||
import type { Row } from '~/composables' |
||||
import { useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports' |
||||
|
||||
interface Props { |
||||
row: Row |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
const currentRow = toRef(props, 'row') |
||||
|
||||
const { meta } = useSmartsheetStoreOrThrow() |
||||
const { isNew, state, syncLTARRefs } = useProvideSmartsheetRowStore(meta, currentRow) |
||||
|
||||
// on changing isNew(new record insert) status sync LTAR cell values |
||||
watch(isNew, async (nextVal, prevVal) => { |
||||
if (prevVal && !nextVal) { |
||||
await syncLTARRefs(currentRow.value.row) |
||||
// update row values without invoking api |
||||
currentRow.value.row = { ...currentRow.value.row, ...state.value } |
||||
currentRow.value.oldRow = { ...currentRow.value.row, ...state.value } |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<slot :state="state" /> |
||||
</template> |
@ -0,0 +1,93 @@
|
||||
<script setup lang="ts"> |
||||
import { nextTick, useExpandedFormStoreOrThrow } from '#imports' |
||||
import { enumColor, timeAgo } from '~/utils' |
||||
import MdiAccountIcon from '~icons/mdi/account-circle' |
||||
|
||||
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment } = |
||||
useExpandedFormStoreOrThrow() |
||||
|
||||
const commentsWrapperEl = ref<HTMLDivElement>() |
||||
|
||||
await loadCommentsAndLogs() |
||||
|
||||
watch( |
||||
commentsAndLogs, |
||||
() => { |
||||
nextTick(() => { |
||||
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight |
||||
}) |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="h-full d-flex flex-column w-full"> |
||||
<div ref="commentsWrapperEl" class="flex-grow-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2"> |
||||
<v-skeleton-loader v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" /> |
||||
|
||||
<template v-else> |
||||
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs"> |
||||
<MdiAccountIcon class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" /> |
||||
<div class="flex-grow"> |
||||
<p class="mb-1 caption edited-text text-[10px] text-gray"> |
||||
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }} |
||||
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }} |
||||
</p> |
||||
<p |
||||
v-if="log.op_type === 'COMMENT'" |
||||
class="caption mb-0 nc-chip w-full min-h-20px" |
||||
:style="{ backgroundColor: enumColor.light[2] }" |
||||
> |
||||
{{ log.description }} |
||||
</p> |
||||
|
||||
<p v-else v-dompurify-html="log.details" class="caption mb-0" style="word-break: break-all" /> |
||||
|
||||
<p class="time text-right text-[10px] mb-0"> |
||||
{{ timeAgo(log.created_at) }} |
||||
</p> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</div> |
||||
<div class="border-1 my-2 w-full ml-6" /> |
||||
<div class="p-0"> |
||||
<div class="flex justify-center"> |
||||
<a-checkbox v-model:checked="commentsOnly" @change="loadCommentsAndLogs" |
||||
><span class="text-[11px] text-gray-500">Comments only</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div class="flex-shrink-1 mt-2 d-flex pl-4"> |
||||
<a-input |
||||
v-model:value="comment" |
||||
class="!text-xs" |
||||
ghost |
||||
:class="{ focus: showborder }" |
||||
@focusin="showborder = true" |
||||
@focusout="showborder = false" |
||||
@keyup.enter.prevent="saveComment" |
||||
> |
||||
<template #addonBefore> |
||||
<div class="flex align-center"> |
||||
<mdi-account-circle class="text-lg text-pink-300" small @click="saveComment" /> |
||||
</div> |
||||
</template> |
||||
<template #suffix> |
||||
<mdi-keyboard-return v-if="comment" class="text-sm" small @click="saveComment" /> |
||||
</template> |
||||
</a-input> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
:deep(.red.lighten-4) { |
||||
@apply bg-red-100; |
||||
} |
||||
|
||||
:deep(.green.lighten-4) { |
||||
@apply bg-green-100; |
||||
} |
||||
</style> |
@ -0,0 +1,60 @@
|
||||
<script lang="ts" setup> |
||||
import { |
||||
computed, |
||||
useExpandedFormStoreOrThrow, |
||||
useSmartsheetRowStoreOrThrow, |
||||
useSmartsheetStoreOrThrow, |
||||
useUIPermission, |
||||
} from '#imports' |
||||
import MdiDoorOpen from '~icons/mdi/door-open' |
||||
import MdiDoorClosed from '~icons/mdi/door-closed' |
||||
|
||||
const emit = defineEmits(['cancel']) |
||||
const { meta } = useSmartsheetStoreOrThrow() |
||||
const { commentsDrawer, primaryValue, save: _save } = useExpandedFormStoreOrThrow() |
||||
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow() |
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const save = async () => { |
||||
if (isNew.value) { |
||||
const data = await _save() |
||||
await syncLTARRefs(data) |
||||
} else { |
||||
await _save() |
||||
} |
||||
} |
||||
|
||||
const drawerToggleIcon = computed(() => (commentsDrawer.value ? MdiDoorOpen : MdiDoorClosed)) |
||||
|
||||
// todo: accept as a prop / inject |
||||
const iconColor = '#1890ff' |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex p-2 align-center gap-2"> |
||||
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0"> |
||||
<mdi-table-arrow-right :style="{ color: iconColor }" /> |
||||
|
||||
<template v-if="meta"> |
||||
{{ meta.title }} |
||||
</template> |
||||
<template v-else> |
||||
{{ table }} |
||||
</template> |
||||
<template v-if="primaryValue">: {{ primaryValue }}</template> |
||||
</h5> |
||||
<div class="flex-grow" /> |
||||
<mdi-reload class="cursor-pointer select-none" /> |
||||
<component :is="drawerToggleIcon" class="cursor-pointer select-none" @click="commentsDrawer = !commentsDrawer" /> |
||||
<a-button size="small" class="!text" @click="emit('cancel')"> |
||||
<!-- Cancel --> |
||||
{{ $t('general.cancel') }} |
||||
</a-button> |
||||
<a-button size="small" :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save"> |
||||
<!-- Save Row --> |
||||
{{ $t('activity.saveRow') }} |
||||
</a-button> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,139 @@
|
||||
<script setup lang="ts"> |
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import { isVirtualCol } from 'nocodb-sdk' |
||||
import Comments from './Comments.vue' |
||||
import Header from './Header.vue' |
||||
import { |
||||
computedInject, |
||||
provide, |
||||
toRef, |
||||
useNuxtApp, |
||||
useProvideExpandedFormStore, |
||||
useProvideSmartsheetStore, |
||||
useVModel, |
||||
watch, |
||||
} from '#imports' |
||||
import { NOCO } from '~/lib' |
||||
import { extractPkFromRow } from '~/utils' |
||||
import type { Row } from '~/composables' |
||||
import { FieldsInj, IsFormInj, MetaInj } from '~/context' |
||||
|
||||
interface Props { |
||||
modelValue: string | null |
||||
row: Row |
||||
state?: Record<string, any> | null |
||||
meta: TableType |
||||
loadRow?: boolean |
||||
useMetaFields?: boolean |
||||
} |
||||
|
||||
const props = defineProps<Props>() |
||||
const emits = defineEmits(['update:modelValue']) |
||||
const row = toRef(props, 'row') |
||||
const state = toRef(props, 'state') |
||||
const meta = toRef(props, 'meta') |
||||
|
||||
const _fields = computedInject(FieldsInj, (_fields) => { |
||||
if (props.useMetaFields) { |
||||
return meta.value.columns ?? [] |
||||
} |
||||
return _fields?.value ?? [] |
||||
}) |
||||
|
||||
provide(MetaInj, meta) |
||||
|
||||
const { commentsDrawer, changedColumns, state: rowState } = useProvideExpandedFormStore(meta, row) |
||||
|
||||
const { $api } = useNuxtApp() |
||||
if (props.loadRow) { |
||||
const { project } = useProject() |
||||
row.value.row = await $api.dbTableRow.read( |
||||
NOCO, |
||||
project.value.id as string, |
||||
meta.value.title, |
||||
extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]), |
||||
) |
||||
row.value.oldRow = { ...row.value.row } |
||||
row.value.rowMeta = {} |
||||
} |
||||
|
||||
useProvideSmartsheetStore(ref({}) as any, meta) |
||||
|
||||
provide(IsFormInj, true) |
||||
|
||||
// accept as a prop |
||||
// const row: Row = { row: {}, rowMeta: {}, oldRow: {} } |
||||
|
||||
watch( |
||||
state, |
||||
() => { |
||||
if (state.value) { |
||||
rowState.value = state.value |
||||
} else { |
||||
rowState.value = {} |
||||
} |
||||
}, |
||||
{ immediate: true }, |
||||
) |
||||
|
||||
const isExpanded = useVModel(props, 'modelValue', emits) |
||||
</script> |
||||
|
||||
<template> |
||||
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false"> |
||||
<Header @cancel="isExpanded = false" /> |
||||
<a-card class="!bg-gray-100"> |
||||
<div class="flex h-full nc-form-wrapper items-stretch"> |
||||
<div class="flex-grow overflow-auto scrollbar-thin-primary"> |
||||
<div class="w-[500px] mx-auto"> |
||||
<div v-for="col in fields" :key="col.title" class="mt-2"> |
||||
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" /> |
||||
<SmartsheetHeaderCell v-else :column="col" /> |
||||
|
||||
<div class="!bg-white rounded px-1 min-h-[35px] flex align-center"> |
||||
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" /> |
||||
<SmartsheetCell |
||||
v-else |
||||
v-model="row.row[col.title]" |
||||
:column="col" |
||||
:edit-enabled="true" |
||||
@update:model-value="changedColumns.add(col.title)" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="nc-comments-drawer min-w-0 min-h-full max-h-full" :class="{ active: commentsDrawer }"> |
||||
<div class="h-full"> |
||||
<Comments v-if="commentsDrawer" /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</a-card> |
||||
</a-modal> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
:deep(input, select, textarea) { |
||||
@apply !bg-white; |
||||
} |
||||
|
||||
:deep(.ant-modal-body) { |
||||
@apply !bg-gray-100; |
||||
} |
||||
|
||||
.nc-comments-drawer { |
||||
@apply w-0 transition-width ease-in-out duration-200; |
||||
overflow: hidden; |
||||
|
||||
&.active { |
||||
@apply w-[250px] border-left-1; |
||||
} |
||||
} |
||||
|
||||
.nc-form-wrapper { |
||||
max-height: max(calc(90vh - 100px), 600px); |
||||
height: max-content !important; |
||||
} |
||||
</style> |
@ -0,0 +1,182 @@
|
||||
<script setup lang="ts"> |
||||
import HTTPSnippet from 'httpsnippet' |
||||
import { useClipboard } from '@vueuse/core' |
||||
import { notification } from 'ant-design-vue' |
||||
import { ActiveViewInj, MetaInj } from '~/context' |
||||
|
||||
const props = defineProps<Props>() |
||||
|
||||
const emits = defineEmits(['update:modelValue']) |
||||
|
||||
interface Props { |
||||
modelValue: boolean |
||||
} |
||||
|
||||
const { project } = $(useProject()) |
||||
const { appInfo, token } = $(useGlobal()) |
||||
const meta = $(inject(MetaInj)) |
||||
const view = $(inject(ActiveViewInj)) |
||||
const { xWhere } = useSmartsheetStoreOrThrow() |
||||
const { queryParams } = $(useViewData(meta, view as any, xWhere)) |
||||
const { copy } = useClipboard() |
||||
|
||||
let vModel = $(useVModel(props, 'modelValue', emits)) |
||||
|
||||
const langs = [ |
||||
{ |
||||
name: 'shell', |
||||
clients: ['curl', 'wget'], |
||||
}, |
||||
{ |
||||
name: 'javascript', |
||||
clients: ['axios', 'fetch', 'jquery', 'xhr'], |
||||
}, |
||||
{ |
||||
name: 'node', |
||||
clients: ['axios', 'fetch', 'request', 'native', 'unirest'], |
||||
}, |
||||
{ |
||||
name: 'nocodb-sdk', |
||||
clients: ['javascript', 'node'], |
||||
}, |
||||
{ |
||||
name: 'php', |
||||
}, |
||||
{ |
||||
name: 'python', |
||||
clients: ['python3', 'requests'], |
||||
}, |
||||
{ |
||||
name: 'ruby', |
||||
}, |
||||
{ |
||||
name: 'java', |
||||
}, |
||||
{ |
||||
name: 'c', |
||||
}, |
||||
] |
||||
|
||||
const selectedClient = $ref<string | undefined>(langs[0].clients && langs[0].clients[0]) |
||||
|
||||
const selectedLangName = $ref(langs[0].name) |
||||
|
||||
const apiUrl = $computed( |
||||
() => |
||||
new URL(`/api/v1/db/data/noco/${project.id}/${meta.title}/views/${view.title}`, (appInfo && appInfo.ncSiteUrl) || '/').href, |
||||
) |
||||
|
||||
const snippet = $computed( |
||||
() => |
||||
new HTTPSnippet({ |
||||
method: 'GET', |
||||
headers: [{ name: 'xc-auth', value: token as string, comment: 'JWT Auth token' }], |
||||
url: apiUrl, |
||||
queryString: Object.entries(queryParams || {}).map(([name, value]) => { |
||||
return { |
||||
name, |
||||
value: String(value), |
||||
} |
||||
}), |
||||
}), |
||||
) |
||||
|
||||
const activeLang = $computed(() => langs.find((lang) => lang.name === selectedLangName)) |
||||
|
||||
const code = $computed(() => { |
||||
if (activeLang?.name === 'nocodb-sdk') { |
||||
return `${selectedClient === 'node' ? 'const { Api } require("nocodb-sdk");' : 'import { Api } from "nocodb-sdk";'} |
||||
const api = new Api({ |
||||
baseURL: ${JSON.stringify(apiUrl)}, |
||||
headers: { |
||||
"xc-auth": ${JSON.stringify(token as string)} |
||||
} |
||||
}) |
||||
|
||||
api.dbViewRow.list( |
||||
"noco", |
||||
${JSON.stringify(project.title)}, |
||||
${JSON.stringify(meta.title)}, |
||||
${JSON.stringify(view.title)}, ${JSON.stringify(queryParams, null, 4)}).then(function (data) { |
||||
console.log(data); |
||||
}).catch(function (error) { |
||||
console.error(error); |
||||
}); |
||||
` |
||||
} |
||||
|
||||
return snippet.convert(activeLang?.name, selectedClient || (activeLang?.clients && activeLang?.clients[0]), {}) |
||||
}) |
||||
|
||||
const onCopyToClipboard = () => { |
||||
copy(code) |
||||
notification.info({ message: 'Copied to clipboard' }) |
||||
} |
||||
|
||||
const afterVisibleChange = (visible: boolean) => { |
||||
vModel = visible |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-drawer |
||||
v-model:visible="vModel" |
||||
class="h-full relative" |
||||
style="color: red" |
||||
placement="right" |
||||
size="large" |
||||
:closable="false" |
||||
@after-visible-change="afterVisibleChange" |
||||
> |
||||
<div class="flex flex-col w-full h-full"> |
||||
<a-typography-title :level="4">Code Snippet</a-typography-title> |
||||
<a-tabs v-model:activeKey="selectedLangName" class="!h-full"> |
||||
<a-tab-pane v-for="item in langs" :key="item.name" class="!h-full"> |
||||
<template #tab> |
||||
<div class="capitalize select-none"> |
||||
{{ item.name }} |
||||
</div> |
||||
</template> |
||||
<monaco-editor |
||||
class="h-[60vh] border-1 border-gray-100 py-4 rounded-sm" |
||||
:model-value="code" |
||||
:read-only="true" |
||||
lang="typescript" |
||||
:validate="false" |
||||
:disable-deep-compare="true" |
||||
/> |
||||
<div class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase"> |
||||
<a-button |
||||
v-t="[ |
||||
'c:snippet:copy', |
||||
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name }, |
||||
]" |
||||
type="primary" |
||||
@click="onCopyToClipboard" |
||||
>Copy to clipboard</a-button |
||||
> |
||||
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem"> |
||||
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client"> |
||||
{{ client }} |
||||
</a-select-option> |
||||
</a-select> |
||||
</div> |
||||
|
||||
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]"> |
||||
<a |
||||
v-t="['e:hiring']" |
||||
class="px-4 py-2 ! rounded shadow" |
||||
href="https://angel.co/company/nocodb" |
||||
target="_blank" |
||||
@click.stop |
||||
> |
||||
🚀 We are Hiring! 🚀 |
||||
</a> |
||||
</div> |
||||
</a-tab-pane> |
||||
</a-tabs> |
||||
</div> |
||||
</a-drawer> |
||||
</template> |
||||
|
||||
<style scoped></style> |
@ -0,0 +1,209 @@
|
||||
import { UITypes } from 'nocodb-sdk' |
||||
import type { ColumnType, TableType } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { message, notification } from 'ant-design-vue' |
||||
import dayjs from 'dayjs' |
||||
import { useApi, useInjectionState, useProject, useProvideSmartsheetRowStore } from '#imports' |
||||
import { NOCO } from '~/lib' |
||||
import { useNuxtApp } from '#app' |
||||
import type { Row } from '~/composables/useViewData' |
||||
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils' |
||||
|
||||
const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => { |
||||
const { $e, $state, $api } = useNuxtApp() |
||||
const { api, isLoading: isCommentsLoading, error: commentsError } = useApi() |
||||
// { useGlobalInstance: true },
|
||||
// state
|
||||
const commentsOnly = ref(false) |
||||
const commentsAndLogs = ref([]) |
||||
const comment = ref('') |
||||
const commentsDrawer = ref(false) |
||||
const changedColumns = ref(new Set<string>()) |
||||
const { project } = useProject() |
||||
const rowStore = useProvideSmartsheetRowStore(meta, row) |
||||
// todo
|
||||
// const activeView = inject(ActiveViewInj)
|
||||
|
||||
// const { updateOrSaveRow, insertRow } = useViewData(meta, activeView as any)
|
||||
|
||||
// getters
|
||||
const primaryValue = computed(() => { |
||||
if (row?.value?.row) { |
||||
const col = meta?.value?.columns?.find((c) => c.pv) |
||||
if (!col) { |
||||
return |
||||
} |
||||
const value = row.value.row?.[col.title as string] |
||||
const uidt = col.uidt |
||||
if (uidt === UITypes.Date) { |
||||
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD') |
||||
} else if (uidt === UITypes.DateTime) { |
||||
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD HH:mm') |
||||
} else if (uidt === UITypes.Time) { |
||||
let dateTime = dayjs(value) |
||||
if (!dateTime.isValid()) { |
||||
dateTime = dayjs(value, 'HH:mm:ss') |
||||
} |
||||
if (!dateTime.isValid()) { |
||||
dateTime = dayjs(`1999-01-01 ${value}`) |
||||
} |
||||
if (!dateTime.isValid()) { |
||||
return value |
||||
} |
||||
return dateTime.format('HH:mm:ss') |
||||
} |
||||
return value |
||||
} |
||||
}) |
||||
|
||||
// actions
|
||||
const loadCommentsAndLogs = async () => { |
||||
if (!row.value) return |
||||
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) |
||||
if (!rowId) return |
||||
commentsAndLogs.value = |
||||
( |
||||
await api.utils.commentList({ |
||||
row_id: rowId, |
||||
fk_model_id: meta.value.id as string, |
||||
comments_only: commentsOnly.value, |
||||
}) |
||||
)?.reverse?.() || [] |
||||
} |
||||
|
||||
const isYou = (email: string) => { |
||||
return $state.user?.value?.email === email |
||||
} |
||||
|
||||
const saveComment = async () => { |
||||
try { |
||||
if (!row.value || !comment.value) return |
||||
const rowId = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) |
||||
if (!rowId) return |
||||
|
||||
await api.utils.commentRow({ |
||||
fk_model_id: meta.value?.id as string, |
||||
row_id: rowId, |
||||
// todo: swagger type correction
|
||||
description: comment.value, |
||||
} as any) |
||||
|
||||
comment.value = '' |
||||
message.success('Comment added successfully') |
||||
await loadCommentsAndLogs() |
||||
} catch (e: any) { |
||||
message.error(e.message) |
||||
} |
||||
|
||||
$e('a:row-expand:comment') |
||||
} |
||||
|
||||
const save = async () => { |
||||
let data |
||||
try { |
||||
// todo:
|
||||
// if (this.presetValues) {
|
||||
// // cater presetValues
|
||||
// for (const k in this.presetValues) {
|
||||
// this.$set(this.changedColumns, k, true);
|
||||
// }
|
||||
// }
|
||||
|
||||
const updateOrInsertObj = [...changedColumns.value].reduce((obj, col) => { |
||||
obj[col] = row.value.row[col] |
||||
return obj |
||||
}, {} as Record<string, any>) |
||||
|
||||
if (row.value.rowMeta.new) { |
||||
data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, updateOrInsertObj) |
||||
|
||||
/* todo: |
||||
// save hasmany and manytomany relations from local state
|
||||
if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) { |
||||
for (const vcell of this.$refs.virtual) { |
||||
if (vcell.save) { |
||||
await vcell.save(this.localState); |
||||
} |
||||
} |
||||
} */ |
||||
row.value = { |
||||
row: data, |
||||
rowMeta: {}, |
||||
oldRow: { ...data }, |
||||
} |
||||
|
||||
/// todo:
|
||||
// await this.reload();
|
||||
} else if (Object.keys(updateOrInsertObj).length) { |
||||
const id = extractPkFromRow(row.value.row, meta.value.columns as ColumnType[]) |
||||
|
||||
if (!id) { |
||||
return message.info("Update not allowed for table which doesn't have primary Key") |
||||
} |
||||
await $api.dbTableRow.update(NOCO, project.value.title as string, meta.value.title, id, updateOrInsertObj) |
||||
for (const key of Object.keys(updateOrInsertObj)) { |
||||
// audit
|
||||
$api.utils |
||||
.auditRowUpdate(id, { |
||||
fk_model_id: meta.value.id, |
||||
column_name: key, |
||||
row_id: id, |
||||
value: getPlainText(updateOrInsertObj[key]), |
||||
prev_value: getPlainText(row.value.oldRow[key]), |
||||
}) |
||||
.then(() => {}) |
||||
} |
||||
} else { |
||||
return message.info('No columns to update') |
||||
} |
||||
|
||||
// this.$emit('update:oldRow', { ...this.localState });
|
||||
// this.changedColumns = {};
|
||||
// this.$emit('input', this.localState);
|
||||
// this.$emit('update:isNew', false);
|
||||
|
||||
notification.success({ |
||||
message: `${primaryValue.value || 'Row'} updated successfully.`, |
||||
// position: 'bottom-right',
|
||||
}) |
||||
|
||||
changedColumns.value = new Set() |
||||
} catch (e: any) { |
||||
notification.error({ message: `Failed to update row`, description: await extractSdkResponseErrorMsg(e) }) |
||||
} |
||||
$e('a:row-expand:add') |
||||
return data |
||||
} |
||||
|
||||
return { |
||||
...rowStore, |
||||
commentsOnly, |
||||
loadCommentsAndLogs, |
||||
commentsAndLogs, |
||||
isCommentsLoading, |
||||
commentsError, |
||||
saveComment, |
||||
comment, |
||||
isYou, |
||||
commentsDrawer, |
||||
row, |
||||
primaryValue, |
||||
save, |
||||
changedColumns, |
||||
} |
||||
}, 'expanded-form-store') |
||||
|
||||
export { useProvideExpandedFormStore } |
||||
|
||||
export function useExpandedFormStoreOrThrow() { |
||||
const expandedFormStore = useExpandedFormStore() |
||||
if (expandedFormStore == null) throw new Error('Please call `useExpandedFormStore` on the appropriate parent component') |
||||
return expandedFormStore |
||||
} |
||||
|
||||
// todo: move to utils
|
||||
function getPlainText(htmlString: string) { |
||||
const div = document.createElement('div') |
||||
div.textContent = htmlString || '' |
||||
return div.innerHTML |
||||
} |
@ -0,0 +1,105 @@
|
||||
import { notification } from 'ant-design-vue' |
||||
import { UITypes } from 'nocodb-sdk' |
||||
import type { ColumnType, LinkToAnotherRecordType, RelationTypes, TableType } from 'nocodb-sdk' |
||||
import type { Ref } from 'vue' |
||||
import { useNuxtApp } from '#app' |
||||
import { useInjectionState, useMetas, useProject, useVirtualCell } from '#imports' |
||||
import type { Row } from '~/composables/useViewData' |
||||
import { NOCO } from '~/lib' |
||||
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils' |
||||
|
||||
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => { |
||||
const { $api } = useNuxtApp() |
||||
const { project } = useProject() |
||||
const { metas } = useMetas() |
||||
|
||||
// state
|
||||
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({}) |
||||
|
||||
// getters
|
||||
const isNew = computed(() => row.value?.rowMeta?.new ?? false) |
||||
|
||||
// actions
|
||||
const addLTARRef = async (value: Record<string, any>, column: ColumnType) => { |
||||
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column))) |
||||
if (isHm || isMm) { |
||||
state.value[column.title!] = state.value[column.title!] || [] |
||||
state.value[column.title!]!.push(value) |
||||
} else if (isBt) { |
||||
state.value[column.title!] = value |
||||
} |
||||
} |
||||
|
||||
// actions
|
||||
const removeLTARRef = async (value: Record<string, any>, column: ColumnType) => { |
||||
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column))) |
||||
if (isHm || isMm) { |
||||
state.value[column.title!]?.splice(state.value[column.title!]?.indexOf(value), 1) |
||||
} else if (isBt) { |
||||
state.value[column.title!] = null |
||||
} |
||||
} |
||||
|
||||
const linkRecord = async (rowId: string, relatedRowId: string, column: ColumnType, type: RelationTypes) => { |
||||
try { |
||||
await $api.dbTableRow.nestedAdd( |
||||
NOCO, |
||||
project.value.title as string, |
||||
meta.value.title as string, |
||||
rowId, |
||||
type, |
||||
column.title as string, |
||||
relatedRowId, |
||||
) |
||||
} catch (e: any) { |
||||
notification.error({ |
||||
message: 'Linking failed', |
||||
description: await extractSdkResponseErrorMsg(e), |
||||
}) |
||||
} |
||||
} |
||||
|
||||
/** sync LTAR relations kept in local state */ |
||||
const syncLTARRefs = async (row: Record<string, any>) => { |
||||
const id = extractPkFromRow(row, meta.value.columns as ColumnType[]) |
||||
for (const column of meta?.value?.columns ?? []) { |
||||
if (column.uidt !== UITypes.LinkToAnotherRecord) continue |
||||
const colOptions = column?.colOptions as LinkToAnotherRecordType |
||||
|
||||
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column))) |
||||
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string] |
||||
|
||||
if (isHm || isMm) { |
||||
const relatedRows = (state.value?.[column.title!] ?? []) as Record<string, any>[] |
||||
for (const relatedRow of relatedRows) { |
||||
await linkRecord(id, extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]), column, colOptions.type) |
||||
} |
||||
} else if (isBt && state?.value?.[column.title!]) { |
||||
await linkRecord( |
||||
id, |
||||
extractPkFromRow(state.value?.[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]), |
||||
column, |
||||
colOptions.type, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return { |
||||
row, |
||||
state, |
||||
isNew, |
||||
// todo: use better name
|
||||
addLTARRef, |
||||
removeLTARRef, |
||||
syncLTARRefs, |
||||
} |
||||
}, 'smartsheet-row-store') |
||||
|
||||
export { useProvideSmartsheetRowStore } |
||||
|
||||
export function useSmartsheetRowStoreOrThrow() { |
||||
const smartsheetRowStore = useSmartsheetRowStore() |
||||
if (smartsheetRowStore == null) throw new Error('Please call `useSmartsheetRowStore` on the appropriate parent component') |
||||
return smartsheetRowStore |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
import VueDOMPurifyHTML from 'vue-dompurify-html' |
||||
import { defineNuxtPlugin } from 'nuxt/app' |
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => { |
||||
nuxtApp.vueApp.use(VueDOMPurifyHTML) |
||||
}) |
@ -0,0 +1,11 @@
|
||||
import type { ColumnType } from 'nocodb-sdk' |
||||
|
||||
export const extractPkFromRow = (row: Record<string, any>, columns: ColumnType[]) => { |
||||
return ( |
||||
row && |
||||
columns |
||||
?.filter((c) => c.pk) |
||||
.map((c) => row?.[c.title as string]) |
||||
.join('___') |
||||
) |
||||
} |
Loading…
Reference in new issue