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. 67
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  6. 49
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  7. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/DeleteTable.vue
  8. 54
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  9. 657
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  10. 124
      packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue
  11. 334
      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. 47
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  16. 14
      packages/nc-gui-v2/components/smartsheet-toolbar/ToggleDrawer.vue
  17. 10
      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. 27
      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. 38
      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. 5
      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 { html {
overflow-y: auto !important; 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' import { computed, inject, onMounted, ref } from '#imports'
interface Props { interface Props {
modelValue: string modelValue?: string
} }
const { modelValue: value } = defineProps<Props>() 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) => { const addTableTab = (table: TableType) => {
$e('a:table:open') $e('a:table:open')
addTab({ title: table.title, id: table.id }) addTab({ title: table.title, id: table.id, type: table.type as any })
} }
</script> </script>
@ -208,7 +208,7 @@ const addTableTab = (table: TableType) => {
</div> </div>
<a-dropdown :trigger="['contextmenu']"> <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 <div
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer" class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList" @click="showTableList = !showTableList"
@ -248,7 +248,7 @@ const addTableTab = (table: TableType) => {
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <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-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu> </a-menu>
</template> </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>

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

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

@ -16,50 +16,19 @@ const applyChanges = () => {}
</script> </script>
<template> <template>
<v-menu offset-y eager transition="slide-y-transition"> <a-dropdown :trigger="['click']">
<template #activator="{ props }">
<v-badge :value="filters.length" color="primary" dot overlap> <v-badge :value="filters.length" color="primary" dot overlap>
<v-btn <a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
v-t="['c:filter']" <div class="flex align-center gap-1">
class="nc-filter-menu-btn px-2 nc-remove-border" <MdiFilterIcon class="text-grey" />
: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" />
<!-- Filter --> <!-- Filter -->
<span class="text-capitalize nc-filter-menu-btn">{{ $t('activity.filter') }}</span> <span class="text-capitalize nc-filter-menu-btn">{{ $t('activity.filter') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDownIcon class="text-grey" />
</v-btn> </div>
</a-button>
</v-badge> </v-badge>
<template #overlay>
<SmartsheetToolbarColumnFilter />
</template> </template>
<SmartsheetToolbarColumnFilter> </a-dropdown>
<!-- <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>
</template> </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>

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

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import { computed } from 'vue'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
interface Props { interface Props {
@ -42,10 +44,28 @@ 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> </script>
<template> <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" ref="field"
v-model="localValue" v-model="localValue"
class="caption" class="caption"
@ -57,22 +77,22 @@ const localValue = computed({
hide-details hide-details
@click.stop @click.stop
> >
<!-- &lt;!&ndash; @change="$emit('change')" &ndash;&gt; --> &lt;!&ndash; &lt;!&ndash; @change="$emit('change')" &ndash;&gt; &ndash;&gt;
<!-- <template #selection="{ item }"> --> &lt;!&ndash; <template #selection="{ item }"> &ndash;&gt;
<!-- <v-icon small class="mr-1"> --> &lt;!&ndash; <v-icon small class="mr-1"> &ndash;&gt;
<!-- {{ item.icon }} --> &lt;!&ndash; {{ item.icon }} &ndash;&gt;
<!-- </v-icon> --> &lt;!&ndash; </v-icon> &ndash;&gt;
<!-- {{ item.title }} --> &lt;!&ndash; {{ item.title }} &ndash;&gt;
<!-- </template> --> &lt;!&ndash; </template> &ndash;&gt;
<!-- <template #item="{ item }"> --> &lt;!&ndash; <template #item="{ item }"> &ndash;&gt;
<!-- <span :class="`caption font-weight-regular nc-fld-${item.title}`"> --> &lt;!&ndash; <span :class="`caption font-weight-regular nc-fld-${item.title}`"> &ndash;&gt;
<!-- <v-icon color="grey" small class="mr-1"> --> &lt;!&ndash; <v-icon color="grey" small class="mr-1"> &ndash;&gt;
<!-- {{ item.icon }} --> &lt;!&ndash; {{ item.icon }} &ndash;&gt;
<!-- </v-icon> --> &lt;!&ndash; </v-icon> &ndash;&gt;
<!-- {{ item.title }} --> &lt;!&ndash; {{ item.title }} &ndash;&gt;
<!-- </span> --> &lt;!&ndash; </span> &ndash;&gt;
<!-- </template> --> &lt;!&ndash; </template> &ndash;&gt;
</v-autocomplete> </v-autocomplete> -->
</template> </template>
<style scoped></style> <style scoped></style>

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

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context' import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewColumns from '~/composables/useViewColumns' import useViewColumns from '~/composables/useViewColumns'
import MdiMenuDownIcon from '~icons/mdi/menu-down' import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline' import MdiEyeIcon from '~icons/mdi/eye-off-outline'
import MdiDragIcon from '~icons/mdi/drag'
const { fieldsOrder, coverImageField, modelValue } = defineProps<{ const { fieldsOrder, coverImageField, modelValue } = defineProps<{
coverImageField?: string coverImageField?: string
@ -23,6 +25,8 @@ const isAnyFieldHidden = computed(() => {
// return meta?.fields?.some(field => field.hidden) // return meta?.fields?.some(field => field.hidden)
}) })
const { $e } = useNuxtApp()
const { const {
showSystemFields, showSystemFields,
sortedAndFilteredFields, sortedAndFilteredFields,
@ -33,6 +37,7 @@ const {
showAll, showAll,
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
sortedFields,
} = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger()) } = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger())
watch( watch(
@ -52,634 +57,76 @@ watch(
{ immediate: true }, { immediate: true },
) )
/* import draggable from 'vuedraggable' const onMove = (event) => {
import { getSystemColumnsIds } from 'nocodb-sdk' // todo : sync with server
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes' // if (!sortedFields?.value) return
// if (sortedFields?.value.length - 1 === event.moved.newIndex) {
export default { // sortedFields.value[event.moved.newIndex].order = sortedFields.value[event.moved.newIndex - 1].order + 1
name: 'FieldsMenu', // } else if (event.moved.newIndex === 0) {
components: { // sortedFields.value[event.moved.newIndex].order = sortedFields.value[1].order / 2
Draggable: draggable, // } else {
}, // sortedFields.value[event.moved.newIndex].order =
props: { // (sortedFields?.value[event.moved.newIndex - 1].order + sortedFields?.value[event.moved.newIndex + 1].order) / 2
coverImageField: String, // // );
groupingField: String, // }
isGallery: Boolean, // saveOrUpdate(sortedFields[event.moved.newIndex], event.moved.newIndex);
isKanban: Boolean, $e('a:fields:reorder')
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')
},
},
} */
</script> </script>
<template> <template>
<v-menu> <a-dropdown :trigger="['click']">
<template #activator="{ props }"> <v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap v-bind="props"> <a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<v-btn <div class="flex align-center gap-1">
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,
}"
>
<!-- <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> --> <!-- <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 --> <!-- Fields -->
<span class="text-sm text-capitalize nc-fields-menu-btn">{{ $t('objects.fields') }}</span> <span class="text-sm text-capitalize nc-fields-menu-btn">{{ $t('objects.fields') }}</span>
<MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon> <MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon>
</v-btn> </div>
</a-button>
</v-badge> </v-badge>
</template>
<v-list density="compact" class="pt-0" min-width="280" @click.stop> <template #overlay>
<div class="pt-0 min-w-[280px] bg-white shadow" @click.stop>
<div class="nc-fields-list py-1"> <div class="nc-fields-list py-1">
<!-- <Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)"> --> <Draggable :list="sortedFields" @change="onMove($event)">
<v-list-item v-for="(field, i) in filteredFieldList" :key="field.id" dense> <template #item="{ element: field }">
<input <div :key="field.id" class="px-2 py-1 flex" @click.stop>
:id="`show-field-${field.id}`" <a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, i)">
v-model="field.show" <span class="text-xs">{{ field.title }}</span>
type="checkbox" </a-checkbox>
class="mt-0 pt-0" <div class="flex-1" />
@click.stop <MdiDragIcon class="cursor-move" />
@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>
</template>
</v-select>
</div> </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> </template>
</v-checkbox> </Draggable>
<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> </div>
<v-divider class="my-2" /> <v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense> <div class="p-2 py-1 flex" @click.stop>
<v-checkbox v-model="showSystemFieldsLoc" class="mt-0 pt-0" dense hide-details @click.stop> <a-checkbox v-model:checked="showSystemFields">
<template #label> <span class="text-xs"> {{ $t('activity.showSystemFields') }}</span>
<span class="caption"> </a-checkbox>
&lt;!&ndash; Show System Fields &ndash;&gt; </div>
{{ $t('activity.showSystemFields') }} <div class="p-2 flex gap-2" @click.stop>
</span> <a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="showAll">
</template> <!-- Show All -->
</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') }} {{ $t('general.showAll') }}
</v-btn> </a-button>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll"> <a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="hideAll">
&lt;!&ndash; Hide All &ndash;&gt; <!-- Hide All -->
{{ $t('general.hideAll') }} {{ $t('general.hideAll') }}
</v-btn> </a-button>
</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-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>
</template>
</v-select>
</div> </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> </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> </template>
</v-checkbox> </a-dropdown>
</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> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/*::v-deep { :deep(.ant-checkbox-input) {
.v-list-item { transform: scale(0.7);
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;
}
} }
}
.drag-icon {
cursor: all-scroll; !*cursor: grab;*!
}
.nc-fields-list {
height: auto;
max-height: 500px;
overflow-y: auto;
}*/
</style> </style>

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

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

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

@ -1,154 +1,43 @@
<script> <script lang="ts" setup>
import FileSaver from 'file-saver'
import { ExportTypes } from 'nocodb-sdk' import { ExportTypes } from 'nocodb-sdk'
import { NOCO } from '~/lib/constants' import { useToast } from 'vue-toastification'
import DropOrSelectFileModal from '~/components/import/DropOrSelectFileModal' import FileSaver from 'file-saver'
import ColumnMappingModal from '~/components/project/spreadsheet/components/importExport/ColumnMappingModal' import { useNuxtApp } from '#app'
import CSVTemplateAdapter from '~/components/import/templateParsers/CSVTemplateAdapter' import useProject from '~/composables/useProject'
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' import { ActiveViewInj, MetaInj } from '~/context'
import WebhookModal from '~/components/project/tableTabs/webhook/WebhookModal' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import WebhookSlider from '~/components/project/tableTabs/webhook/WebhookSlider' import MdiFlashIcon from '~icons/mdi/flash-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
export default { import MdiDownloadIcon from '~icons/mdi/download-outline'
name: 'ExportImport', import MdiUploadIcon from '~icons/mdi/upload-outline'
components: { import MdiHookIcon from '~icons/mdi/hook'
WebhookSlider, import MdiViewListIcon from '~icons/mdi/view-list-outline'
ColumnMappingModal,
DropOrSelectFileModal, // todo : replace with inject
}, const publicViewId = null
props: { const { project } = useProject()
meta: Object,
nodes: Object, const { $api } = useNuxtApp()
selectedView: Object, const toast = useToast()
publicViewId: String,
queryParams: Object, const meta = inject(MetaInj)
isView: Boolean, const selectedView = inject(ActiveViewInj)
reqPayload: Object,
}, const exportCsv = async () => {
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 offset = 0
let c = 1 let c = 1
try { try {
while (!isNaN(offset) && offset > -1) { while (!isNaN(offset) && offset > -1) {
let res let res
if (this.publicViewId) { if (publicViewId) {
res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, { /* res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob', responseType: 'blob',
query: { query: {
fields: fields:
this.queryParams && this.queryParams &&
this.queryParams.fieldsOrder && this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter((c) => this.queryParams.showFields[c]), this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset, offset,
sortArrJson: JSON.stringify( sortArrJson: JSON.stringify(
this.reqPayload && this.reqPayload &&
@ -156,169 +45,82 @@ export default {
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({ this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction, direction,
fk_column_id, fk_column_id,
})), }))
), ),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters), filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
}, },
headers: { headers: {
'xc-password': this.reqPayload && this.reqPayload.password, 'xc-password': this.reqPayload && this.reqPayload.password,
}, },
}) });
*/
} else { } else {
res = await this.$api.dbViewRow.export( res = await $api.dbViewRow.export(
NOCO, 'noco',
this.projectName, project?.value.title as string,
this.meta.title, meta?.value.title as string,
this.selectedView.title, selectedView?.value.title as string,
ExportTypes.CSV, ExportTypes.CSV,
{ {
responseType: 'blob', responseType: 'blob',
query: { query: {
offset, offset,
}, },
}, } as any,
) )
} }
const { data } = res const { data } = res
offset = +res.headers['nc-export-offset'] offset = +res.headers['nc-export-offset']
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${this.meta.title}_exported_${c++}.csv`) FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
if (offset > -1) { if (offset > -1) {
this.$toast.info('Downloading more files').goAway(3000) toast.info('Downloading more files')
} else { } else {
this.$toast.success('Successfully exported all table data').goAway(3000) toast.success('Successfully exported all table data')
} }
} }
} catch (e) { } catch (e) {
console.log(e) toast.error(extractSdkResponseErrorMsg(e))
this.$toast.error(e.message).goAway(3000)
} }
},
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)
}
},
},
} }
</script> </script>
<template> <template>
<div> <a-dropdown>
<v-menu open-on-hover bottom offset-y transition="slide-y-transition"> <a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<template #activator="{ on }"> <div class="flex gap-1 align-center">
<v-btn <MdiFlashIcon class="text-grey" />
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 --> <!-- More -->
{{ $t('general.more') }} {{ $t('general.more') }}
<MdiMenuDownIcon class="text-grey" />
<v-icon small color="#777"> mdi-menu-down </v-icon> </div>
</v-btn> </a-button>
</template> <template #overlay>
<div class="bg-white shadow">
<v-list dense> <div>
<v-list-item v-t="['a:actions:download-csv']" dense @click="exportCsv"> <div class="nc-menu-item" @click.stop="exportCsv">
<v-list-item-title> <MdiDownloadIcon />
<v-icon small class="mr-1"> mdi-download-outline </v-icon>
<span class="caption">
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
</span> </div>
</v-list-item-title> <div class="nc-menu-item" @click.stop>
</v-list-item> <MdiUploadIcon />
<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 --> <!-- Upload CSV -->
{{ $t('activity.uploadCSV') }} {{ $t('activity.uploadCSV') }}
</span> </div>
<div class="nc-menu-item" @click.stop>
<span class="caption grey--text">(<x-icon small color="grey lighten-2"> mdi-alpha </x-icon> version)</span> <MdiViewListIcon />
</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 --> <!-- Shared View List -->
{{ $t('activity.listSharedView') }} {{ $t('activity.listSharedView') }}
</span> </div>
</v-list-item-title> <div class="nc-menu-item" @click.stop>
</v-list-item> <MdiHookIcon />
<v-list-item v-if="_isUIAllowed('webhook') && !isView" v-t="['c:actions:webhook']" dense @click="webhookModal = true"> <!-- todo: i18n -->
<v-list-item-title> Webhook
<v-icon small class="mr-1" color=""> mdi-hook </v-icon> </div>
<span class="caption"> Webhooks </span> </div>
</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> </div>
</template> </template>
</a-dropdown>
<style scoped></style> </template>

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 />

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

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

@ -50,9 +50,9 @@ onKeyStroke(['Enter'], (e) => {
}) })
watch( watch(
[() => meta?.value?.id, () => view?.value?.id], () => view?.value?.id,
async (n: any, o: any) => { async (n?: string, o?: string) => {
if (meta?.value && view?.value) { if (n && n !== o) {
await loadData() await loadData()
} }
}, },
@ -65,7 +65,8 @@ defineExpose({
</script> </script>
<template> <template>
<div class="nc-grid-wrapper"> <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"> <table class="xc-row-table nc-grid backgroundColorDefault">
<thead> <thead>
<tr> <tr>
@ -188,6 +189,7 @@ defineExpose({
</table> </table>
</div> </div>
<SmartsheetPagination /> <SmartsheetPagination />
</div>
</template> </template>
<style scoped lang="scss"> <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"> <div class="d-flex align-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span> <span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<v-spacer /> <v-spacer />
<v-pagination <!-- <v-pagination
v-if="count !== Infinity" v-if="count !== Infinity"
v-model="page" v-model="page"
style="max-width: 100%" style="max-width: 100%"
@ -56,6 +56,17 @@ export default {
:total-visible="8" :total-visible="8"
color="primary lighten-2" color="primary lighten-2"
class="nc-pagination" 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"> <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> <span class="caption" style="white-space: nowrap"> Change page:</span>
@ -79,3 +90,10 @@ export default {
<v-spacer /> <v-spacer />
</div> </div>
</template> </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> <script setup lang="ts"></script>
<template> <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" /> <SmartsheetToolbarFieldsMenu :show-system-fields="false" />
<SmartsheetToolbarColumnFilterMenu /> <SmartsheetToolbarColumnFilterMenu />
<SmartsheetToolbarSortListMenu /> <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> </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>

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

@ -1,25 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEventBus } from '@vueuse/core' import type { ColumnType, ViewType } from 'nocodb-sdk'
import type { ColumnType, FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import { ViewTypes } 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 { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, TabMetaInj } from '~/context'
import useMetas from '~/composables/useMetas' import useMetas from '~/composables/useMetas'
const { tabMeta } = defineProps({
tabMeta: Object,
})
const { getMeta, metas } = useMetas() const { getMeta, metas } = useMetas()
const activeView = ref<GridType | FormType | KanbanType | GalleryType>() const activeView = ref<ViewType>()
const el = ref<any>() const el = ref<any>()
const fields = ref<ColumnType[]>([]) const fields = ref<ColumnType[]>([])
const meta = computed(() => metas.value?.[tabMeta?.id]) const tabMeta = inject(TabMetaInj)
onMounted(async () => { const meta = computed(() => metas.value?.[tabMeta?.value?.id as string])
await getMeta(tabMeta?.id)
watchEffect(async () => {
await getMeta(tabMeta?.value?.id as string)
}) })
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
@ -40,11 +37,12 @@ watch(
</script> </script>
<template> <template>
<div class="overflow-auto"> <div class="nc-container flex h-full">
<div class="flex flex-col h-full flex-1 min-w-0">
<SmartsheetToolbar /> <SmartsheetToolbar />
<template v-if="meta"> <template v-if="meta">
<div class="d-flex"> <div class="flex flex-1 min-h-0">
<div v-if="activeView" class="flex-grow-1 min-w-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" /> <SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" />
<SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" /> <SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" /> <SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" />
@ -53,4 +51,5 @@ watch(
</div> </div>
</template> </template>
</div> </div>
</div>
</template> </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 column = inject(ColumnInj)
const value = inject('value') const value = inject('value')
const active = false const active = false
const localState = null
const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType) const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType)
await loadParentMeta() 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 column = inject(ColumnInj)
const value = inject('value') const value = inject('value')
const active = false const active = false
const isLocked = false
const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType) const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType)
await loadChildMeta() 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 { useState } from '#app'
import useProject from '~/composables/useProject'
enum TabType {
TABLE = 'table',
VIEW = 'view',
AUTH = 'auth',
}
export interface TabItem { export interface TabItem {
type: 'table' | 'view' | 'auth' type: TabType
title: string title: string
id?: string id?: string
} }
@ -15,30 +23,79 @@ function getPredicate(key: Partial<TabItem>) {
export default () => { export default () => {
const tabs = useState<TabItem[]>('tabs', () => []) 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 addTab = (tabMeta: TabItem) => {
const tabIndex = tabs.value.findIndex((tab) => tab.id === tabMeta.id) const tabIndex = tabs.value.findIndex((tab) => tab.id === tabMeta.id)
// if tab already found make it active // if tab already found make it active
if (tabIndex > -1) { if (tabIndex > -1) {
activeTab.value = tabIndex activeTabIndex.value = tabIndex
} }
// if tab not found add it // if tab not found add it
else { else {
tabs.value = [...(tabs.value || []), tabMeta] tabs.value = [...(tabs.value || []), tabMeta]
activeTab.value = tabs.value.length - 1 activeTabIndex.value = tabs.value.length - 1
} }
} }
const clearTabs = () => { const clearTabs = () => {
tabs.value = [] tabs.value = []
} }
const closeTab = async (key: number | Partial<TabItem>) => {
const closeTab = (key: number | Partial<TabItem>) => { const index = typeof key === 'number' ? key : tabs.value.findIndex(getPredicate(key))
if (typeof key === 'number') tabs.value.splice(key, 1) if (activeTabIndex.value === index) {
else { let newTabIndex = index - 1
const index = tabs.value.findIndex(getPredicate(key)) if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (index > -1) tabs.value.splice(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>) => { 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) ?.sort((c1, c2) => c1.order - c2.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) || []) as ColumnType[] ?.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 { return {
fields, fields,
@ -130,5 +134,6 @@ export default function (
saveOrUpdate, saveOrUpdate,
sortedAndFilteredFields, sortedAndFilteredFields,
showSystemFields, showSystemFields,
sortedFields,
} }
} }

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

@ -1,11 +1,12 @@
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' 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 { EventHook } from '@vueuse/core'
import type { useViewData } from '#imports' import type { useViewData } from '#imports'
import type { TabItem } from '~/composables/useTabs'
export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection') export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection')
export const MetaInj: InjectionKey<Ref<TableType>> = Symbol('meta-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']> = export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =
Symbol('pagination-data-injection') Symbol('pagination-data-injection')
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = 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: { css: {
preprocessorOptions: { preprocessorOptions: {
less: { less: {
modifyVars: { 'primary-color': '#1348ba' }, modifyVars: { 'primary-color': '#1348ba', 'text-color': 'rgba(61, 61, 61, 1)' },
javascriptEnabled: true, javascriptEnabled: true,
}, },
}, },

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

@ -9,6 +9,7 @@
"@vueuse/integrations": "^8.9.1", "@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6", "ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -18,6 +19,7 @@
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10", "vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13", "vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3" "xlsx": "^0.17.3"
}, },
@ -15684,6 +15686,11 @@
"node": "^10.12.0 || >=12.0.0" "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": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"dev": true, "dev": true,
@ -21953,6 +21960,22 @@
"vue": "^3.0.0" "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": { "node_modules/vuetify": {
"version": "3.0.0-beta.5", "version": "3.0.0-beta.5",
"license": "MIT", "license": "MIT",
@ -26412,6 +26435,11 @@
"flat-cache": "^3.0.4" "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": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"dev": true "dev": true
@ -36450,6 +36478,21 @@
"is-plain-object": "3.0.1" "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": { "vuetify": {
"version": "3.0.0-beta.5", "version": "3.0.0-beta.5",
"requires": {} "requires": {}

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

@ -15,6 +15,7 @@
"@vueuse/integrations": "^8.9.1", "@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6", "ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -24,8 +25,9 @@
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10", "vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5", "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": { "devDependencies": {
"@antfu/eslint-config": "^0.25.2", "@antfu/eslint-config": "^0.25.2",

38
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"> <script setup lang="ts">
import useTabs from '~/composables/useTabs'
const route = useRoute() const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string) const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { clearTabs, addTab } = useTabs() const { clearTabs, addTab } = useTabs()
const { $state } = useNuxtApp() const { $state } = useNuxtApp()
if (!route.params.type) {
addTab({ type: 'auth', title: 'Team & Auth' }) addTab({ type: 'auth', title: 'Team & Auth' })
}
watch( watch(
() => route.params.projectId, () => route.params.projectId,
@ -27,38 +31,6 @@ $state.sidebarOpen.value = true
<template #sidebar> <template #sidebar>
<DashboardTreeView /> <DashboardTreeView />
</template> </template>
<NuxtPage />
<v-container fluid>
<DashboardTabView />
</v-container>
</NuxtLayout> </NuxtLayout>
</template> </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 { ClientType } from '~/lib/enums'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { readFile } from '~/utils/fileUtils' import { readFile } from '~/utils/fileUtils'
import type { ProjectCreateForm } from '~/utils/projectCreateUtils' import type { ProjectCreateForm } from '~/utils/projectCreateUtils'
import { import {
clientTypes, clientTypes,

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

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

Loading…
Cancel
Save