Browse Source

Merge pull request #2819 from nocodb/feat/main-section

fix(gui-v2): Main view ( tab view ) & Toolbar
pull/2888/head
Pranav C 2 years ago committed by GitHub
parent
commit
cc43e00e7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/nc-gui-v2/assets/style-v2.scss
  2. 2
      packages/nc-gui-v2/components/cell/TextArea.vue
  3. 6
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  4. 10
      packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue
  5. 73
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  6. 53
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  7. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/DeleteTable.vue
  8. 76
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  9. 671
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  10. 130
      packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue
  11. 434
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  12. 11
      packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue
  13. 37
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  14. 18
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  15. 113
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  16. 14
      packages/nc-gui-v2/components/smartsheet-toolbar/ToggleDrawer.vue
  17. 252
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  18. 20
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  19. 26
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  20. 43
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  21. 1
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  22. 1
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  23. 79
      packages/nc-gui-v2/composables/useTabs.ts
  24. 5
      packages/nc-gui-v2/composables/useViewColumns.ts
  25. 5
      packages/nc-gui-v2/context/index.ts
  26. 2
      packages/nc-gui-v2/nuxt.config.ts
  27. 43
      packages/nc-gui-v2/package-lock.json
  28. 6
      packages/nc-gui-v2/package.json
  29. 40
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  30. 152
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  31. 11
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  32. 5
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue
  33. 17
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue
  34. 1
      packages/nc-gui-v2/pages/project/index/create-external.vue
  35. 13
      packages/nocodb-sdk/src/lib/helperFunctions.ts

5
packages/nc-gui-v2/assets/style-v2.scss

