Browse Source

Merge branch 'develop' into feat/gui-v2-formula-options

pull/2998/head
Wing-Kam Wong 2 years ago
parent
commit
2e6c6264cf
  1. 50
      packages/nc-gui-v2/components/cell/Checkbox.vue
  2. 2
      packages/nc-gui-v2/components/cell/Integer.vue
  3. 3
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  4. 93
      packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue
  5. 65
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  6. 50
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  7. 65
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  8. 54
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  9. 295
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  10. 2
      packages/nc-gui-v2/composables/useGridViewColumnWidth.ts
  11. 250
      packages/nc-gui-v2/composables/useViewData.ts
  12. 37
      packages/nc-gui-v2/package-lock.json
  13. 1
      packages/nc-gui-v2/package.json
  14. 47
      packages/nc-gui-v2/utils/iconUtils.ts
  15. 1
      packages/nc-gui-v2/utils/index.ts

50
packages/nc-gui-v2/components/cell/Checkbox.vue

@ -1,20 +1,7 @@
<script setup lang="ts">
import { inject } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
import MdiCheckBold from '~icons/mdi/check-bold'
import MdiCropSquare from '~icons/mdi/crop-square'
import MdiCheckCircleOutline from '~icons/mdi/check-circle-outline'
import MdiCheckboxBlankCircleOutline from '~icons/mdi/checkbox-blank-circle-outline'
import MdiStar from '~icons/mdi/star'
import MdiStarOutline from '~icons/mdi/star-outline'
import MdiHeart from '~icons/mdi/heart'
import MdiHeartOutline from '~icons/mdi/heart-outline'
import MdiMoonFull from '~icons/mdi/moon-full'
import MdiMoonNew from '~icons/mdi/moon-new'
import MdiThumbUp from '~icons/mdi/thumb-up'
import MdiThumbUpOutline from '~icons/mdi/thumb-up-outline'
import MdiFlag from '~icons/mdi/flag'
import MdiFlagOutline from '~icons/mdi/flag-outline'
import { getMdiIcon } from '@/utils'
interface Props {
modelValue?: boolean | undefined | number
@ -42,46 +29,13 @@ const checkboxMeta = $computed(() => {
...(column?.meta || {}),
}
})
const icon = (type: string) => {
switch (type) {
case 'mdi-check-bold':
return MdiCheckBold
case 'mdi-crop-square':
return MdiCropSquare
case 'mdi-check-circle-outline':
return MdiCheckCircleOutline
case 'mdi-checkbox-blank-circle-outline':
return MdiCheckboxBlankCircleOutline
case 'mdi-star':
return MdiStar
case 'mdi-star-outline':
return MdiStarOutline
case 'mdi-heart':
return MdiHeart
case 'mdi-heart-outline':
return MdiHeartOutline
case 'mdi-moon-full':
return MdiMoonFull
case 'mdi-moon-new':
return MdiMoonNew
case 'mdi-thumb-up':
return MdiThumbUp
case 'mdi-thumb-up-outline':
return MdiThumbUpOutline
case 'mdi-flag':
return MdiFlag
case 'mdi-flag-outline':
return MdiFlagOutline
}
}
</script>
<template>
<div class="flex" :class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel }">
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="vModel = !vModel">
<component
:is="icon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{
color: checkboxMeta.color,
}"

2
packages/nc-gui-v2/components/cell/Integer.vue

@ -16,7 +16,7 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const vModel = useVModel(props, 'modelValue', emit)
const vModel = useVModel(props, 'modelValue', emits)
const focus = (el: HTMLInputElement) => el?.focus()

3
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -90,6 +90,7 @@ watchEffect(() => {
<SmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" />
<div>
<div
@ -118,7 +119,7 @@ watchEffect(() => {
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" size="small" @click="addOrUpdate(reloadMeta)">
<a-button html-type="submit" type="primary" size="small" @click="addOrUpdate(reloadMeta), (advancedOptions = false)">
<!-- Save -->
{{ $t('general.save') }}
</a-button>

93
packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue

@ -0,0 +1,93 @@
<script setup lang="ts">
import { Sketch } from '@ckpack/vue-color'
import { useColumnCreateStoreOrThrow } from '#imports'
import { enumColor, getMdiIcon } from '@/utils'
const { formState, validateInfos, setAdditionalValidations, sqlUi, onDataTypeChange, onAlter } = useColumnCreateStoreOrThrow()
// cater existing v1 cases
const iconList = [
{
full: 'mdi-star',
empty: 'mdi-star-outline',
},
{
full: 'mdi-heart',
empty: 'mdi-heart-outline',
},
{
full: 'mdi-moon-full',
empty: 'mdi-moon-new',
},
{
full: 'mdi-thumb-up',
empty: 'mdi-thumb-up-outline',
},
{
full: 'mdi-flag',
empty: 'mdi-flag-outline',
},
]
const advanced = ref(true)
const picked = ref(formState.value.meta.color || enumColor.light[0])
</script>
<template>
<a-row>
<a-col :span="12">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.icon" size="small" class="w-52">
<!-- FIXME: antdv doesn't support object as value -->
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="icon">
<component
:is="getMdiIcon(icon.full)"
:style="{
color: formState.meta.color,
}"
/>
{{ ' ' }}
<component
:is="getMdiIcon(icon.empty)"
:style="{
color: formState.meta.color,
}"
/>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Max">
<a-select v-model:value="formState.meta.max" class="w-52" size="small">
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
{{ v }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-card class="w-full shadow-lg mt-2" body-style="padding: 0px">
<a-collapse v-model:activeKey="advanced" accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class="">
<a-button class="!bg-primary text-white w-full" @click="formState.meta.color = picked.hex"> Pick Color </a-button>
<div class="px-7 py-3">
<Sketch v-model="picked" />
</div>
</a-collapse-panel>
</a-collapse>
</a-card>
</a-row>
</template>
<style scoped lang="scss">
.color-selector:hover {
@apply brightness-90;
}
.color-selector.selected {
@apply py-[5px] px-[10px] brightness-90;
}
</style>

65
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -6,74 +6,13 @@ import { ColumnInj, MetaInj } from '~/context'
import { useProvideColumnCreateStore } from '#imports'
const { column } = defineProps<{ column: ColumnType & { meta: any } }>()
provide(ColumnInj, column)
const meta = inject(MetaInj)
// instantiate column update store
useProvideColumnCreateStore(meta as Ref<TableType>, column)
/*
import { UITypes } from 'nocodb-sdk'
import cell from '@/components/project/spreadsheet/mixins/cell'
import EditColumn from '~/components/project/spreadsheet/components/EditColumn'
export default {
name: 'HeaderCell',
components: { EditColumn },
mixins: [cell],
props: [
'value',
'column',
'isForeignKey',
'meta',
'nodes',
'columnIndex',
'isForm',
'isPublicView',
'isVirtual',
'required',
'isLocked',
],
data: () => ({
editColumnMenu: false,
columnDeleteDialog: false,
}),
methods: {
showColumnEdit() {
if (this.column.uidt === UITypes.ID) {
return this.$toast.info('Primary key column edit is not allowed.').goAway(3000)
}
this.editColumnMenu = true
},
async deleteColumn() {
try {
const column = { ...this.column, cno: this.column.column_name }
column.altered = 4
const columns = this.meta.columns.slice()
columns[this.columnIndex] = column
await this.$api.dbTableColumn.delete(column.id)
this.$emit('colDelete')
this.$emit('saved')
this.columnDeleteDialog = false
} catch (e) {
console.log(e)
}
},
async setAsPrimaryValue() {
// todo: pass only updated fields
try {
await this.$api.dbTableColumn.primaryColumnSet(this.column.id)
this.$toast.success('Successfully updated as primary column').goAway(3000)
} catch (e) {
console.log(e)
this.$toast.error('Failed to update primary column').goAway(3000)
}
this.$emit('saved')
this.columnDeleteDialog = false
},
},
} */
</script>
<template>

50
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -1,10 +1,54 @@
<script lang="ts" setup>
import { Modal } from 'ant-design-vue'
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { useToast } from 'vue-toastification'
import { useNuxtApp } from '#app'
import { useMetas } from '#imports'
import { ColumnInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiEditIcon from '~icons/mdi/pencil'
import MdiStarIcon from '~icons/mdi/star'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const { virtual = false } = defineProps<{ virtual?: boolean }>()
const editColumnDropdown = $ref(false)
const column = inject(ColumnInj)
const meta = inject(MetaInj)
const { $api } = useNuxtApp()
const { t } = useI18n()
const toast = useToast()
const { getMeta } = useMetas()
const deleteColumn = () =>
Modal.confirm({
title: h('div', ['Do you want to delete ', h('span', { class: 'font-weight-bold' }, [column?.title]), ' column ?']),
okText: t('general.delete'),
okType: 'danger',
cancelText: t('general.cancel'),
async onOk() {
try {
await $api.dbTableColumn.delete(column?.id as string)
getMeta(meta?.value?.id as string, true)
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
},
})
const setAsPrimaryValue = async () => {
try {
await $api.dbTableColumn.primaryColumnSet(column?.id as string)
getMeta(meta?.value?.id as string, true)
toast.success('Successfully updated as primary column')
} catch (e) {
console.log(e)
toast.error('Failed to update primary column')
}
}
</script>
<template>
@ -18,12 +62,12 @@ const editColumnDropdown = $ref(false)
<MdiMenuDownIcon class="text-grey" />
<template #overlay>
<div class="shadow bg-white">
<div class="nc-column-edit nc-menu-item" @click="editColumnDropdown = true">
<div v-if="!virtual" class="nc-column-edit nc-menu-item" @click="editColumnDropdown = true">
<MdiEditIcon class="text-primary" />
<!-- Edit -->
{{ $t('general.edit') }}
</div>
<div v-t="['a:column:set-primary']" class="nc-menu-item">
<div v-if="!virtual" v-t="['a:column:set-primary']" class="nc-menu-item" @click="setAsPrimaryValue">
<MdiStarIcon class="text-primary" />
<!-- todo : tooltip -->
@ -31,7 +75,7 @@ const editColumnDropdown = $ref(false)
{{ $t('activity.setPrimary') }}
<!-- <span class="caption font-weight-bold">Primary value will be shown in place of primary key</span> -->
</div>
<div class="nc-column-delete nc-menu-item">
<div class="nc-column-delete nc-menu-item" @click="deleteColumn">
<MdiDeleteIcon class="text-error" />
<!-- Delete -->
{{ $t('general.delete') }}

65
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -145,71 +145,8 @@ provide(ColumnInj, column)
<!-- </v-tooltip> -->
<v-spacer />
<!--
todo: implement delete or edit column
<v-menu
v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('edit-column') && !isForm"
offset-y
open-on-hover
left
transition="slide-y-transition"
>
<template #activator="{ on }">
<v-icon v-if="!isLocked && !isForm" small v-on="on"> mdi-menu-down </v-icon>
</template>
<v-list dense>
<v-list-item dense @click="editColumnMenu = true">
<x-icon small class="mr-1 nc-column-edit" color="primary"> mdi-pencil </x-icon>
<span class="caption">
&lt;!&ndash; Edit &ndash;&gt;
{{ $t('general.edit') }}
</span>
</v-list-item>
<v-list-item @click="columnDeleteDialog = true">
<x-icon small class="mr-1 nc-column-delete" color="error"> mdi-delete-outline </x-icon>
<span class="caption">
&lt;!&ndash; Delete &ndash;&gt;
{{ $t('general.delete') }}
</span>
</v-list-item>
</v-list>
</v-menu>
<v-dialog v-model="columnDeleteDialog" max-width="500" persistent>
<v-card>
<v-card-title class="grey darken-2 subheading white&#45;&#45;text"> Confirm </v-card-title>
<v-divider />
<v-card-text class="mt-4 title">
Do you want to delete <span class="font-weight-bold">'{{ column.title }}'</span> column ?
</v-card-text>
<v-divider />
<v-card-actions class="d-flex pa-4">
<v-spacer />
<v-btn small @click="columnDeleteDialog = false">
&lt;!&ndash; Cancel &ndash;&gt;
{{ $t('general.cancel') }}
</v-btn>
<v-btn v-t="['a:column:delete']" small color="error" @click="deleteColumn"> Confirm </v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-menu v-model="editColumnMenu" offset-y content-class="" left transition="slide-y-transition">
<template #activator="{ on }">
<span v-on="on" />
</template>
<edit-virtual-column
v-if="editColumnMenu"
v-model="editColumnMenu"
:nodes="nodes"
:edit-column="true"
:column="column"
:meta="meta"
:sql-ui="sqlUi"
v-on="$listeners"
/>
</v-menu> -->
<SmartsheetHeaderMenu :virtual="true" />
</div>
</template>

54
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -1,25 +1,71 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import { provide } from 'vue'
import { useColumn, useVModel } from '#imports'
import { computed, useColumn, useDebounceFn, useVModel } from '#imports'
import { ColumnInj } from '~/context'
interface Props {
column: ColumnType
modelValue: any
editEnabled: boolean
}
interface Emits {
(event: 'update:modelValue', value: any): void
}
const { column, ...rest } = defineProps<Props>()
const { column, editEnabled, ...rest } = defineProps<Props>()
const emit = defineEmits<Emits>()
const emit = defineEmits(['update:modelValue', 'save'])
provide(ColumnInj, column)
const vModel = useVModel(rest, 'modelValue', emit)
provide(
'editEnabled',
computed(() => editEnabled),
)
let changed = $ref(false)
const syncValue = useDebounceFn(function () {
emit('save')
}, 1000)
const isAutoSaved = $computed(() => {
return [
UITypes.SingleLineText,
UITypes.LongText,
UITypes.PhoneNumber,
UITypes.Email,
UITypes.URL,
UITypes.Number,
UITypes.Decimal,
UITypes.Percent,
UITypes.Count,
UITypes.AutoNumber,
UITypes.SpecificDBType,
UITypes.Geometry,
].includes(column.uidt as UITypes)
})
const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Year, UITypes.Time, UITypes.Duration].includes(column.uidt as UITypes)
})
const vModel = computed({
get: () => rest.modelValue,
set: (val) => {
if (val !== rest.modelValue) {
changed = true
emit('update:modelValue', val)
if (isAutoSaved) {
syncValue()
} else if (!isManualSaved) {
emit('save')
}
}
},
})
const {
isURL,

295
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -1,12 +1,14 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { ColumnType, isVirtualCol } from 'nocodb-sdk'
import {
Row,
inject,
onKeyStroke,
onMounted,
provide,
useGridViewColumnWidth,
useProvideColumnCreateStore,
useSmartsheetStoreOrThrow,
useViewData,
} from '#imports'
import {
@ -16,36 +18,55 @@ import {
FieldsInj,
IsFormInj,
IsGridInj,
IsLockedInj,
MetaInj,
PaginationDataInj,
ReloadViewDataHookInj,
} from '~/context'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiArrowExpandIcon from '~icons/mdi/arrow-expand'
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
// keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery
const fields = inject(FieldsInj)
const fields = inject(FieldsInj, ref([]))
const isLocked = inject(IsLockedInj, false)
// todo: get from parent ( inject or use prop )
const isPublicView = false
const selected = reactive<{ row?: number | null; col?: number | null }>({})
const editEnabled = ref(false)
const selected = reactive<{ row: number | null; col: number | null }>({ row: null, col: null })
let editEnabled = $ref(false)
const { sqlUi } = useProject()
const { xWhere } = useSmartsheetStoreOrThrow()
const addColumnDropdown = ref(false)
const contextMenu = ref(false)
const contextMenuTarget = ref(false)
const visibleColLength = $computed(() => {
const cols = fields.value
return cols.filter((col) => !isVirtualCol(col)).length
})
const { loadData, paginationData, formattedData: data, updateRowProperty, changePage } = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view)
const {
loadData,
paginationData,
formattedData: data,
updateOrSaveRow,
changePage,
addEmptyRow,
deleteRow,
deleteSelectedRows,
selectedAllRecords,
} = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns)
provide(IsFormInj, false)
provide(IsGridInj, true)
provide(PaginationDataInj, paginationData)
provide(ChangePageInj, changePage)
provide(EditModeInj, editEnabled)
provide(ChangePageInj, changePage)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
reloadViewDataHook?.on(() => {
@ -59,7 +80,7 @@ const selectCell = (row: number, col: number) => {
onKeyStroke(['Enter'], (e) => {
if (selected.row !== null && selected.col !== null) {
editEnabled.value = true
editEnabled = true
}
})
@ -89,75 +110,200 @@ defineExpose({
// watchEffect(() => {
if (meta) useProvideColumnCreateStore(meta)
// })
// reset context menu target on hide
watch(contextMenu, () => {
if (!contextMenu.value) {
contextMenuTarget.value = false
}
})
const clearCell = async (ctx: { row: number; col: number }) => {
const rowObj = data.value[ctx.row]
const columnObj = fields.value[ctx.col]
if (isVirtualCol(columnObj)) {
return
}
rowObj.row[columnObj.title] = null
// update/save cell value
await updateOrSaveRow(rowObj, columnObj.title)
}
/** handle keypress events */
onKeyStroke(['Tab', 'Shift', 'Enter', 'Delete', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'], async (e: KeyboardEvent) => {
if (selected.row !== null && selected.col !== null) {
/** on tab key press navigate through cells */
switch (e.key) {
case 'Tab':
e.preventDefault()
if (e.shiftKey) {
if (selected.col > 0) {
selected.col--
} else if (selected.row > 0) {
selected.row--
selected.col = visibleColLength - 1
}
} else {
if (selected.col < visibleColLength - 1) {
selected.col++
} else if (selected.row < data.value.length - 1) {
selected.row++
selected.col = 0
}
}
break
/** on enter key press make cell editable */
case 'Enter':
e.preventDefault()
editEnabled = true
break
/** on delete key press clear cell */
case 'Delete':
e.preventDefault()
await clearCell(selected as { row: number; col: number })
break
/** on arrow key press navigate through cells */
case 'ArrowRight':
e.preventDefault()
if (selected.col < visibleColLength - 1) selected.col++
break
case 'ArrowLeft':
e.preventDefault()
if (selected.col > 0) selected.col--
break
case 'ArrowUp':
e.preventDefault()
if (selected.row > 0) selected.row--
break
case 'ArrowDown':
e.preventDefault()
if (selected.row < data.value.length - 1) selected.row++
break
}
}
})
</script>
<template>
<div class="flex flex-col h-100 min-h-0 w-100">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-primary">
<table class="xc-row-table nc-grid backgroundColorDefault">
<thead>
<tr>
<th>#</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-t="['c:column:add']" @click="addColumnDropdown = true">
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-full flex align-center justify-center">
<MdiPlusIcon class="text-sm" />
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<thead>
<tr class="group">
<th>
<div class="flex align-center w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="group-hover:flex w-full align-center"
>
<a-checkbox v-model:checked="selectedAllRecords" />
<span class="flex-1" />
</div>
</div>
</th>
<th
v-for="col in fields"
:key="col.title"
v-xc-ver-resize
:data-col="col.id"
@xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th v-t="['c:column:add']" @click="addColumnDropdown = true">
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-full flex align-center justify-center">
<MdiPlusIcon class="text-sm" />
</div>
<template #overlay>
<SmartsheetColumnEditOrAdd @click.stop @cancel="addColumnDropdown = false" />
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data" :key="rowIndex" class="nc-grid-row group">
<td key="row-index" class="caption nc-grid-cell">
<div class="align-center flex w-[80px]">
<div class="group-hover:hidden" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="group-hover:flex w-full align-center"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
<span class="flex-1" />
<MdiArrowExpandIcon class="text-sm text-pink hidden group-hover:inline-block" />
</div>
</div>
<template #overlay>
<SmartsheetColumnEditOrAdd @click.stop @cancel="addColumnDropdown = false" />
</template>
</a-dropdown>
</th>
</tr>
</thead>
<tbody>
<tr v-for="({ row }, rowIndex) in data" :key="rowIndex" class="nc-grid-row">
<td key="row-index" style="width: 65px" class="caption nc-grid-cell">
<div class="d-flex align-center">
{{ rowIndex + 1 }}
</div>
</td>
<td
v-for="(columnObj, colIndex) in fields"
:key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell"
:class="{
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
// 'primary-column': primaryValueColumn === columnObj.title,
// 'text-center': isCentrallyAligned(columnObj),
// 'required': isRequired(columnObj, rowObj),
}"
:data-col="columnObj.id"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
>
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row[columnObj.title]" :column="columnObj" />
<SmartsheetCell
v-else
v-model="row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
@update:model-value="updateRowProperty(row, columnObj.title)"
/>
</td>
</tr>
</tbody>
</table>
</td>
<td
v-for="(columnObj, colIndex) in fields"
:key="rowIndex + columnObj.title"
class="cell pointer nc-grid-cell"
:class="{
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
}"
:data-col="columnObj.id"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
@contextmenu="contextMenuTarget = { row: rowIndex, col: colIndex }"
>
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row.row[columnObj.title]" :column="columnObj" />
<SmartsheetCell
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
@save="updateOrSaveRow(row, columnObj.title)"
/>
</td>
</tr>
<tr v-if="!isLocked">
<td
v-t="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell"
@click="addEmptyRow()"
>
<a-tooltip top left>
<div class="w-min flex align-center">
<MdiPlusIcon class="text-pint-500 text-xs" />
<span class="ml-1 caption grey--text">
{{ $t('activity.addRow') }}
</span>
</div>
<template #title>
<span class="caption"> Add new row</span>
</template>
</a-tooltip>
</td>
</tr>
</tbody>
</table>
<template #overlay>
<div class="bg-white shadow" @click="contextMenu = false">
<div v-if="contextMenuTarget" class="nc-menu-item" @click="deleteRow(contextMenuTarget.row)">Delete row</div>
<div class="nc-menu-item" @click="deleteSelectedRows">Delete all selected rows</div>
<div v-if="contextMenuTarget" class="nc-menu-item" @click="clearCell(contextMenuTarget)">Clear cell</div>
<div v-if="contextMenuTarget" class="nc-menu-item" @click="addEmptyRow(contextMenuTarget.row + 1)">
Insert new row
</div>
</div>
</template>
</a-dropdown>
</div>
<SmartsheetPagination />
</div>
</template>
@ -175,6 +321,11 @@ if (meta) useProvideColumnCreateStore(meta)
height: 41px !important;
position: relative;
padding: 0 5px;
& > * {
@apply flex align-center h-auto;
}
overflow: hidden;
}
@ -186,8 +337,6 @@ if (meta) useProvideColumnCreateStore(meta)
border-bottom: 1px solid #7f828b33 !important;
border-top: 1px solid #7f828b33 !important;
border-collapse: collapse;
font-size: 0.8rem;
}
td {

2
packages/nc-gui-v2/composables/useGridViewColumnWidth.ts

@ -19,7 +19,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
watch(
// todo : update type in swagger
() => [gridViewCols, resizingCol, resizingColWidth],
() => [gridViewCols, resizingCol, resizingColWidth, columns],
() => {
let style = ''
for (const c of columns?.value || []) {

250
packages/nc-gui-v2/composables/useViewData.ts

@ -1,8 +1,10 @@
import type { Api, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { notification } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { NOCO } from '~/lib'
import { extractSdkResponseErrorMsg } from '~/utils'
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
@ -11,95 +13,227 @@ const formatData = (list: Record<string, any>[]) =>
rowMeta: {},
}))
export interface Row {
row: Record<string, any>
oldRow: Record<string, any>
rowMeta?: any
}
export function useViewData(
meta: Ref<TableType> | ComputedRef<TableType> | undefined,
viewMeta: Ref<ViewType & { id: string }> | ComputedRef<ViewType & { id: string }> | undefined,
where?: ComputedRef<string | undefined>,
) {
const data = ref<Record<string, any>[]>()
const formattedData = ref<{ row: Record<string, any>; oldRow: Record<string, any>; rowMeta?: any }[]>()
const formattedData = ref<Row[]>([])
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const { project } = useProject()
const { $api } = useNuxtApp()
const selectedAllRecords = computed({
get() {
return formattedData.value.every((row) => row.rowMeta.selected)
},
set(selected) {
formattedData.value.forEach((row) => (row.rowMeta.selected = selected))
},
})
const loadData = async (params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
if (!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) return
const response = await $api.dbViewRow.list('noco', project.value.id, meta.value.id, viewMeta.value.id, {
...params,
where: where?.value,
})
data.value = response.list
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
}
const insertRow = async (row: Record<string, any>, rowIndex = formattedData.value?.length) => {
try {
const insertObj = meta?.value?.columns?.reduce((o: any, col) => {
if (!col.ai && row?.[col.title as string] !== null) {
o[col.title as string] = row?.[col.title as string]
}
return o
}, {})
const insertedData = await $api.dbViewRow.create(
NOCO,
project?.value.id as string,
meta?.value.id as string,
viewMeta?.value?.id as string,
insertObj,
)
formattedData.value?.splice(rowIndex ?? 0, 1, {
row: insertedData,
rowMeta: {},
oldRow: { ...insertedData },
})
} catch (error: any) {
notification.error({
message: 'Row insert failed',
description: await extractSdkResponseErrorMsg(error),
})
}
}
const updateRowProperty = async (row: Record<string, any>, property: string) => {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => row[c.title as string])
.join('___') as string
return $api.dbViewRow.update(
NOCO,
project?.value.id as string,
try {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => row[c.title as string])
.join('___') as string
return $api.dbViewRow.update(
NOCO,
project?.value.id as string,
meta?.value.id as string,
viewMeta?.value?.id as string,
id,
{
[property]: row[property],
},
// todo:
// {
// query: { ignoreWebhook: !saved }
// }
)
/*
todo: audit
// audit
this.$api.utils
.auditRowUpdate(id, {
fk_model_id: this.meta.id,
column_name: column.title,
row_id: id,
value: getPlainText(rowObj[column.title]),
prev_value: getPlainText(oldRow[column.title])
})
.then(() => {})
*/
} catch (error: any) {
notification.error({
message: 'Row update failed',
description: await extractSdkResponseErrorMsg(error),
})
}
}
const updateOrSaveRow = async (row: Row, property: string) => {
if (row.rowMeta.new) {
await insertRow(row.row, formattedData.value.indexOf(row))
} else {
await updateRowProperty(row.row, property)
}
}
const changePage = async (page: number) => {
paginationData.value.page = page
await loadData({ offset: (page - 1) * (paginationData.value.pageSize || 25), where: where?.value } as any)
}
const addEmptyRow = (addAfter = formattedData.value.length) => {
formattedData.value.splice(addAfter, 0, {
row: {},
oldRow: {},
rowMeta: { new: true },
})
}
const deleteRowById = async (id: string) => {
if (!id) {
throw new Error("Delete not allowed for table which doesn't have primary Key")
}
const res: any = await $api.dbViewRow.delete(
'noco',
project.value.id as string,
meta?.value.id as string,
viewMeta?.value?.id as string,
viewMeta?.value.id as string,
id,
{
[property]: row[property],
},
// todo:
// {
// query: { ignoreWebhook: !saved }
// }
)
/*
todo: audit
// audit
this.$api.utils
.auditRowUpdate(id, {
fk_model_id: this.meta.id,
column_name: column.title,
row_id: id,
value: getPlainText(rowObj[column.title]),
prev_value: getPlainText(oldRow[column.title])
})
.then(() => {})
*/
if (res.message) {
notification.info({
message: 'Row delete failed',
description: h('div', {
innerHTML: `<div style="padding:10px 4px">Unable to delete row with ID ${id} because of the following:
<br><br>${res.message.join('<br>')}<br><br>
Clear the data first & try again</div>`,
}),
})
return false
}
return true
}
const insertRow = async (row: Record<string, any>, rowIndex = formattedData.value?.length) => {
// todo: implement insert row
const insertObj = meta?.value?.columns?.reduce((o: any, col) => {
if (!col.ai && row?.[col.title as string] !== null) {
o[col.title as string] = row?.[col.title as string]
const deleteRow = async (rowIndex: number) => {
try {
const row = formattedData.value[rowIndex]
if (!row.rowMeta.new) {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => row.row[c.title as any])
.join('___')
const deleted = await deleteRowById(id as string)
if (!deleted) {
return
}
}
return o
}, {})
formattedData.value.splice(rowIndex, 1)
} catch (e: any) {
notification.error({
message: 'Failed to delete row',
description: await extractSdkResponseErrorMsg(e),
})
}
}
const insertedData = await $api.dbViewRow.create(
NOCO,
project?.value.id as string,
meta?.value.id as string,
viewMeta?.value?.id as string,
insertObj,
)
const deleteSelectedRows = async () => {
let row = formattedData.value.length
while (row--) {
try {
const { row: rowObj, rowMeta } = formattedData.value[row]
if (!rowMeta.selected) {
continue
}
if (!rowMeta.new) {
const id = meta?.value?.columns
?.filter((c) => c.pk)
.map((c) => rowObj[c.title as string])
.join('___')
formattedData.value?.splice(rowIndex ?? 0, 1, {
row: insertedData,
rowMeta: {},
oldRow: { ...insertedData },
})
const successfulDeletion = await deleteRowById(id as string)
if (!successfulDeletion) {
continue
}
}
formattedData.value.splice(row, 1)
} catch (e: any) {
return notification.error({
message: 'Failed to delete row',
description: await extractSdkResponseErrorMsg(e),
})
}
}
}
const changePage = async (page: number) => {
paginationData.value.page = page
await loadData({ offset: (page - 1) * (paginationData.value.pageSize || 25), where: where?.value } as any)
return {
loadData,
paginationData,
formattedData,
insertRow,
updateRowProperty,
changePage,
addEmptyRow,
deleteRow,
deleteSelectedRows,
updateOrSaveRow,
selectedAllRecords,
}
return { data, loadData, paginationData, formattedData, insertRow, updateRowProperty, changePage }
}

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

@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",
@ -859,6 +860,22 @@
"node": ">=6.9.0"
}
},
"node_modules/@ckpack/vue-color": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.2.0.tgz",
"integrity": "sha512-c9X82nppYjSxjlITO6jdLLdt9HoyZzqEWpqDL2V6NJd859d6GCh/2AHeRXk+37uRJ1UdTkCuty93WOEqja8quw==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.0",
"lodash-es": "^4.17.21",
"material-colors": "^1.2.6"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@cloudflare/kv-asset-handler": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz",
@ -9325,6 +9342,11 @@
"semver": "bin/semver.js"
}
},
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"node_modules/mdast-util-from-markdown": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz",
@ -15371,6 +15393,16 @@
"to-fast-properties": "^2.0.0"
}
},
"@ckpack/vue-color": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ckpack/vue-color/-/vue-color-1.2.0.tgz",
"integrity": "sha512-c9X82nppYjSxjlITO6jdLLdt9HoyZzqEWpqDL2V6NJd859d6GCh/2AHeRXk+37uRJ1UdTkCuty93WOEqja8quw==",
"requires": {
"@ctrl/tinycolor": "^3.4.0",
"lodash-es": "^4.17.21",
"material-colors": "^1.2.6"
}
},
"@cloudflare/kv-asset-handler": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.2.0.tgz",
@ -21702,6 +21734,11 @@
}
}
},
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"mdast-util-from-markdown": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz",

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

