mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
15 changed files with 2097 additions and 109 deletions
@ -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> |
@ -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> |
@ -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--text text--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--text" @click.stop="showAll"> |
||||
<!-- Show All --> |
||||
{{ $t('general.showAll') }} |
||||
</v-btn> |
||||
<v-btn small class="elevation-0 grey--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--text text--darken-3': isAnyFieldHidden, |
||||
}" |
||||
v-on="on" |
||||
> |
||||
<v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> |
||||
<!– Fields –> |
||||
{{ $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 |
||||
> |
||||
<!– <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--text" @click.stop="showAll"> |
||||
<!– Show All –> |
||||
{{ $t('general.showAll') }} |
||||
</v-btn> |
||||
<v-btn small class="elevation-0 grey--text" @click.stop="hideAll"> |
||||
<!– Hide All –> |
||||
{{ $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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 } |
||||
} |
@ -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 {} |
||||
} |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue