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