@ -11,6 +11,7 @@
"coverage": "vitest -c test/vite.config.ts run --coverage"
},
"dependencies": {
"@ckpack/vue-color": "^1.2.0",
"@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10",

47
packages/nc-gui-v2/utils/iconUtils.ts

@ -0,0 +1,47 @@
import MdiCheckBold from '~icons/mdi/check-bold'
import MdiCropSquare from '~icons/mdi/crop-square'
import MdiCheckCircleOutline from '~icons/mdi/check-circle-outline'
import MdiCheckboxBlankCircleOutline from '~icons/mdi/checkbox-blank-circle-outline'
import MdiStar from '~icons/mdi/star'
import MdiStarOutline from '~icons/mdi/star-outline'
import MdiHeart from '~icons/mdi/heart'
import MdiHeartOutline from '~icons/mdi/heart-outline'
import MdiMoonFull from '~icons/mdi/moon-full'
import MdiMoonNew from '~icons/mdi/moon-new'
import MdiThumbUp from '~icons/mdi/thumb-up'
import MdiThumbUpOutline from '~icons/mdi/thumb-up-outline'
import MdiFlag from '~icons/mdi/flag'
import MdiFlagOutline from '~icons/mdi/flag-outline'
export const getMdiIcon = (type: string) => {
switch (type) {
case 'mdi-check-bold':
return MdiCheckBold
case 'mdi-crop-square':
return MdiCropSquare
case 'mdi-check-circle-outline':
return MdiCheckCircleOutline
case 'mdi-checkbox-blank-circle-outline':
return MdiCheckboxBlankCircleOutline
case 'mdi-star':
return MdiStar
case 'mdi-star-outline':
return MdiStarOutline
case 'mdi-heart':
return MdiHeart
case 'mdi-heart-outline':
return MdiHeartOutline
case 'mdi-moon-full':
return MdiMoonFull
case 'mdi-moon-new':
return MdiMoonNew
case 'mdi-thumb-up':
return MdiThumbUp
case 'mdi-thumb-up-outline':
return MdiThumbUpOutline
case 'mdi-flag':
return MdiFlag
case 'mdi-flag-outline':
return MdiFlagOutline
}
}

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

@ -5,6 +5,7 @@ export * from './formulaUtils'
export * from './durationUtils'
export * from './errorUtils'
export * from './fileUtils'
export * from './iconUtils'
export * from './filterUtils'
export * from './generateName'
export * from './projectCreateUtils'

Loading…
Cancel
Save