Browse Source

wip(gui-v2): toolbar

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/2716/head
Pranav C 2 years ago
parent
commit
4f2ce10ca6
  1. 2
      packages/nc-gui-v2/components/index.ts
  2. 462
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  3. 116
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  4. 596
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  5. 35
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenuItem.vue
  6. 80
      packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue
  7. 323
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  8. 172
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  9. 2
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  10. 9
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  11. 2
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  12. 15
      packages/nc-gui-v2/composables/useMeta.ts
  13. 30
      packages/nc-gui-v2/composables/useViewColumn.ts
  14. 361
      packages/nc-gui-v2/package-lock.json
  15. 1
      packages/nc-gui-v2/package.json

2
packages/nc-gui-v2/components/index.ts

@ -2,7 +2,7 @@ import type { ColumnType, TableType } from 'nocodb-sdk'
import type { InjectionKey, Ref } from 'vue' import type { InjectionKey, Ref } from 'vue'
import type useViewData from '~/composables/useViewData' import type useViewData from '~/composables/useViewData'
export const ColumnInj: InjectionKey<ColumnType> = 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<any> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> = export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =

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

@ -0,0 +1,462 @@
<script>
import { UITypes, getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
export default {
name: 'ColumnFilter',
components: {
FieldListAutoCompleteDropdown,
},
props: {
fieldList: [Array],
meta: Object,
nested: Boolean,
parentId: String,
viewId: String,
shared: Boolean,
webHook: Boolean,
hookId: String,
},
data: () => ({
filters: [],
opList: [
'is equal',
'is not equal',
'is like',
'is not like',
// 'is empty', 'is not empty',
'is null',
'is not null',
'>',
'<',
'>=',
'<=',
],
comparisonOp: [
{
text: 'is equal',
value: 'eq',
},
{
text: 'is not equal',
value: 'neq',
},
{
text: 'is like',
value: 'like',
},
{
text: 'is not like',
value: 'nlike',
},
{
text: 'is empty',
value: 'empty',
ignoreVal: true,
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true,
},
{
text: 'is null',
value: 'null',
ignoreVal: true,
},
{
text: 'is not null',
value: 'notnull',
ignoreVal: true,
},
{
text: '>',
value: 'gt',
},
{
text: '<',
value: 'lt',
},
{
text: '>=',
value: 'gte',
},
{
text: '<=',
value: 'lte',
},
],
}),
computed: {
columnIcon() {
return this.meta.columns.reduce((iconsObj, c) => {
return { ...iconsObj, [c.title]: getUIDTIcon(c.uidt) }
}, {})
},
columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {})
},
autoApply() {
return this.$store.state.settings.autoApplyFilter && !this.webHook
},
columns() {
return (
this.meta &&
this.meta.columns
.filter((c) => c && (!c.colOptions || !c.system))
.map((c) => ({
...c,
icon: getUIDTIcon(c.uidt),
}))
)
},
types() {
if (!this.meta || !this.meta.columns || !this.meta.columns.length) {
return {}
}
return this.meta.columns.reduce((obj, col) => {
switch (col.uidt) {
case UITypes.Number:
case UITypes.Decimal:
obj[col.title] = obj[col.column_name] = 'number'
break
case UITypes.Checkbox:
obj[col.title] = obj[col.column_name] = 'boolean'
break
default:
break
}
return obj
}, {})
},
},
watch: {
async viewId(v) {
if (v) {
await this.loadFilter()
}
},
filters: {
handler(v) {
this.$emit('input', v && v.filter((f) => (f.fk_column_id && f.comparison_op) || f.is_group))
},
deep: true,
},
},
created() {
this.loadFilter()
},
methods: {
filterComparisonOp(f) {
return this.comparisonOp.filter((op) => {
if (
f &&
f.fk_column_id &&
this.columnsById[f.fk_column_id] &&
this.columnsById[f.fk_column_id].uidt === UITypes.LinkToAnotherRecord &&
this.columnsById[f.fk_column_id].uidt === UITypes.Lookup
) {
return !['notempty', 'empty', 'notnull', 'null'].includes(op.value)
}
return true
})
},
async applyChanges(nested = false, { hookId } = {}) {
for (const [i, filter] of Object.entries(this.filters)) {
if (filter.status === 'delete') {
if (this.hookId || hookId) {
await this.$api.dbTableFilter.delete(filter.id)
} else {
await this.$api.dbTableFilter.delete(filter.id)
}
} else if (filter.status === 'update') {
if (filter.id) {
if (this.hookId || hookId) {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId,
})
} else {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId,
})
}
} else if (this.hookId || hookId) {
this.$set(
this.filters,
i,
await this.$api.dbTableWebhookFilter.create(this.hookId || hookId, {
...filter,
fk_parent_id: this.parentId,
}),
)
} else {
this.$set(
this.filters,
i,
await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId,
}),
)
}
}
}
if (this.$refs.nestedFilter) {
for (const nestedFilter of this.$refs.nestedFilter) {
await nestedFilter.applyChanges(true)
}
}
this.loadFilter()
if (!nested) {
this.$emit('updated')
}
},
async loadFilter() {
let filters = []
if (this.viewId && this._isUIAllowed('filterSync')) {
filters = this.parentId
? await this.$api.dbTableFilter.childrenRead(this.parentId)
: await this.$api.dbTableFilter.read(this.viewId)
}
if (this.hookId && this._isUIAllowed('filterSync')) {
filters = this.parentId
? await this.$api.dbTableFilter.childrenRead(this.parentId)
: await this.$api.dbTableWebhookFilter.read(this.hookId)
}
this.filters = filters
},
addFilter() {
this.filters.push({
fk_column_id: null,
comparison_op: 'eq',
value: '',
status: 'update',
logical_op: 'and',
})
this.filters = this.filters.slice()
this.$e('a:filter:add', { length: this.filters.length })
},
addFilterGroup() {
this.filters.push({
parentId: this.parentId,
is_group: true,
status: 'update',
})
this.filters = this.filters.slice()
const index = this.filters.length - 1
this.saveOrUpdate(this.filters[index], index)
},
filterUpdateCondition(filter, i) {
this.saveOrUpdate(filter, i)
this.$e('a:filter:update', {
logical: filter.logical_op,
comparison: filter.comparison_op,
})
},
async saveOrUpdate(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) {
// this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op))
this.$emit('updated')
} else if (!this.autoApply) {
filter.status = 'update'
} else if (filter.id) {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId,
})
this.$emit('updated')
} else {
this.$set(
this.filters,
i,
await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId,
}),
)
this.$emit('updated')
}
},
async deleteFilter(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) {
this.filters.splice(i, 1)
this.$emit('updated')
} else if (filter.id) {
if (!this.autoApply) {
this.$set(filter, 'status', 'delete')
} else {
await this.$api.dbTableFilter.delete(filter.id)
await this.loadFilter()
this.$emit('updated')
}
} else {
this.filters.splice(i, 1)
this.$emit('updated')
}
this.$e('a:filter:delete')
},
},
}
</script>
<template>
<div class="backgroundColor pa-2 menu-filter-dropdown" :style="{ width: nested ? '100%' : '530px' }">
<div class="grid" @click.stop>
<template v-for="(filter, i) in filters" dense>
<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-if="!filter.readOnly"
:key="`${i}_3`"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter, i)"
>
mdi-close-box
</v-icon>
<span v-else :key="`${i}_1`" />
<v-select
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']"
solo
flat
dense
hide-details
placeholder="Group op"
@click.stop
@change="saveOrUpdate(filter, i)"
>
<template #item="{ item }">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
</div>
<column-filter
v-if="filter.id || shared"
ref="nestedFilter"
v-model="filter.children"
:parent-id="filter.id"
:view-id="viewId"
nested
:meta="meta"
:shared="shared"
:web-hook="webHook"
:hook-id="hookId"
@updated="$emit('updated')"
@input="$emit('input', filters)"
/>
</div>
<template v-else>
<v-icon
v-if="!filter.readOnly"
:key="`${i}_3`"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter, i)"
>
mdi-close-box
</v-icon>
<span v-else :key="`${i}_1`" />
<span v-if="!i" :key="`${i}_2`" class="caption d-flex align-center">{{ $t('labels.where') }}</span>
<v-select
v-else
:key="`${i}_4`"
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']"
solo
flat
dense
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`"
v-model="filter.fk_column_id"
class="caption nc-filter-field-select"
:columns="columns"
:disabled="filter.readOnly"
@click.stop
@change="saveOrUpdate(filter, i)"
/>
<v-select
:key="`k${i}`"
v-model="filter.comparison_op"
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select"
:items="filterComparisonOp(filter)"
:placeholder="$t('labels.operation')"
solo
flat
style="max-width: 120px"
dense
:disabled="filter.readOnly"
hide-details
item-value="value"
@click.stop
@change="filterUpdateCondition(filter, i)"
>
<template #item="{ item }">
<span class="caption font-weight-regular">{{ item.text }}</span>
</template>
</v-select>
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="`span${i}`" />
<v-checkbox
v-else-if="types[filter.field] === 'boolean'"
:key="`${i}_7`"
v-model="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<v-text-field
v-else
:key="`${i}_7`"
v-model="filter.value"
solo
flat
hide-details
dense
class="caption nc-filter-value-select"
:disabled="filter.readOnly"
@click.stop
@input="saveOrUpdate(filter, i)"
/>
</template>
</template>
</template>
</div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addFilter">
<v-icon small color="grey"> mdi-plus </v-icon>
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</v-btn>
<slot />
</div>
</template>
<style scoped>
.grid {
display: grid;
grid-template-columns: 22px 80px auto auto auto;
column-gap: 6px;
row-gap: 6px;
}
</style>

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

@ -0,0 +1,116 @@
<script>
import ColumnFilter from '~/components/project/spreadsheet/components/ColumnFilter'
export default {
name: 'ColumnFilterMenu',
components: { ColumnFilter },
props: ['fieldList', 'isLocked', 'value', 'meta', 'viewId', 'shared'],
data: () => ({
filters: [],
}),
computed: {
autosave: {
set(v) {
this.$store.commit('settings/MutAutoApplyFilter', v)
this.$e('a:filter:auto-apply', { flag: v })
},
get() {
return this.$store.state.settings.autoApplyFilter
},
},
},
watch: {
filters: {
handler(v) {
if (this.autosave) {
this.$emit('input', v)
}
},
deep: true,
},
autosave(v) {
if (!v) {
this.filters = JSON.parse(JSON.stringify(this.value || []))
}
},
value(v) {
this.filters = this.autosave ? v || [] : JSON.parse(JSON.stringify(v || []))
},
},
created() {
this.filters = this.autosave ? this.value || [] : JSON.parse(JSON.stringify(this.value || []))
},
methods: {
applyChanges() {
this.$emit('input', this.filters)
if (this.$refs.filter) {
this.$refs.filter.applyChanges()
}
this.$e('a:filter:apply')
},
},
}
</script>
<template>
<v-menu offset-y eager transition="slide-y-transition">
<template #activator="{ on }">
<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-on="on"
>
<v-icon small class="mr-1" color="grey darken-3"> mdi-filter-outline </v-icon>
<!-- Filter -->
{{ $t('activity.filter') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
</v-badge>
</template>
<ColumnFilter
ref="filter"
v-model="filters"
:shared="shared"
:view-id="viewId"
:field-list="fieldList"
:meta="meta"
v-on="$listeners"
>
<div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
id="col-filter-checkbox"
v-model="autosave"
class="col-filter-checkbox"
hide-details
dense
type="checkbox"
color="grey"
>
<template #label>
<span class="grey--text caption">
{{ $t('msg.info.filterAutoApply') }}
<!-- Auto apply -->
</span>
</template>
</v-checkbox>
<v-spacer />
<v-btn v-show="!autosave" color="primary" small class="caption ml-2" @click="applyChanges"> Apply changes </v-btn>
</div>
</ColumnFilter>
</v-menu>
</template>
<style scoped>
/deep/ .col-filter-checkbox .v-input--selection-controls__input {
transform: scale(0.7);
}
</style>

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

@ -0,0 +1,596 @@
<script setup lang="ts">
import { computed, inject } from 'vue'
import { MetaInj } from '~/components'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline'
const { showSystemFields, fieldsOrder, coverImageField, modelValue } = defineProps<{
showSystemFields?: boolean
coverImageField?: string
fieldsOrder?: string[]
modelValue?: Record<string, boolean>
}>()
const meta = inject(MetaInj)
const isLocked = false // inject(IsLockedInj)
const isAnyFieldHidden = computed(() => {
return false
// todo: implement
// return meta?.fields?.some(field => field.hidden)
})
/* 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')
},
},
} */
</script>
<template>
<v-menu>
<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> -->
<MdiEyeIcon class="mr-1 text-grey"></MdiEyeIcon>
<!-- Fields -->
{{ $t('objects.fields') }}
<MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon>
</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>
<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
>
<!-- <template v-slot:prepend-inner>
<v-icon small color="grey" class="mt-2">
mdi-magnify
</v-icon>
</template> -->
</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">
<!-- Show System Fields -->
{{ $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">
<!-- Show All -->
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll">
<!-- Hide All -->
{{ $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-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>
<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>
<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;
}
}
}
.drag-icon {
cursor: all-scroll; !*cursor: grab;*!
}
.nc-fields-list {
height: auto;
max-height: 500px;
overflow-y: auto;
}*/
</style>

35
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenuItem.vue

@ -0,0 +1,35 @@
<script>
import cell from '@/components/project/spreadsheet/mixins/cell'
export default {
name: 'FieldsMenuItem',
mixins: [cell],
props: {
sqlUi: [Object, Function],
column: Object,
},
}
</script>
<template>
<div>
<v-icon v-if="column.pk" color="warning" x-small class="mr-1"> mdi-key-variant </v-icon>
<v-icon v-else-if="uiDatatypeIcon" small class="mr-1">
{{ uiDatatypeIcon }}
</v-icon>
<v-icon v-else-if="isForeignKey" color="purple" small class="mr-1"> mdi-link-variant </v-icon>
<span v-else-if="isInt" class="font-weight-bold mr-1" style="font-size: 15px">#</span>
<v-icon v-else-if="isFloat" color="grey" class="mr-1 mt-n1"> mdi-decimal </v-icon>
<v-icon v-else-if="isDate" color="grey" small class="mr-1"> mdi-calendar </v-icon>
<v-icon v-else-if="isDateTime" color="grey" small class="mr-1"> mdi-calendar-clock </v-icon>
<v-icon v-else-if="isSet" color="grey" small class="mr-1"> mdi-checkbox-multiple-marked </v-icon>
<v-icon v-else-if="isEnum" color="grey" small class="mr-1"> mdi-radiobox-marked </v-icon>
<v-icon v-else-if="isBoolean" color="grey" small class="mr-1"> mdi-check-box-outline </v-icon>
<v-icon v-else-if="isString" color="grey" class=""> mdi-alpha-a </v-icon>
<v-icon v-else-if="isTextArea" color="grey" small class="mr-1"> mdi-card-text-outline </v-icon>
</div>
</template>
<style scoped></style>

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

@ -0,0 +1,80 @@
<script>
export default {
name: 'LockMenu',
props: ['value'],
data: () => ({}),
methods: {
changeLockType(type) {
this.$e('a:grid:lockmenu', { lockType: type })
if (type === 'personal') {
return this.$toast.info('Coming soon').goAway(3000)
}
this.$emit('input', type)
this.$toast.success(`Successfully Switched to ${type} view`).goAway(3000)
},
},
}
</script>
<template>
<v-menu offset-y max-width="350">
<template #activator="{ on }">
<v-icon v-if="value === 'locked'" small class="mx-1 nc-view-lock-menu" v-on="on"> mdi-lock-outline </v-icon>
<v-icon v-else-if="value === 'personal'" small class="mx-1 nc-view-lock-menu" v-on="on"> mdi-account </v-icon>
<v-icon v-else small class="mx-1 nc-view-lock-menu" v-on="on"> mdi-account-group-outline </v-icon>
</template>
<v-list maxc-width="350">
<v-list-item two-line class="pb-4" @click="changeLockType('collaborative')">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="!value || value === '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('locked')">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === '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('personal')">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === '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>
</template>
<style scoped></style>

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

@ -0,0 +1,323 @@
<script>
import FileSaver from 'file-saver'
import { ExportTypes } from 'nocodb-sdk'
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,
WebhookModal,
ColumnMappingModal,
DropOrSelectFileModal,
},
props: {
meta: Object,
nodes: Object,
selectedView: Object,
publicViewId: String,
queryParams: Object,
isView: Boolean,
reqPayload: Object,
},
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)
}
},
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>
<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>
</template>
<style scoped></style>

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

@ -0,0 +1,172 @@
<script>
import { RelationTypes, UITypes } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
export default {
name: 'SortListMenu',
components: { FieldListAutoCompleteDropdown },
props: {
fieldList: Array,
value: [Array, Object],
isLocked: Boolean,
meta: [Object],
viewId: String,
shared: Boolean,
},
data: () => ({
sortList: [],
}),
computed: {
columns() {
if (!this.meta || !this.meta.columns) {
return []
}
return this.meta.columns
.filter((c) => !(c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== RelationTypes.BELONGS_TO))
.map((c) => ({
...c,
icon: getUIDTIcon(c.uidt),
}))
},
},
watch: {
value(v) {
this.sortList = v || []
},
async viewId(v) {
if (v) {
await this.loadSortList()
}
},
},
async created() {
this.sortList = this.value || []
this.loadSortList()
},
methods: {
addSort() {
this.sortList.push({
fk_column_id: null,
direction: 'asc',
})
this.sortList = this.sortList.slice()
this.$e('a:sort:add', { length: this.sortList.length })
},
async loadSortList() {
if (!this.shared) {
// && !this._isUIAllowed('sortSync')) {
let sortList = []
if (this.viewId) {
const data = await this.$api.dbTableSort.list(this.viewId)
sortList = data.sorts.list
}
this.sortList = sortList
}
},
async saveOrUpdate(sort, i) {
if (!this.shared && this._isUIAllowed('sortSync')) {
if (sort.id) {
await this.$api.dbTableSort.update(sort.id, sort)
} else {
this.$set(this.sortList, i, await this.$api.dbTableSort.create(this.viewId, sort))
}
} else {
this.$emit('input', this.sortList)
}
this.$emit('updated')
this.$e('a:sort:dir', { direction: sort.direction })
},
async deleteSort(sort, i) {
if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
await this.$api.dbTableSort.delete(sort.id)
await this.loadSortList()
} else {
this.sortList.splice(i, 1)
this.$emit('input', this.sortList)
}
this.$emit('updated')
this.$e('a:sort:delete')
},
},
}
</script>
<template>
<v-menu offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-badge :value="sortList && sortList.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--text text--darken-3': sortList && sortList.length,
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-sort </v-icon>
<!-- Sort -->
{{ $t('activity.sort') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
</v-badge>
</template>
<div class="backgroundColor pa-2 menu-filter-dropdown" style="min-width: 330px">
<div class="sort-grid" @click.stop>
<template v-for="(sort, i) in sortList || []" dense>
<v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon>
<FieldListAutoCompleteDropdown
:key="`${i}sel1`"
v-model="sort.fk_column_id"
class="caption nc-sort-field-select"
:columns="columns"
@click.stop
@change="saveOrUpdate(sort, i)"
/>
<v-select
:key="`${i}sel2`"
v-model="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="[
{ text: 'A -> Z', value: 'asc' },
{ text: 'Z -> A', value: 'desc' },
]"
:label="$t('labels.operation')"
solo
flat
dense
hide-details
@click.stop
@change="saveOrUpdate(sort, i)"
>
<template #item="{ item }">
<span class="caption font-weight-regular">{{ item.text }}</span>
</template>
</v-select>
</template>
</div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addSort">
<v-icon small color="grey"> mdi-plus </v-icon>
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</v-btn>
</div>
</v-menu>
</template>
<style scoped>
.sort-grid {
display: grid;
grid-template-columns: 22px auto 100px;
column-gap: 6px;
row-gap: 6px;
}
</style>

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

@ -174,7 +174,7 @@ todo :
v-on="parentListeners" v-on="parentListeners"
/> --> /> -->
<CellBoolean v-else-if="isBoolean" v-model="localState" /> <CellCheckbox v-else-if="isBoolean" v-model="localState" />
<!-- &lt;!&ndash; v-model="localState" --> <!-- &lt;!&ndash; v-model="localState" -->
<!-- :column="column" --> <!-- :column="column" -->
<!-- :is-form="isForm" --> <!-- :is-form="isForm" -->

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

@ -0,0 +1,9 @@
<script setup lang="ts"></script>
<template>
<v-toolbar dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom mx-1" style="z-index: 7">
<SmartsheetToolbarFieldsMenu :show-system-fields="false" />
</v-toolbar>
</template>
<style scoped></style>

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

@ -28,7 +28,7 @@ watch(
<template> <template>
<div class="overflow-auto"> <div class="overflow-auto">
<v-toolbar dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom mx-1 z-7" /> <SmartsheetToolbar />
<template v-if="meta && tabMeta"> <template v-if="meta && tabMeta">
<SmartsheetGrid /> <SmartsheetGrid />
</template> </template>

15
packages/nc-gui-v2/composables/useMeta.ts

@ -0,0 +1,15 @@
import { TableType } from 'nocodb-sdk'
import useMetas from '~/composables/useMetas'
export default function (metaIdOrTitle: string) {
const { metas, getMeta } = useMetas()
const meta = computed(() => {
return metas.value?.[metaIdOrTitle]
})
const loadMeta = async () => {
await getMeta(metaIdOrTitle)
}
return { meta, loadMeta }
}

30
packages/nc-gui-v2/composables/useViewColumn.ts

@ -0,0 +1,30 @@
import { useNuxtApp } from '#app'
export default function (viewId: string) {
const columns = ref<any[]>()
const { metas, getMeta } = useMetas()
const { $api } = useNuxtApp()
const loadColumns = async () => {
const data = await $api.dbViewColumn.list(viewId)
const fieldById = data.reduce(
(o, f) => ({
...o,
[f.fk_column_id]: f,
}),
{},
)
// const
// 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);
}
return {}
}

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

File diff suppressed because it is too large Load Diff

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

@ -14,6 +14,7 @@
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
"socket.io-client": "^4.5.1", "socket.io-client": "^4.5.1",
"vite-plugin-vuetify": "^1.0.0-alpha.12",
"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" "vuetify": "^3.0.0-alpha.13"

Loading…
Cancel
Save