@ -64,3 +64,8 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
html {
overflow-y: auto !important;
}
.nc-menu-item {
@apply cursor-pointer text-xs flex align-center gap-2 p-4 relative after:(content-[''] absolute top-0 left-0 w-full h-full right 0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}

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

@ -2,7 +2,7 @@
import { computed, inject, onMounted, ref } from '#imports'
interface Props {
modelValue: string
modelValue?: string
}
const { modelValue: value } = defineProps<Props>()

6
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -192,7 +192,7 @@ const reloadTables = async () => {
}
const addTableTab = (table: TableType) => {
$e('a:table:open')
addTab({ title: table.title, id: table.id })
addTab({ title: table.title, id: table.id, type: table.type as any })
}
</script>
@ -208,7 +208,7 @@ const addTableTab = (table: TableType) => {
</div>
<a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column">
<div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-primary">
<div
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@ -248,7 +248,7 @@ const addTableTab = (table: TableType) => {
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="cursor-pointer">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(table)"> Rename </a-menu-item>
<a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu>
</template>

10
packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue

@ -0,0 +1,10 @@
<script setup lang="ts">
import MdiAddIcon from '~icons/mdi/plus-outline '
const emit = defineEmits(['add-row'])
</script>
<template>
<MdiAddIcon class="text-grey" @click="emit('add-row')" />
</template>
<style scoped></style>

73
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -7,7 +7,7 @@ import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewFilters from '~/composables/useViewFilters'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
const { nested = false, parentId } = defineProps<{ nested?: boolean; parentId?: string }>()
const meta = inject(MetaInj)
@ -72,13 +72,13 @@ watch(
</script>
<template>
<div class="backgroundColor pa-2 menu-filter-dropdown bg-background" :style="{ width: nested ? '100%' : '630px' }">
<div class="bg-white shadow pa-2 menu-filter-dropdown" :style="{ width: nested ? '100%' : '630px' }">
<div v-if="filters && filters.length" class="grid" @click.stop>
<template v-for="(filter, i) in filters" :key="filter.id || i">
<template v-if="filter.status !== 'delete'">
<div v-if="filter.is_group" :key="i" style="grid-column: span 4; padding: 6px" class="elevation-4">
<div class="d-flex" style="gap: 6px; padding: 0 6px">
<v-icon
<!-- <v-icon
v-if="!filter.readOnly"
:key="`${i}_3`"
small
@ -86,10 +86,18 @@ watch(
@click.stop="deleteFilter(filter, i)"
>
mdi-close-box
</v-icon>
</v-icon> -->
<MdiDeleteIcon
v-if="!filter.readOnly"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter, i)"
/>
<span v-else :key="`${i}_1`" />
<v-select
v-model="filter.logical_op"
<a-select
v-model:value="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']"
density="compact"
@ -102,7 +110,7 @@ watch(
<!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item }}</span> -->
<!-- </template> -->
</v-select>
</a-select>
</div>
<!-- <column-filter
v-if="filter.id || shared"
@ -134,28 +142,24 @@ watch(
v-if="!filter.readOnly"
class="nc-filter-item-remove-btn text-grey align-self-center"
@click.stop="deleteFilter(filter, i)"
></MdiDeleteIcon>
/>
<span v-else />
<span v-if="!i" :key="`${i}_2`" class="text-xs d-flex align-center">{{ $t('labels.where') }}</span>
<span v-if="!i" class="text-xs d-flex align-center">{{ $t('labels.where') }}</span>
<v-select
<a-select
v-else
:key="`${i}_4`"
v-model="filter.logical_op"
class="w-full elevation-0 caption"
:items="['and', 'or']"
density="compact"
variant="solo"
v-model:value="filter.logical_op"
class="h-full"
:options="[
{ value: 'and', text: 'AND' },
{ value: 'or', text: 'OR' },
]"
hide-details
:disabled="filter.readOnly"
@click.stop
@change="filterUpdateCondition(filter, i)"
/>
<!-- <template #item="{ item }">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select> -->
<FieldListAutoCompleteDropdown
:key="`${i}_6`"
@ -167,10 +171,10 @@ watch(
@change="saveOrUpdate(filter, i)"
/>
<v-select
v-model="filter.comparison_op"
<a-select
v-model:value="filter.comparison_op"
class="caption nc-filter-operation-select text-sm"
:items="comparisonOpList.map((it) => it.value)"
:options="comparisonOpList"
:placeholder="$t('labels.operation')"
density="compact"
variant="solo"
@ -189,21 +193,17 @@ watch(
<!-- </template> -->
<!-- </v-select> -->
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="`span${i}`" />
<v-checkbox
<a-checkbox
v-else-if="types[filter.field] === 'boolean'"
:key="`${i}_7`"
v-model="filter.value"
v-model:value="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<v-text-field
<a-input
v-else
:key="`${i}_7`"
v-model="filter.value"
density="compact"
variant="solo"
hide-details
class="caption text-sm nc-filter-value-select"
:disabled="filter.readOnly"
@click.stop
@ -214,11 +214,14 @@ watch(
</template>
</div>
<v-btn small class="elevation-0 text-sm text-capitalize text-grey my-3" @click.stop="addFilter">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</v-btn>
<a-button small class="elevation-0 text-sm text-capitalize text-grey my-3" @click.stop="addFilter">
<div class="flex align-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiAddIcon />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</a-button>
<slot />
</div>
</template>

53
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue

@ -16,50 +16,19 @@ const applyChanges = () => {}
</script>
<template>
<v-menu offset-y eager transition="slide-y-transition">
<template #activator="{ props }">
<v-badge :value="filters.length" color="primary" dot overlap>
<v-btn
v-t="['c:filter']"
class="nc-filter-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey--text text--darken-3': filters.length,
}"
v-bind="props"
>
<MdiFilterIcon class="mr-1 text-grey" />
<a-dropdown :trigger="['click']">
<v-badge :value="filters.length" color="primary" dot overlap>
<a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<div class="flex align-center gap-1">
<MdiFilterIcon class="text-grey" />
<!-- Filter -->
<span class="text-capitalize nc-filter-menu-btn">{{ $t('activity.filter') }}</span>
<MdiMenuDownIcon class="text-grey" />
</v-btn>
</v-badge>
</div>
</a-button>
</v-badge>
<template #overlay>
<SmartsheetToolbarColumnFilter />
</template>
<SmartsheetToolbarColumnFilter>
<!-- <div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
id="col-filter-checkbox"
v-model="autoApplyFilter"
class="col-filter-checkbox"
hide-details
dense
type="checkbox"
color="grey"
>
<template #label>
<span class="grey&#45;&#45;text caption">
{{ $t('msg.info.filterAutoApply') }}
&lt;!&ndash; Auto apply &ndash;&gt;
</span>
</template>
</v-checkbox>
<v-spacer />
<v-btn v-show="!autoApplyFilter" color="primary" small class="caption ml-2" @click="applyChanges"> Apply changes </v-btn>
</div> -->
</SmartsheetToolbarColumnFilter>
</v-menu>
</a-dropdown>
</template>

9
packages/nc-gui-v2/components/smartsheet-toolbar/DeleteTable.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
import MdiDeleteIcon from '~icons/mdi/delete-outline'
</script>
<template>
<MdiDeleteIcon class="text-grey" />
</template>
<style scoped></style>

76
packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue

@ -1,4 +1,6 @@
<script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import { computed } from 'vue'
import { MetaInj } from '~/context'
interface Props {
@ -42,37 +44,55 @@ const localValue = computed({
},
},
} */
const options = computed<SelectProps['options']>(() =>
meta?.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
})),
)
const filterOption = (input: string, option: any) => {
return option.value.toLowerCase()?.includes(input.toLowerCase())
}
</script>
<template>
<v-autocomplete
ref="field"
v-model="localValue"
class="caption"
:items="meta.columns"
item-value="id"
item-text="title"
:label="$t('objects.field')"
variant="solo"
hide-details
@click.stop
>
<!-- &lt;!&ndash; @change="$emit('change')" &ndash;&gt; -->
<!-- <template #selection="{ item }"> -->
<!-- <v-icon small class="mr-1"> -->
<!-- {{ item.icon }} -->
<!-- </v-icon> -->
<!-- {{ item.title }} -->
<!-- </template> -->
<!-- <template #item="{ item }"> -->
<!-- <span :class="`caption font-weight-regular nc-fld-${item.title}`"> -->
<!-- <v-icon color="grey" small class="mr-1"> -->
<!-- {{ item.icon }} -->
<!-- </v-icon> -->
<!-- {{ item.title }} -->
<!-- </span> -->
<!-- </template> -->
</v-autocomplete>
<a-select
v-model:value="localValue"
show-search
placeholder="Select a field"
:options="options"
:filter-option="filterOption"
></a-select>
<!-- <v-autocomplete
ref="field"
v-model="localValue"
class="caption"
:items="meta.columns"
item-value="id"
item-text="title"
:label="$t('objects.field')"
variant="solo"
hide-details
@click.stop
>
&lt;!&ndash; &lt;!&ndash; @change="$emit('change')" &ndash;&gt; &ndash;&gt;
&lt;!&ndash; <template #selection="{ item }"> &ndash;&gt;
&lt;!&ndash; <v-icon small class="mr-1"> &ndash;&gt;
&lt;!&ndash; {{ item.icon }} &ndash;&gt;
&lt;!&ndash; </v-icon> &ndash;&gt;
&lt;!&ndash; {{ item.title }} &ndash;&gt;
&lt;!&ndash; </template> &ndash;&gt;
&lt;!&ndash; <template #item="{ item }"> &ndash;&gt;
&lt;!&ndash; <span :class="`caption font-weight-regular nc-fld-${item.title}`"> &ndash;&gt;
&lt;!&ndash; <v-icon color="grey" small class="mr-1"> &ndash;&gt;
&lt;!&ndash; {{ item.icon }} &ndash;&gt;
&lt;!&ndash; </v-icon> &ndash;&gt;
&lt;!&ndash; {{ item.title }} &ndash;&gt;
&lt;!&ndash; </span> &ndash;&gt;
&lt;!&ndash; </template> &ndash;&gt;
</v-autocomplete> -->
</template>
<style scoped></style>

671
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewColumns from '~/composables/useViewColumns'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline'
import MdiDragIcon from '~icons/mdi/drag'
const { fieldsOrder, coverImageField, modelValue } = defineProps<{
coverImageField?: string
@ -23,6 +25,8 @@ const isAnyFieldHidden = computed(() => {
// return meta?.fields?.some(field => field.hidden)
})
const { $e } = useNuxtApp()
const {
showSystemFields,
sortedAndFilteredFields,
@ -33,6 +37,7 @@ const {
showAll,
hideAll,
saveOrUpdate,
sortedFields,
} = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger())
watch(
@ -52,634 +57,76 @@ watch(
{ immediate: true },
)
/* import draggable from 'vuedraggable'
import { getSystemColumnsIds } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
export default {
name: 'FieldsMenu',
components: {
Draggable: draggable,
},
props: {
coverImageField: String,
groupingField: String,
isGallery: Boolean,
isKanban: Boolean,
sqlUi: [Object, Function],
meta: Object,
fieldsOrder: [Array],
value: [Object, Array],
fieldList: [Array, Object],
showSystemFields: {
type: [Boolean, Number],
default: false,
},
isLocked: Boolean,
isPublic: Boolean,
viewId: String,
},
data: () => ({
fields: [],
fieldFilter: '',
showFields: {},
fieldsOrderLoc: [],
}),
computed: {
systemColumnsIds() {
return getSystemColumnsIds(this.meta && this.meta.columns)
},
attachmentFields() {
return [
...(this.meta && this.meta.columns ? this.meta.columns.filter((f) => f.uidt === 'Attachment') : []),
{
alias: 'None',
id: null,
},
]
},
singleSelectFields() {
return [
...(this.meta && this.meta.columns ? this.meta.columns.filter((f) => f.uidt === 'SingleSelect') : []),
{
alias: 'None',
id: null,
},
]
},
coverImageFieldLoc: {
get() {
return this.coverImageField
},
set(val) {
this.$emit('update:coverImageField', val)
},
},
groupingFieldLoc: {
get() {
return this.groupingField
},
set(val) {
this.$emit('update:groupingField', val)
},
},
columnMeta() {
return this.meta && this.meta.columns
? this.meta.columns.reduce(
(o, c) => ({
...o,
[c.title]: c,
}),
{},
)
: {}
},
isAnyFieldHidden() {
return this.fields.some((f) => !(!this.showSystemFieldsLoc && this.systemColumnsIds.includes(f.fk_column_id)) && !f.show) // Object.values(this.showFields).some(v => !v)
},
showSystemFieldsLoc: {
get() {
return this.showSystemFields
},
set(v) {
this.$emit('update:showSystemFields', v)
this.showFields = this.fields.reduce((o, c) => ({ [c.title]: c.show, ...o }), {})
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
this.$e('a:fields:system-fields')
},
},
},
watch: {
async viewId(v) {
if (v) {
await this.loadFields()
}
},
fieldList(f) {
this.fieldsOrderLoc = [...f]
},
showFields: {
handler(v) {
this.$nextTick(() => {
this.$emit('input', v)
})
},
deep: true,
},
value(v) {
this.showFields = v || []
},
fieldsOrder(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.fieldsOrderLoc = n
}
this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList]
},
fieldsOrderLoc: {
handler(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.$emit('update:fieldsOrder', n)
}
},
deep: true,
},
},
created() {
this.loadFields()
this.showFields = this.value
this.fieldsOrderLoc = this.fieldsOrder && this.fieldsOrder.length ? this.fieldsOrder : [...this.fieldList]
},
methods: {
async loadFields() {
let fields = []
let order = 1
if (this.viewId) {
const data = await this.$api.dbViewColumn.list(this.viewId)
const fieldById = data.reduce(
(o, f) => ({
...o,
[f.fk_column_id]: f,
}),
{},
)
fields = this.meta.columns
.map((c) => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
icon: getUIDTIcon(c.uidt),
}))
.sort((a, b) => a.order - b.order)
} else if (this.isPublic) {
fields = this.meta.columns
}
this.fields = fields
this.$emit(
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
}),
{},
),
)
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
},
async saveOrUpdate(field, i) {
if (!this.isPublic && this._isUIAllowed('fieldsSync')) {
if (field.id) {
await this.$api.dbViewColumn.update(this.viewId, field.id, field)
} else {
this.fields[i] = await this.$api.dbViewColumn.create(this.viewId, field)
}
}
this.$emit('updated')
this.$emit(
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
}),
{},
),
)
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
this.$e('a:fields:show-hide')
},
async showAll() {
if (!this.isPublic) {
await this.$api.dbView.showAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = true
}
this.$emit('updated')
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => ((o[k] = true), o), {})
this.$e('a:fields:show-all')
},
async hideAll() {
if (!this.isPublic) {
await this.$api.dbView.hideAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = false
}
this.$emit('updated')
this.$nextTick(() => {
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => ((o[k] = false), o), {})
})
this.$e('a:fields:hide-all')
},
onMove(event) {
if (this.fields.length - 1 === event.moved.newIndex) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[event.moved.newIndex - 1].order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[1].order / 2)
} else {
this.$set(
this.fields[event.moved.newIndex],
'order',
(this.fields[event.moved.newIndex - 1].order + this.fields[event.moved.newIndex + 1].order) / 2,
)
}
this.saveOrUpdate(this.fields[event.moved.newIndex], event.moved.newIndex)
this.$e('a:fields:reorder')
},
},
} */
const onMove = (event) => {
// todo : sync with server
// if (!sortedFields?.value) return
// if (sortedFields?.value.length - 1 === event.moved.newIndex) {
// sortedFields.value[event.moved.newIndex].order = sortedFields.value[event.moved.newIndex - 1].order + 1
// } else if (event.moved.newIndex === 0) {
// sortedFields.value[event.moved.newIndex].order = sortedFields.value[1].order / 2
// } else {
// sortedFields.value[event.moved.newIndex].order =
// (sortedFields?.value[event.moved.newIndex - 1].order + sortedFields?.value[event.moved.newIndex + 1].order) / 2
// // );
// }
// saveOrUpdate(sortedFields[event.moved.newIndex], event.moved.newIndex);
$e('a:fields:reorder')
}
</script>
<template>
<v-menu>
<template #activator="{ props }">
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap v-bind="props">
<v-btn
v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey--text text--darken-3': isAnyFieldHidden,
}"
>
<a-dropdown :trigger="['click']">
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<div class="flex align-center gap-1">
<!-- <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> -->
<MdiEyeIcon class="mr-1 text-grey"></MdiEyeIcon>
<MdiEyeIcon class="text-grey"></MdiEyeIcon>
<!-- Fields -->
<span class="text-sm text-capitalize nc-fields-menu-btn">{{ $t('objects.fields') }}</span>
<MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon>
</v-btn>
</v-badge>
</template>
<v-list density="compact" class="pt-0" min-width="280" @click.stop>
<div class="nc-fields-list py-1">
<!-- <Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)"> -->
<v-list-item v-for="(field, i) in filteredFieldList" :key="field.id" dense>
<input
:id="`show-field-${field.id}`"
v-model="field.show"
type="checkbox"
class="mt-0 pt-0"
@click.stop
@change="saveOrUpdate(field, i)"
/>
<!-- @change="saveOrUpdate(field, i)"> -->
<!-- <template #label>
&lt;!&ndash; <v-icon small class="mr-1">
{{ field.icon }}
</v-icon> &ndash;&gt;
<span class="caption">{{ field.title }}</span>
</template> -->
<!-- </input> -->
<label :for="`show-field-${field.id}`" class="ml-2 text-sm">{{ field.title }}</label>
<v-spacer />
<!-- <v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"> mdi-drag </v-icon> -->
</v-list-item>
<!-- </Draggable> -->
</div>
<v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense>
<!--
show_system_fields
<v-checkbox v-model="showSystemFields" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption text-sm">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox> -->
<input :id="`${activeView?.id}-show-system-fields`" v-model="showSystemFields" type="checkbox" />
<label :for="`${activeView.id}-show-system-fields`" class="caption text-sm ml-2">{{
$t('activity.showSystemFields')
}}</label>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey--text text-sm text-capitalize" @click.stop="showAll">
<!-- Show All -->
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey--text text-sm text-capitalize" @click.stop="hideAll">
<!-- Hide All -->
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list>
<!--
<v-list dense class="pt-0" min-width="280" @click.stop>
<template v-if="isGallery && _isUIAllowed('updateCoverImage')">
<div class="pa-2">
<v-select
v-model="coverImageFieldLoc"
label="Cover Image"
class="caption field-caption"
dense
outlined
:items="attachmentFields"
item-text="alias"
item-value="id"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-image </v-icon>
</template>
</v-select>
</div>
<v-divider />
</template>
<template v-if="isKanban">
<div class="pa-2">
<v-select
v-model="groupingFieldLoc"
label="Grouping Field"
class="caption field-caption"
dense
outlined
:items="singleSelectFields"
item-text="alias"
item-value="title"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-select-group </v-icon>
</a-button>
</v-badge>
<template #overlay>
<div class="pt-0 min-w-[280px] bg-white shadow" @click.stop>
<div class="nc-fields-list py-1">
<Draggable :list="sortedFields" @change="onMove($event)">
<template #item="{ element: field }">
<div :key="field.id" class="px-2 py-1 flex" @click.stop>
<a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, i)">
<span class="text-xs">{{ field.title }}</span>
</a-checkbox>
<div class="flex-1" />
<MdiDragIcon class="cursor-move" />
</div>
</template>
</v-select>
</Draggable>
</div>
<v-divider />
</template>
<v-list-item dense class="">
<v-text-field
v-model="fieldFilter"
dense
flat
class="caption mt-3 mb-2"
color="grey"
:placeholder="$t('placeholder.searchFields')"
hide-details
@click.stop
>
&lt;!&ndash; <template v-slot:prepend-inner>
<v-icon small color="grey" class="mt-2">
mdi-magnify
</v-icon>
</template> &ndash;&gt;
</v-text-field>
</v-list-item>
<div class="nc-fields-list py-1">
&lt;!&ndash; <Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)"> &ndash;&gt;
<template v-for="(field, i) in fields">
<v-list-item
v-show="
(!fieldFilter || (field.title || '').toLowerCase().includes(fieldFilter.toLowerCase())) &&
!(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id))
"
:key="field.id"
dense
>
<v-checkbox v-model="field.show" class="mt-0 pt-0" dense hide-details @click.stop @change="saveOrUpdate(field, i)">
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"> mdi-drag </v-icon>
</v-list-item>
</template>
&lt;!&ndash; </Draggable> &ndash;&gt;
</div>
<v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense>
<v-checkbox v-model="showSystemFieldsLoc" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="showAll">
&lt;!&ndash; Show All &ndash;&gt;
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll">
&lt;!&ndash; Hide All &ndash;&gt;
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list> -->
</v-menu>
<!-- <v-menu offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<v-btn
v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey&#45;&#45;text text&#45;&#45;darken-3': isAnyFieldHidden,
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon>
&lt;!&ndash; Fields &ndash;&gt;
{{ $t('objects.fields') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
</v-badge>
</template>
<v-divider class="my-2" />
<v-list dense class="pt-0" min-width="280" @click.stop>
<template v-if="isGallery && _isUIAllowed('updateCoverImage')">
<div class="pa-2">
<v-select
v-model="coverImageFieldLoc"
label="Cover Image"
class="caption field-caption"
dense
outlined
:items="attachmentFields"
item-text="alias"
item-value="id"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-image </v-icon>
</template>
</v-select>
<div class="p-2 py-1 flex" @click.stop>
<a-checkbox v-model:checked="showSystemFields">
<span class="text-xs"> {{ $t('activity.showSystemFields') }}</span>
</a-checkbox>
</div>
<v-divider />
</template>
<template v-if="isKanban">
<div class="pa-2">
<v-select
v-model="groupingFieldLoc"
label="Grouping Field"
class="caption field-caption"
dense
outlined
:items="singleSelectFields"
item-text="alias"
item-value="title"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-select-group </v-icon>
</template>
</v-select>
<div class="p-2 flex gap-2" @click.stop>
<a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="showAll">
<!-- Show All -->
{{ $t('general.showAll') }}
</a-button>
<a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="hideAll">
<!-- Hide All -->
{{ $t('general.hideAll') }}
</a-button>
</div>
<v-divider />
</template>
<v-list-item dense class="">
<v-text-field
v-model="fieldFilter"
dense
flat
class="caption mt-3 mb-2"
color="grey"
:placeholder="$t('placeholder.searchFields')"
hide-details
@click.stop
>
&lt;!&ndash; <template v-slot:prepend-inner>
<v-icon small color="grey" class="mt-2">
mdi-magnify
</v-icon>
</template> &ndash;&gt;
</v-text-field>
</v-list-item>
<div class="nc-fields-list py-1">
<Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)">
<template v-for="(field, i) in fields">
<v-list-item
v-show="
(!fieldFilter || (field.title || '').toLowerCase().includes(fieldFilter.toLowerCase())) &&
!(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id))
"
:key="field.id"
dense
>
<v-checkbox v-model="field.show" class="mt-0 pt-0" dense hide-details @click.stop @change="saveOrUpdate(field, i)">
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`">
mdi-drag
</v-icon>
</v-list-item>
</template>
</Draggable>
</div>
<v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense>
<v-checkbox v-model="showSystemFieldsLoc" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="showAll">
&lt;!&ndash; Show All &ndash;&gt;
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll">
&lt;!&ndash; Hide All &ndash;&gt;
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list>
</v-menu> -->
</template>
</a-dropdown>
</template>
<style scoped lang="scss">
/*::v-deep {
.v-list-item {
min-height: 30px;
}
.v-input--checkbox .v-icon {
font-size: 12px !important;
}
.field-caption {
.v-input__append-inner {
margin-top: 4px !important;
}
.v-input__slot {
min-height: 25px !important;
}
&.v-input input {
max-height: 20px !important;
}
.field-icon {
margin-top: 2px;
}
}
:deep(.ant-checkbox-input) {
transform: scale(0.7);
}
.drag-icon {
cursor: all-scroll; !*cursor: grab;*!
}
.nc-fields-list {
height: auto;
max-height: 500px;
overflow-y: auto;
}*/
</style>

130
packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue

@ -1,5 +1,10 @@
<script lang="ts" setup>
import { computed } from '@vue/reactivity'
import { useToast } from 'vue-toastification'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
import MdiCheckIcon from '~icons/mdi/check-bold'
interface Props {
modelValue?: LockType
@ -32,6 +37,18 @@ function changeLockType(type: LockType) {
toast.success(`Successfully Switched to ${type} view`, { timeout: 3000 })
}
const Icon = computed(() => {
switch (vModel.value) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
return MdiLockOutlineIcon
case LockType.Collaborative:
default:
return MdiAccountGroupIcon
}
})
</script>
<script lang="ts">
@ -41,68 +58,57 @@ export default {
</script>
<template>
<v-menu offset-y max-width="350">
<template #activator="{ props: menuProps }">
<v-icon v-if="vModel === LockType.Locked" small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick">
mdi-lock-outline
</v-icon>
<v-icon v-else-if="vModel === LockType.Personal" small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick">
mdi-account
</v-icon>
<v-icon v-else small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick"> mdi-account-group-outline </v-icon>
<a-dropdown max-width="350" :trigger="['click']">
<Icon class="mx-1 nc-view-lock-menu text-grey"> mdi-lock-outline </Icon>
<template #overlay>
<div class="min-w-[350px] max-w-[500px] shadow bg-white">
<div>
<div class="nc-menu-item">
<MdiCheckIcon v-if="!vModel || vModel === LockType.Collaborative" />
<span v-else />
<div>
<MdiAccountGroupIcon />
Collaborative view
<div class="nc-subtitle">Collaborators with edit permissions or higher can change the view configuration.</div>
</div>
</div>
<div class="nc-menu-item">
<MdiCheckIcon v-if="vModel === LockType.Locked" />
<span v-else />
<div>
<MdiLockOutlineIcon />
Locked View
<div class="nc-subtitle">No one can edit the view configuration until it is unlocked.</div>
</div>
</div>
<div class="nc-menu-item">
<MdiCheckIcon v-if="vModel === LockType.Personal" />
<span v-else />
<div>
<MdiAccountIcon />
Personal view
<div class="nc-subtitle">
Only you can edit the view configuration. Other collaborators personal views are hidden by default.
</div>
</div>
</div>
</div>
</div>
</template>
<v-list maxc-width="350">
<v-list-item two-line class="pb-4" @click="changeLockType(LockType.Collaborative)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="!vModel || vModel === LockType.Collaborative" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content class="pb-1">
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-account-group </v-icon>
Collaborative view
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
Collaborators with edit permissions or higher can change the view configuration.
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item two-line class="pb-4" @click="changeLockType(LockType.Locked)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="vModel === LockType.Locked" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content class="pb-1">
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-lock </v-icon>
Locked View
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
No one can edit the view configuration until it is unlocked.
</v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Locked view.</span>
</v-list-item-content>
</v-list-item>
<v-list-item three-line @click="changeLockType(LockType.Personal)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="vModel === LockType.Personal" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-account </v-icon>
Personal view
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
Only you can edit the view configuration. Other collaborators personal views are hidden by default.
</v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Coming soon.</span>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</a-dropdown>
</template>
<style scoped></style>
<style scoped>
.nc-menu-item {
@apply grid grid-cols-[30px,auto] gap-2 p-4;
}
.nc-menu-option > :first-child {
@apply align-self-center;
}
.nc-subtitle {
@apply font-size-sm font-weight-light;
}
</style>

434
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -1,324 +1,126 @@
<script>
import FileSaver from 'file-saver'
<script lang="ts" setup>
import { ExportTypes } from 'nocodb-sdk'
import { NOCO } from '~/lib/constants'
import DropOrSelectFileModal from '~/components/import/DropOrSelectFileModal'
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/ColumnMappingModal'
import CSVTemplateAdapter from '~/components/import/templateParsers/CSVTemplateAdapter'
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes'
import WebhookModal from '~/components/project/tableTabs/webhook/WebhookModal'
import WebhookSlider from '~/components/project/tableTabs/webhook/WebhookSlider'
export default {
name: 'ExportImport',
components: {
WebhookSlider,
ColumnMappingModal,
DropOrSelectFileModal,
},
props: {
meta: Object,
nodes: Object,
selectedView: Object,
publicViewId: String,
queryParams: Object,
isView: Boolean,
reqPayload: Object,
},
emits: ['reload', 'showAdditionalFeatOverlay'],
data() {
return {
importModal: false,
columnMappingModal: false,
parsedCsv: {},
webhookModal: false,
}
},
methods: {
async onCsvFileSelection(file) {
const reader = new FileReader()
reader.onload = async (e) => {
const templateGenerator = new CSVTemplateAdapter(file.name, e.target.result)
await templateGenerator.init()
templateGenerator.parseData()
this.parsedCsv.columns = templateGenerator.getColumns()
this.parsedCsv.data = templateGenerator.getData()
this.columnMappingModal = true
this.importModal = false
}
reader.readAsText(file)
},
async extractCsvData() {
return Promise.all(
this.data.map(async (r) => {
const row = {}
for (const col of this.availableColumns) {
if (col.virtual) {
let prop, cn
if (col.mm || (col.lk && col.lk.type === 'mm')) {
const tn = col.mm ? col.mm.rtn : col.lk.ltn
const title = col.mm ? col.mm._rtn : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn,
})
prop = `${title}MMList`
cn = col.lk
? col.lk._lcn
: (
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) ||
this.$store.state.meta.metas[tn].columns.find((c) => c.pk) ||
{}
).title
row[col.title] = r.row[prop] && r.row[prop].map((r) => cn && r[cn])
} else if (col.hm || (col.lk && col.lk.type === 'hm')) {
const tn = col.hm ? col.hm.table_name : col.lk.ltn
const title = col.hm ? col.hm.title : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn,
})
prop = `${title}List`
cn = col.lk
? col.lk._lcn
: (
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) ||
this.$store.state.meta.metas[tn].columns.find((c) => c.pk)
).title
row[col.title] = r.row[prop] && r.row[prop].map((r) => cn && r[cn])
} else if (col.bt || (col.lk && col.lk.type === 'bt')) {
const tn = col.bt ? col.bt.rtn : col.lk.ltn
const title = col.bt ? col.bt._rtn : col.lk._ltn
await this.$store.dispatch('meta/ActLoadMeta', {
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
tn,
})
prop = `${title}Read`
cn = col.lk
? col.lk._lcn
: (
this.$store.state.meta.metas[tn].columns.find((c) => c.pv) ||
this.$store.state.meta.metas[tn].columns.find((c) => c.pk) ||
{}
).title
row[col.title] = r.row[prop] && r.row[prop] && cn && r.row[prop][cn]
} else {
row[col.title] = r.row[col.title]
}
} else if (col.uidt === 'Attachment') {
let data = []
try {
if (typeof r.row[col.title] === 'string') {
data = JSON.parse(r.row[col.title])
} else if (r.row[col.title]) {
data = r.row[col.title]
}
} catch {}
row[col.title] = (data || []).map((a) => `${a.title}(${a.url})`)
} else {
row[col.title] = r.row[col.title]
}
}
return row
}),
)
},
async exportCsv() {
let offset = 0
let c = 1
try {
while (!isNaN(offset) && offset > -1) {
let res
if (this.publicViewId) {
res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob',
query: {
fields:
this.queryParams &&
this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter((c) => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(
this.reqPayload &&
this.reqPayload.sorts &&
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction,
fk_column_id,
})),
),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password,
},
})
} else {
res = await this.$api.dbViewRow.export(
NOCO,
this.projectName,
this.meta.title,
this.selectedView.title,
ExportTypes.CSV,
{
responseType: 'blob',
query: {
offset,
},
},
)
}
const { data } = res
offset = +res.headers['nc-export-offset']
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${this.meta.title}_exported_${c++}.csv`)
if (offset > -1) {
this.$toast.info('Downloading more files').goAway(3000)
} else {
this.$toast.success('Successfully exported all table data').goAway(3000)
}
}
} catch (e) {
console.log(e)
this.$toast.error(e.message).goAway(3000)
import { useToast } from 'vue-toastification'
import FileSaver from 'file-saver'
import { useNuxtApp } from '#app'
import useProject from '~/composables/useProject'
import { ActiveViewInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiFlashIcon from '~icons/mdi/flash-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiDownloadIcon from '~icons/mdi/download-outline'
import MdiUploadIcon from '~icons/mdi/upload-outline'
import MdiHookIcon from '~icons/mdi/hook'
import MdiViewListIcon from '~icons/mdi/view-list-outline'
// todo : replace with inject
const publicViewId = null
const { project } = useProject()
const { $api } = useNuxtApp()
const toast = useToast()
const meta = inject(MetaInj)
const selectedView = inject(ActiveViewInj)
const exportCsv = async () => {
let offset = 0
let c = 1
try {
while (!isNaN(offset) && offset > -1) {
let res
if (publicViewId) {
/* res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob',
query: {
fields:
this.queryParams &&
this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(
this.reqPayload &&
this.reqPayload.sorts &&
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction,
fk_column_id,
}))
),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password,
},
});
*/
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
ExportTypes.CSV,
{
responseType: 'blob',
query: {
offset,
},
} as any,
)
}
},
async importData(columnMappings) {
try {
const data = this.parsedCsv.data
for (let i = 0, progress = 0; i < data.length; i += 500) {
const batchData = data.slice(i, i + 500).map((row) =>
columnMappings.reduce((res, col) => {
// todo: parse data
if (col.enabled && col.destCn) {
const v = this.meta && this.meta.columns.find((c) => c.title === col.destCn)
let input = row[col.sourceCn]
// parse potential boolean values
if (v.uidt === UITypes.Checkbox) {
input = input.replace(/["']/g, '').toLowerCase().trim()
if (input === 'false' || input === 'no' || input === 'n') {
input = '0'
} else if (input === 'true' || input === 'yes' || input === 'y') {
input = '1'
}
} else if (v.uidt === UITypes.Number) {
if (input === '') {
input = null
}
} else if (v.uidt === UITypes.SingleSelect || v.uidt === UITypes.MultiSelect) {
if (input === '') {
input = null
}
}
res[col.destCn] = input
}
return res
}, {}),
)
await this.$api.dbTableRow.bulkCreate(NOCO, this.projectName, this.meta.title, batchData)
progress += batchData.length
this.$store.commit('loader/MutMessage', `Importing data : ${progress}/${data.length}`)
this.$store.commit('loader/MutProgress', Math.round((100 * progress) / data.length))
}
this.columnMappingModal = false
this.$store.commit('loader/MutClear')
this.$emit('reload')
this.$toast.success('Successfully imported table data').goAway(3000)
} catch (e) {
this.$toast.error(e.message).goAway(3000)
const { data } = res
offset = +res.headers['nc-export-offset']
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
if (offset > -1) {
toast.info('Downloading more files')
} else {
toast.success('Successfully exported all table data')
}
},
},
}
} catch (e) {
toast.error(extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<div>
<v-menu open-on-hover bottom offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-btn
v-t="['c:actions']"
outlined
class="nc-actions-menu-btn caption px-2 nc-remove-border font-weight-medium"
small
text
v-on="on"
>
<v-icon small color="#777"> mdi-flash-outline </v-icon>
<!-- More -->
{{ $t('general.more') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-t="['a:actions:download-csv']" dense @click="exportCsv">
<v-list-item-title>
<v-icon small class="mr-1"> mdi-download-outline </v-icon>
<span class="caption">
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="_isUIAllowed('csvImport') && !isView" v-t="['a:actions:upload-csv']" dense @click="importModal = true">
<v-list-item-title>
<v-icon small class="mr-1" color=""> mdi-upload-outline </v-icon>
<span class="caption">
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</span>
<span class="caption grey--text">(<x-icon small color="grey lighten-2"> mdi-alpha </x-icon> version)</span>
</v-list-item-title>
</v-list-item>
<v-list-item
v-if="_isUIAllowed('SharedViewList') && !isView"
v-t="['a:actions:shared-view-list']"
dense
@click="$emit('showAdditionalFeatOverlay', 'shared-views')"
>
<v-list-item-title>
<v-icon small class="mr-1" color=""> mdi-view-list-outline </v-icon>
<span class="caption">
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="_isUIAllowed('webhook') && !isView" v-t="['c:actions:webhook']" dense @click="webhookModal = true">
<v-list-item-title>
<v-icon small class="mr-1" color=""> mdi-hook </v-icon>
<span class="caption"> Webhooks </span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<DropOrSelectFileModal v-model="importModal" accept=".csv" text="CSV" @file="onCsvFileSelection" />
<ColumnMappingModal
v-if="columnMappingModal && meta"
v-model="columnMappingModal"
:meta="meta"
:import-data-columns="parsedCsv.columns"
:parsed-csv="parsedCsv"
@import="importData"
/>
<!-- <webhook-modal v-if="webhookModal" v-model="webhookModal" :meta="meta" /> -->
<WebhookSlider v-model="webhookModal" :meta="meta" />
</div>
<a-dropdown>
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 align-center">
<MdiFlashIcon class="text-grey" />
<!-- More -->
{{ $t('general.more') }}
<MdiMenuDownIcon class="text-grey" />
</div>
</a-button>
<template #overlay>
<div class="bg-white shadow">
<div>
<div class="nc-menu-item" @click.stop="exportCsv">
<MdiDownloadIcon />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
<div class="nc-menu-item" @click.stop>
<MdiUploadIcon />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</div>
<div class="nc-menu-item" @click.stop>
<MdiViewListIcon />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</div>
<div class="nc-menu-item" @click.stop>
<MdiHookIcon />
<!-- todo: i18n -->
Webhook
</div>
</div>
</div>
</template>
</a-dropdown>
</template>
<style scoped></style>

11
packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue

@ -0,0 +1,11 @@
<script setup lang="ts">
import { ReloadViewDataHookInj } from '~/context'
import MdiReloadIcon from '~icons/mdi/reload'
const reloadTri = inject(ReloadViewDataHookInj)
</script>
<template>
<MdiReloadIcon class="text-grey" @click="reloadTri.trigger()" />
</template>
<style scoped></style>

37
packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { MetaInj } from '~/context'
const { modelValue, field } = defineProps<{
modelValue?: string
field?: any
}>()
const emit = defineEmits(['update:modelValue', 'update:field'])
const localValue = computed({
get: () => modelValue,
set: (val) => emit('update:modelValue', val),
})
const localField = computed({
get: () => field,
set: (val) => emit('update:field', val),
})
const meta = inject(MetaInj)
const columns = computed(() =>
meta?.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
})),
)
</script>
<template>
<a-input v-model:value="localValue" size="small" class="max-w-[250px]" placeholder="Filter query">
<template #addonBefore>
<a-select v-model:value="localField" :options="columns" style="width: 80px" class="!text-xs" size="small" />
</template>
</a-input>
</template>
<style scoped></style>

18
packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue

@ -0,0 +1,18 @@
<script lang="ts" setup>
import MdiOpenInNew from '~icons/mdi/open-in-new'
const { isUIAllowed } = useUIPermission()
</script>
<template>
<div>
<a-button v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn" size="small">
<div class="flex align-center gap-1">
<MdiOpenInNew class="text-grey" />
<!-- Share View -->
{{ $t('activity.shareView') }}
</div>
</a-button>
</div>
</template>
<style scoped />

113
packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue

@ -6,6 +6,7 @@ import useViewSorts from '~/composables/useViewSorts'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiSortIcon from '~icons/mdi/sort'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
@ -26,68 +27,64 @@ watch(
</script>
<template>
<v-menu offset-y transition="slide-y-transition">
<template #activator="{ props }">
<v-badge :value="sorts && sorts.length" color="primary" dot overlap>
<v-btn
v-t="['c:sort']"
class="nc-sort-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
small
text
outlined
:class="{
'primary lighten-5 grey&#45;&#45;text text&#45;&#45;darken-3': sorts && sorts.length,
}"
v-bind="props"
>
<MdiSortIcon class="mr-1 text-grey" />
<a-dropdown offset-y class="" :trigger="['click']">
<v-badge :value="sorts && sorts.length" color="primary" dot overlap>
<a-button v-t="['c:sort']" size="small" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex align-center gap-1">
<MdiSortIcon class="text-grey" />
<!-- Sort -->
<span class="text-capitalize nc-sort-menu-btn">{{ $t('activity.sort') }}</span>
<MdiMenuDownIcon class="text-grey" />
</v-btn>
</v-badge>
</template>
<div class="backgroundColor pa-2 menu-filter-dropdown bg-background min-w-[400px]">
<div class="sort-grid" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> -->
<MdiDeleteIcon
class="nc-sort-item-remove-btn text-grey align-self-center"
small
@click.stop="deleteSort(sort, i)"
></MdiDeleteIcon>
<FieldListAutoCompleteDropdown
v-model="sort.fk_column_id"
class="caption nc-sort-field-select"
:columns="columns"
@click.stop
@update:model-value="saveOrUpdate(sort, i)"
/>
<v-select
v-model="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="['asc', 'desc']"
:label="$t('labels.operation')"
density="compact"
variant="solo"
hide-details
@click.stop
@update:model-value="saveOrUpdate(sort, i)"
/>
<!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item.text }}</span> -->
<!-- </template> -->
<!-- </v-select> -->
</template>
</div>
</a-button>
</v-badge>
<template #overlay>
<div class="bg-white shadow p-2 menu-filter-dropdown min-w-[400px]">
<div class="sort-grid" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> -->
<MdiDeleteIcon
class="nc-sort-item-remove-btn text-grey align-self-center"
small
@click.stop="deleteSort(sort, i)"
></MdiDeleteIcon>
<FieldListAutoCompleteDropdown
v-model="sort.fk_column_id"
class="caption nc-sort-field-select"
:columns="columns"
@click.stop
@update:model-value="saveOrUpdate(sort, i)"
/>
<a-select
v-model:value="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="[
{ text: 'asc', value: 'asc' },
{ text: 'desc', value: 'desc' },
]"
:label="$t('labels.operation')"
density="compact"
variant="solo"
hide-details
@click.stop
@update:model-value="saveOrUpdate(sort, i)"
/>
<!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item.text }}</span> -->
<!-- </template> -->
<!-- </v-select> -->
</template>
</div>
<a-button size="small" class="text-grey text-capitalize text-sm my-3" @click.stop="addSort">
<div class="flex gap-1 align-center">
<MdiAddIcon />
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</div>
</a-button>
</div>
<v-btn small class="elevation-0 text-grey text-capitalize text-sm my-3" @click.stop="addSort">
<!-- todo: <v-icon small color="grey"> mdi-plus </v-icon> -->
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</v-btn>
</div>
</v-menu>
</template>
</a-dropdown>
</template>
<style scoped>

14
packages/nc-gui-v2/components/smartsheet-toolbar/ToggleDrawer.vue

@ -0,0 +1,14 @@
<script setup lang="ts">
import { ReloadViewDataHookInj } from '~/context'
import MdiDoorOpenIcon from '~icons/mdi/door-open'
import MdiDoorClosedIcon from '~icons/mdi/door-closed'
const navDrawerOpened = ref(false)
const Icon = computed(() => (navDrawerOpened.value ? MdiDoorOpenIcon : MdiDoorClosedIcon))
</script>
<template>
<Icon class="text-grey" @click="navDrawerOpened = !navDrawerOpened" />
</template>
<style scoped></style>

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

@ -50,9 +50,9 @@ onKeyStroke(['Enter'], (e) => {
})
watch(
[() => meta?.value?.id, () => view?.value?.id],
async (n: any, o: any) => {
if (meta?.value && view?.value) {
() => view?.value?.id,
async (n?: string, o?: string) => {
if (n && n !== o) {
await loadData()
}
},
@ -65,129 +65,131 @@ defineExpose({
</script>
<template>
<div class="nc-grid-wrapper">
<table class="xc-row-table nc-grid backgroundColorDefault">
<thead>
<tr>
<th>#</th>
<th v-for="col in fields" :key="col.title">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
</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.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
>
<!-- @contextmenu=" -->
<!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) -->
<!-- " -->
<!-- > -->
<!-- <virtual-cell -->
<!-- v-if="isVirtualCol(columnObj)" -->
<!-- :password="password" -->
<!-- :is-public="isPublicView" -->
<!-- :metas="metas" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :row="rowObj" -->
<!-- :nodes="nodes" -->
<!-- :meta="meta" -->
<!-- :api="api" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :is-new="rowMeta.new" -->
<!-- v-on="$listeners" -->
<!-- @updateCol=" -->
<!-- (...args) => -->
<!-- updateCol( -->
<!-- ...args, -->
<!-- columnObj.bt -->
<!-- && meta.columns.find( -->
<!-- (c) => c.column_name === columnObj.bt.column_name, -->
<!-- ), -->
<!-- col, -->
<!-- row, -->
<!-- ) -->
<!-- " -->
<!-- @saveRow="onCellValueChange(col, row, columnObj, true)" -->
<!-- /> -->
<!-- <editable-cell -->
<!-- v-else-if=" -->
<!-- ((isPkAvail || rowMeta.new) -->
<!-- && !isView -->
<!-- && !isLocked -->
<!-- && !isPublicView -->
<!-- && editEnabled.col === col -->
<!-- && editEnabled.row === row) -->
<!-- || enableEditable(columnObj) -->
<!-- " -->
<!-- v-model="rowObj[columnObj.title]" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :is-locked="isLocked" -->
<!-- :is-public="isPublicView" -->
<!-- :view-id="viewId" -->
<!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" -->
<!-- @cancel="editEnabled = {}" -->
<!-- @update="onCellValueChange(col, row, columnObj, false)" -->
<!-- @blur="onCellValueChange(col, row, columnObj, true)" -->
<!-- @input="unsaved = true" -->
<!-- @navigateToNext="navigateToNext" -->
<!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<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)"
/>
<!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :selected="selected.col === col && selected.row === row" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :value="rowObj[columnObj.title]" -->
<!-- :sql-ui="sqlUi" -->
<!-- @enableedit=" -->
<!-- makeSelected(col, row); -->
<!-- makeEditable(col, row, columnObj.ai, rowMeta); -->
<!-- " -->
<!-- /> -->
</td>
</tr>
</tbody>
</table>
<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">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
</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.title"
@click="selectCell(rowIndex, colIndex)"
@dblclick="editEnabled = true"
>
<!-- @contextmenu=" -->
<!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) -->
<!-- " -->
<!-- > -->
<!-- <virtual-cell -->
<!-- v-if="isVirtualCol(columnObj)" -->
<!-- :password="password" -->
<!-- :is-public="isPublicView" -->
<!-- :metas="metas" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :row="rowObj" -->
<!-- :nodes="nodes" -->
<!-- :meta="meta" -->
<!-- :api="api" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :is-new="rowMeta.new" -->
<!-- v-on="$listeners" -->
<!-- @updateCol=" -->
<!-- (...args) => -->
<!-- updateCol( -->
<!-- ...args, -->
<!-- columnObj.bt -->
<!-- && meta.columns.find( -->
<!-- (c) => c.column_name === columnObj.bt.column_name, -->
<!-- ), -->
<!-- col, -->
<!-- row, -->
<!-- ) -->
<!-- " -->
<!-- @saveRow="onCellValueChange(col, row, columnObj, true)" -->
<!-- /> -->
<!-- <editable-cell -->
<!-- v-else-if=" -->
<!-- ((isPkAvail || rowMeta.new) -->
<!-- && !isView -->
<!-- && !isLocked -->
<!-- && !isPublicView -->
<!-- && editEnabled.col === col -->
<!-- && editEnabled.row === row) -->
<!-- || enableEditable(columnObj) -->
<!-- " -->
<!-- v-model="rowObj[columnObj.title]" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :active="selected.col === col && selected.row === row" -->
<!-- :sql-ui="sqlUi" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :is-locked="isLocked" -->
<!-- :is-public="isPublicView" -->
<!-- :view-id="viewId" -->
<!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" -->
<!-- @cancel="editEnabled = {}" -->
<!-- @update="onCellValueChange(col, row, columnObj, false)" -->
<!-- @blur="onCellValueChange(col, row, columnObj, true)" -->
<!-- @input="unsaved = true" -->
<!-- @navigateToNext="navigateToNext" -->
<!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<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)"
/>
<!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :selected="selected.col === col && selected.row === row" -->
<!-- :is-locked="isLocked" -->
<!-- :column="columnObj" -->
<!-- :meta="meta" -->
<!-- :db-alias="nodes.dbAlias" -->
<!-- :value="rowObj[columnObj.title]" -->
<!-- :sql-ui="sqlUi" -->
<!-- @enableedit=" -->
<!-- makeSelected(col, row); -->
<!-- makeEditable(col, row, columnObj.ai, rowMeta); -->
<!-- " -->
<!-- /> -->
</td>
</tr>
</tbody>
</table>
</div>
<SmartsheetPagination />
</div>
<SmartsheetPagination />
</template>
<style scoped lang="scss">

20
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -48,7 +48,7 @@ export default {
<div class="d-flex align-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<v-spacer />
<v-pagination
<!-- <v-pagination
v-if="count !== Infinity"
v-model="page"
style="max-width: 100%"
@ -56,6 +56,17 @@ export default {
:total-visible="8"
color="primary lighten-2"
class="nc-pagination"
/> -->
<a-pagination
v-if="count !== Infinity"
v-model:current="page"
size="small"
class="!text-xs !m-1"
:total="count"
:page-size="size"
show-less-items
:show-size-changer="false"
/>
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px">
<span class="caption" style="white-space: nowrap"> Change page:</span>
@ -79,3 +90,10 @@ export default {
<v-spacer />
</div>
</template>
<style scoped>
:deep(.ant-pagination-item a) {
line-height: 21px !important;
@apply text-sm;
}
</style>

26
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -1,11 +1,31 @@
<script setup lang="ts"></script>
<template>
<v-toolbar dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom" style="z-index: 7">
<div dense class="nc-table-toolbar w-100 p-1 flex gap-1 align-center" style="z-index: 7">
<SmartsheetToolbarSearchData class="flex-shrink" />
<SmartsheetToolbarFieldsMenu :show-system-fields="false" />
<SmartsheetToolbarColumnFilterMenu />
<SmartsheetToolbarSortListMenu />
</v-toolbar>
<SmartsheetToolbarShareView />
<SmartsheetToolbarMoreActions />
<div class="flex-1" />
<SmartsheetToolbarLockMenu />
<div class="dot" />
<SmartsheetToolbarReload />
<div class="dot" />
<SmartsheetToolbarAddRow />
<div class="dot" />
<SmartsheetToolbarDeleteTable />
<div class="dot" />
<SmartsheetToolbarToggleDrawer class="mr-2" />
</div>
</template>
<style scoped></style>
<style scoped>
:deep(.nc-toolbar-btn) {
@apply border-0 !text-xs font-semibold px-2;
}
.dot {
@apply w-[3px] h-[3px] bg-gray-300 mx-1 rounded-full;
}
</style>

43
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -1,25 +1,22 @@
<script setup lang="ts">
import { useEventBus } from '@vueuse/core'
import type { ColumnType, FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import type { ColumnType, ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import { computed, onMounted, provide, watch } from '#imports'
import { computed, inject, onMounted, provide, watch, watchEffect } from '#imports'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, TabMetaInj } from '~/context'
import useMetas from '~/composables/useMetas'
const { tabMeta } = defineProps({
tabMeta: Object,
})
const { getMeta, metas } = useMetas()
const activeView = ref<GridType | FormType | KanbanType | GalleryType>()
const activeView = ref<ViewType>()
const el = ref<any>()
const fields = ref<ColumnType[]>([])
const meta = computed(() => metas.value?.[tabMeta?.id])
const tabMeta = inject(TabMetaInj)
const meta = computed(() => metas.value?.[tabMeta?.value?.id as string])
onMounted(async () => {
await getMeta(tabMeta?.id)
watchEffect(async () => {
await getMeta(tabMeta?.value?.id as string)
})
const reloadEventHook = createEventHook<void>()
@ -40,17 +37,19 @@ watch(
</script>
<template>
<div class="overflow-auto">
<SmartsheetToolbar />
<template v-if="meta">
<div class="d-flex">
<div v-if="activeView" class="flex-grow-1 min-w-0">
<SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" />
<SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" />
<div class="nc-container flex h-full">
<div class="flex flex-col h-full flex-1 min-w-0">
<SmartsheetToolbar />
<template v-if="meta">
<div class="flex flex-1 min-h-0">
<div v-if="activeView" class="h-full flex-grow min-w-0 min-h-0">
<SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" />
<SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" />
</div>
<SmartsheetSidebar />
</div>
<SmartsheetSidebar />
</div>
</template>
</template>
</div>
</div>
</template>

1
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -7,6 +7,7 @@ import useBelongsTo from '~/composables/useBelongsTo'
const column = inject(ColumnInj)
const value = inject('value')
const active = false
const localState = null
const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType)
await loadParentMeta()

1
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -6,6 +6,7 @@ import useManyToMany from '~/composables/useManyToMany'
const column = inject(ColumnInj)
const value = inject('value')
const active = false
const isLocked = false
const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType)
await loadChildMeta()

79
packages/nc-gui-v2/composables/useTabs.ts

@ -1,7 +1,15 @@
import type { WritableComputedRef } from '@vue/reactivity'
import { useState } from '#app'
import useProject from '~/composables/useProject'
enum TabType {
TABLE = 'table',
VIEW = 'view',
AUTH = 'auth',
}
export interface TabItem {
type: 'table' | 'view' | 'auth'
type: TabType
title: string
id?: string
}
@ -15,30 +23,79 @@ function getPredicate(key: Partial<TabItem>) {
export default () => {
const tabs = useState<TabItem[]>('tabs', () => [])
const activeTab = useState<number>('activeTab', () => 0)
// const activeTab = useState<number>('activeTab', () => 0)
const route = useRoute()
const router = useRouter()
const { tables } = useProject()
const activeTabIndex: WritableComputedRef<number> = computed({
get() {
console.log(route?.name)
if ((route?.name as string)?.startsWith('nc-projectId-index-index-type-title-viewTitle') && tables?.value?.length) {
const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string }
const id = tables?.value?.find((t) => t.title === tab.title)?.id
tab.id = id as string
let index = tabs.value.findIndex((t) => t.id === tab.id)
if (index === -1) {
tabs.value.push(tab as TabItem)
index = tabs.value.length - 1
}
return index
} else if ((route?.name as string)?.startsWith('nc-projectId-index-index-auth')) {
return tabs.value.findIndex((t) => t.type === 'auth')
}
return -1
},
set(index: number) {
if (index === -1) {
router.push(`/nc/${route.params.projectId}`)
} else {
const tab = tabs.value[index]
if (!tab) {
return
}
if (tab.type === TabType.TABLE) {
router.push(`/nc/${route.params.projectId}/table/${tab?.title}`)
} else if (tab.type === TabType.VIEW) {
router.push(`/nc/${route.params.projectId}/view/${tab?.title}`)
} else if (tab.type === TabType.AUTH) {
router.push(`/nc/${route.params.projectId}/auth`)
}
}
},
})
const activeTab = computed(() => tabs.value?.[activeTabIndex.value])
const addTab = (tabMeta: TabItem) => {
const tabIndex = tabs.value.findIndex((tab) => tab.id === tabMeta.id)
// if tab already found make it active
if (tabIndex > -1) {
activeTab.value = tabIndex
activeTabIndex.value = tabIndex
}
// if tab not found add it
else {
tabs.value = [...(tabs.value || []), tabMeta]
activeTab.value = tabs.value.length - 1
activeTabIndex.value = tabs.value.length - 1
}
}
const clearTabs = () => {
tabs.value = []
}
const closeTab = (key: number | Partial<TabItem>) => {
if (typeof key === 'number') tabs.value.splice(key, 1)
else {
const index = tabs.value.findIndex(getPredicate(key))
if (index > -1) tabs.value.splice(index, 1)
const closeTab = async (key: number | Partial<TabItem>) => {
const index = typeof key === 'number' ? key : tabs.value.findIndex(getPredicate(key))
if (activeTabIndex.value === index) {
let newTabIndex = index - 1
if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (newTabIndex === -1) {
await router.push(`/nc/${route.params.projectId}`)
} else {
await router.push(`/nc/${route.params.projectId}/table/${tabs.value?.[newTabIndex]?.title}`)
}
}
tabs.value.splice(index, 1)
}
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
@ -48,5 +105,5 @@ export default () => {
}
}
return { tabs, addTab, activeTab, clearTabs, closeTab, updateTab }
return { tabs, addTab, activeTabIndex, activeTab, clearTabs, closeTab, updateTab }
}

5
packages/nc-gui-v2/composables/useViewColumns.ts

@ -119,6 +119,10 @@ export default function (
?.sort((c1, c2) => c1.order - c2.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) || []) as ColumnType[]
})
const sortedFields = computed<ColumnType[]>(() => {
return (fields?.value?.sort((c1, c2) => c1.order - c2.order)?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) ||
[]) as ColumnType[]
})
return {
fields,
@ -130,5 +134,6 @@ export default function (
saveOrUpdate,
sortedAndFilteredFields,
showSystemFields,
sortedFields,
}
}

5
packages/nc-gui-v2/context/index.ts

@ -1,11 +1,12 @@
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import type { InjectionKey, Ref } from 'vue'
import type { ComputedRef, InjectionKey, Ref } from 'vue'
import type { EventHook } from '@vueuse/core'
import type { useViewData } from '#imports'
import type { TabItem } from '~/composables/useTabs'
export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection')
export const MetaInj: InjectionKey<Ref<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<any> = Symbol('tab-meta-injection')
export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =
Symbol('pagination-data-injection')
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')

2
packages/nc-gui-v2/nuxt.config.ts

@ -49,7 +49,7 @@ export default defineNuxtConfig({
css: {
preprocessorOptions: {
less: {
modifyVars: { 'primary-color': '#1348ba' },
modifyVars: { 'primary-color': '#1348ba', 'text-color': 'rgba(61, 61, 61, 1)' },
javascriptEnabled: true,
},
},

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

@ -9,6 +9,7 @@
"@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -18,6 +19,7 @@
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3"
},
@ -15684,6 +15686,11 @@
"node": "^10.12.0 || >=12.0.0"
}
},
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"dev": true,
@ -21953,6 +21960,22 @@
"vue": "^3.0.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/vuedraggable/node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
},
"node_modules/vuetify": {
"version": "3.0.0-beta.5",
"license": "MIT",
@ -26412,6 +26435,11 @@
"flat-cache": "^3.0.4"
}
},
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"file-uri-to-path": {
"version": "1.0.0",
"dev": true
@ -36450,6 +36478,21 @@
"is-plain-object": "3.0.1"
}
},
"vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"requires": {
"sortablejs": "1.14.0"
},
"dependencies": {
"sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
}
}
},
"vuetify": {
"version": "3.0.0-beta.5",
"requires": {}

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

@ -15,6 +15,7 @@
"@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -24,8 +25,9 @@
"unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5",
"vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3"
"xlsx": "^0.17.3",
"vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13"
},
"devDependencies": {
"@antfu/eslint-config": "^0.25.2",

40
packages/nc-gui-v2/pages/nc/[projectId].vue → packages/nc-gui-v2/pages/nc/[projectId]/index.vue

@ -1,10 +1,14 @@
<script setup lang="ts">
import useTabs from '~/composables/useTabs'
const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { clearTabs, addTab } = useTabs()
const { $state } = useNuxtApp()
addTab({ type: 'auth', title: 'Team & Auth' })
if (!route.params.type) {
addTab({ type: 'auth', title: 'Team & Auth' })
}
watch(
() => route.params.projectId,
@ -27,38 +31,6 @@ $state.sidebarOpen.value = true
<template #sidebar>
<DashboardTreeView />
</template>
<v-container fluid>
<DashboardTabView />
</v-container>
<NuxtPage />
</NuxtLayout>
</template>
<style scoped lang="scss">
.nc-container {
.nc-topbar {
position: fixed;
top: 0;
left: 0;
height: 50px;
width: 100%;
z-index: 5;
}
.nc-sidebar {
position: fixed;
top: 50px;
left: 0;
height: calc(100% - 50px);
width: 250px;
}
.nc-content {
position: fixed;
top: 50px;
left: 250px;
height: calc(100% - 50px);
width: calc(100% - 250px);
}
}
</style>

152
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

@ -0,0 +1,152 @@
<script setup lang="ts">
import useTabs from '~/composables/useTabs'
import { TabMetaInj } from '~/context'
import useUIPermission from '~/composables/useUIPermission'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiTableIcon from '~icons/mdi/table'
import MdiCsvIcon from '~icons/mdi/file-document-outline'
import MdiExcelIcon from '~icons/mdi/file-excel'
import MdiJSONIcon from '~icons/mdi/code-json'
import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiRequestDataSourceIcon from '~icons/mdi/open-in-new'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()
const { isUIAllowed } = useUIPermission()
const tableCreateDialog = ref(false)
const airtableImportDialog = ref(false)
const quickImportDialog = ref(false)
const importType = ref('')
const currentMenu = ref<string[]>(['addORImport'])
provide(TabMetaInj, activeTab)
function onEdit(targetKey: number, action: string) {
if (action !== 'add') {
closeTab(targetKey)
}
}
function openQuickImportDialog(type: string) {
quickImportDialog.value = true
importType.value = type
}
</script>
<template>
<div class="nc-container d-flex flex-column">
<div>
<a-tabs v-model:activeKey="activeTabIndex" size="small" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i" :tab="tab.title" />
<template #leftExtra>
<a-menu v-model:selectedKeys="currentMenu" mode="horizontal">
<a-sub-menu key="addORImport">
<template #title>
<div class="text-sm flex items-center gap-2">
<MdiPlusIcon />
Add / Import
</div>
</template>
<a-menu-item-group v-if="isUIAllowed('addTable')">
<a-menu-item key="add-new-table" v-t="['a:actions:create-table']" @click="tableCreateDialog = true">
<span class="flex items-center gap-2">
<MdiTableIcon class="text-primary" />
<!-- Add new table -->
{{ $t('tooltip.addTable') }}
</span>
</a-menu-item>
</a-menu-item-group>
<a-menu-item-group title="QUICK IMPORT FROM">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
v-t="['a:actions:import-airtable']"
@click="airtableImportDialog = true"
>
<span class="flex items-center gap-2">
<MdiAirTableIcon class="text-primary" />
<!-- TODO: i18n -->
Airtable
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('csvImport')"
key="quick-import-csv"
v-t="['a:actions:import-csv']"
@click="openQuickImportDialog('csv')"
>
<span class="flex items-center gap-2">
<MdiCsvIcon class="text-primary" />
<!-- TODO: i18n -->
CSV file
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('jsonImport')"
key="quick-import-json"
v-t="['a:actions:import-json']"
@click="openQuickImportDialog('json')"
>
<span class="flex items-center gap-2">
<MdiJSONIcon class="text-primary" />
<!-- TODO: i18n -->
JSON file
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
v-t="['a:actions:import-excel']"
@click="openQuickImportDialog('excel')"
>
<span class="flex items-center gap-2">
<MdiExcelIcon class="text-primary" />
<!-- TODO: i18n -->
Microsoft Excel
</span>
</a-menu-item>
</a-menu-item-group>
<a-divider class="ma-0 mb-2" />
<a-menu-item
v-if="isUIAllowed('importRequest')"
key="add-new-table"
v-t="['e:datasource:import-request']"
class="ma-0 mt-3"
>
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank" class="prose-sm pa-0">
<span class="flex items-center gap-2">
<MdiRequestDataSourceIcon class="text-primary" />
<!-- TODO: i18n -->
Request a data source you need?
</span>
</a>
</a-menu-item>
</a-sub-menu>
</a-menu>
</template>
</a-tabs>
</div>
<div class="flex-1 min-h-0">
<NuxtPage />
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
</div>
</template>
<style scoped>
.nc-container {
height: calc(calc(100vh - var(--header-height)));
@apply overflow-hidden;
}
:deep(.ant-tabs-nav) {
@apply !mb-0;
}
</style>

11
packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -0,0 +1,11 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<TabsSmartsheet />
</template>
<style scoped></style>

5
packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue

@ -0,0 +1,5 @@
<template>
<div>
<h2 class="text-3xl mt-3">Team & Auth</h2>
</div>
</template>

17
packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue

@ -0,0 +1,17 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<div class="nc-main-tab">
<span>Welcome to NocoDB!</span>
</div>
</template>
<style scoped>
.nc-main-tab {
@apply w-full text-3xl text-gray-400 flex align-center justify-center;
}
</style>

1
packages/nc-gui-v2/pages/project/index/create-external.vue

@ -8,7 +8,6 @@ import { navigateTo, useNuxtApp } from '#app'
import { ClientType } from '~/lib/enums'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { readFile } from '~/utils/fileUtils'
import type { ProjectCreateForm } from '~/utils/projectCreateUtils'
import {
clientTypes,

13
packages/nocodb-sdk/src/lib/helperFunctions.ts

@ -12,12 +12,13 @@ const getSystemColumnsIds = (columns) => {
const getSystemColumns = (columns) => columns.filter(isSystemColumn) || [];
const isSystemColumn = (col) =>
col.uidt === UITypes.ForeignKey ||
col.column_name === 'created_at' ||
col.column_name === 'updated_at' ||
(col.pk && (col.ai || col.cdf)) ||
(col.pk && col.meta && col.meta.ag) ||
col.system;
col &&
(col.uidt === UITypes.ForeignKey ||
col.column_name === 'created_at' ||
col.column_name === 'updated_at' ||
(col.pk && (col.ai || col.cdf)) ||
(col.pk && col.meta && col.meta.ag) ||
col.system);
export {
filterOutSystemColumns,

Loading…
Cancel
Save