Browse Source

wip(gui-v2): filter and sort

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/2716/head
Pranav C 2 years ago
parent
commit
033b09716e
  1. 6
      packages/nc-gui-v2/components/index.ts
  2. 167
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  3. 48
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  4. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  5. 20
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  6. 10
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  7. 2
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  8. 22
      packages/nc-gui-v2/composables/useViewColumns.ts
  9. 77
      packages/nc-gui-v2/composables/useViewFilters.ts
  10. 24
      packages/nc-gui-v2/composables/useViewSorts.ts
  11. 54
      packages/nc-gui-v2/utils/filterUtils.ts
  12. 9
      packages/nocodb-sdk/src/lib/Api.ts
  13. 17
      scripts/sdk/swagger.json

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

@ -1,4 +1,4 @@
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { ColumnType, FormType, GalleryType, GridType, KanbanType, TableType } from 'nocodb-sdk'
import type { InjectionKey, Ref } from 'vue'
import type useViewData from '~/composables/useViewData'
@ -10,7 +10,9 @@ export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['pag
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')
export const IsFormInj: InjectionKey<boolean> = Symbol('is-form-injection')
export const IsGridInj: InjectionKey<boolean> = Symbol('is-grid-injection')
export const IsLockedInj: InjectionKey<boolean> = Symbol('is-locked-injection')
export const ValueInj: InjectionKey<any> = Symbol('value-injection')
export const ActiveViewInj: InjectionKey<any> = Symbol('active-view-injection')
export const ActiveViewInj: InjectionKey<Ref<(GridType | GalleryType | FormType | KanbanType) & { id?: string }>> =
Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<any> = Symbol('readonly-injection')
export const ReloadViewDataInj: InjectionKey<any> = Symbol('reload-view-data-injection')

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

@ -1,5 +1,18 @@
<script>
import { UITypes, getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
<script setup lang="ts">
import { inject } from 'vue'
import { ActiveViewInj, MetaInj } from '~/components'
import useViewFilters from '~/composables/useViewFilters'
import { comparisonOp } from '~/utils/comparisonOp'
const meta = inject(MetaInj)
const activeView = inject(ActiveViewInj)
const { filters, deleteFilter, saveOrUpdate } = useViewFilters(activeView)
const filterUpdateCondition = (filter, i) => {}
const filterComparisonOp = (filter) => {}
/* import { UITypes, getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
export default {
@ -302,11 +315,149 @@ export default {
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">
<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>
</div>
<!-- <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'">
@ -443,20 +594,20 @@ export default {
</template>
</div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addFilter">
<v-btn small class="elevation-0 grey&#45;&#45;text my-3" @click.stop="addFilter">
<v-icon small color="grey"> mdi-plus </v-icon>
<!-- Add Filter -->
&lt;!&ndash; Add Filter &ndash;&gt;
{{ $t('activity.addFilter') }}
</v-btn>
<slot />
</div>
</div> -->
</template>
<style scoped>
.grid {
/*.grid {
display: grid;
grid-template-columns: 22px 80px auto auto auto;
column-gap: 6px;
row-gap: 6px;
}
}*/
</style>

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

@ -1,5 +1,19 @@
<script>
import ColumnFilter from '~/components/project/spreadsheet/components/ColumnFilter'
<script setup lang="ts">
// todo: move to persisted state
import { useState } from '#app'
import { IsLockedInj } from '~/components'
import Smartsheet from '~/components/tabs/Smartsheet.vue'
const autoApplyFilter = useState('autoApplyFilter', () => false)
const isLocked = inject(IsLockedInj)
// todo: emit from child
const filters = []
// todo: implement
const applyChanges = () => {}
/* import ColumnFilter from '~/components/project/spreadsheet/components/ColumnFilter'
export default {
name: 'ColumnFilterMenu',
@ -49,12 +63,12 @@ export default {
this.$e('a:filter:apply')
},
},
}
} */
</script>
<template>
<v-menu offset-y eager transition="slide-y-transition">
<template #activator="{ on }">
<template #activator="props">
<v-badge :value="filters.length" color="primary" dot overlap>
<v-btn
v-t="['c:filter']"
@ -66,28 +80,28 @@ export default {
:class="{
'primary lighten-5 grey--text text--darken-3': filters.length,
}"
v-on="on"
v-bind="props"
>
<v-icon small class="mr-1" color="grey darken-3"> mdi-filter-outline </v-icon>
<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-icon small color="#777"> mdi-menu-down</v-icon>
</v-btn>
</v-badge>
</template>
<ColumnFilter
ref="filter"
<SmartsheetToolbarColumnFilter ref="filter">
<!--
v-model="filters"
:shared="shared"
:view-id="viewId"
:field-list="fieldList"
:meta="meta"
v-on="$listeners"
>
:meta="meta" -->
<!-- v-on="$listeners" -->
<div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
id="col-filter-checkbox"
v-model="autosave"
v-model="autoApplyFilter"
class="col-filter-checkbox"
hide-details
dense
@ -103,14 +117,14 @@ export default {
</v-checkbox>
<v-spacer />
<v-btn v-show="!autosave" color="primary" small class="caption ml-2" @click="applyChanges"> Apply changes </v-btn>
<v-btn v-show="!autoApplyFilter" color="primary" small class="caption ml-2" @click="applyChanges"> Apply changes </v-btn>
</div>
</ColumnFilter>
</SmartsheetToolbarColumnFilter>
</v-menu>
</template>
<style scoped>
/deep/ .col-filter-checkbox .v-input--selection-controls__input {
/*/deep/ .col-filter-checkbox .v-input--selection-controls__input {
transform: scale(0.7);
}
}*/
</style>

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

@ -13,9 +13,8 @@ const { showSystemFields, fieldsOrder, coverImageField, modelValue } = definePro
}>()
const meta = inject(MetaInj)
const isLocked = false
const activeView = inject(ActiveViewInj)
const isLocked = false
const isAnyFieldHidden = computed(() => {
return false
@ -23,13 +22,13 @@ const isAnyFieldHidden = computed(() => {
// return meta?.fields?.some(field => field.hidden)
})
const { fields, loadViewColumns, filteredFieldList, filterQuery, showAll, hideAll, sync } = useViewColumns()
const { fields, loadViewColumns, filteredFieldList, filterQuery, showAll, hideAll, sync } = useViewColumns(activeView, meta)
watch(
() => activeView?.value?.id,
async (newVal, oldVal) => {
if (newVal !== oldVal && meta?.value) {
await loadViewColumns(meta, newVal)
await loadViewColumns()
}
},
{ immediate: true },
@ -300,7 +299,7 @@ export default {
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap v-bind="props">
<v-btn
v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border "
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small

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

@ -1,5 +1,5 @@
<script>
import { RelationTypes, UITypes } from 'nocodb-sdk'
/* import { RelationTypes, UITypes } from 'nocodb-sdk'
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes'
import FieldListAutoCompleteDropdown from '~/components/project/spreadsheet/components/FieldListAutoCompleteDropdown'
@ -92,11 +92,11 @@ export default {
this.$e('a:sort:delete')
},
},
}
} */
</script>
<template>
<v-menu offset-y transition="slide-y-transition">
<!-- <v-menu offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-badge :value="sortList && sortList.length" color="primary" dot overlap>
<v-btn
@ -107,12 +107,12 @@ export default {
text
outlined
:class="{
'primary lighten-5 grey--text text--darken-3': sortList && sortList.length,
'primary lighten-5 grey&#45;&#45;text text&#45;&#45;darken-3': sortList && sortList.length,
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-sort </v-icon>
<!-- Sort -->
&lt;!&ndash; Sort &ndash;&gt;
{{ $t('activity.sort') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
@ -153,20 +153,20 @@ export default {
</v-select>
</template>
</div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addSort">
<v-btn small class="elevation-0 grey&#45;&#45;text my-3" @click.stop="addSort">
<v-icon small color="grey"> mdi-plus </v-icon>
<!-- Add Sort Option -->
&lt;!&ndash; Add Sort Option &ndash;&gt;
{{ $t('activity.addSort') }}
</v-btn>
</div>
</v-menu>
</v-menu> -->
</template>
<style scoped>
.sort-grid {
/*.sort-grid {
display: grid;
grid-template-columns: 22px auto 100px;
column-gap: 6px;
row-gap: 6px;
}
}*/
</style>

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

@ -1,15 +1,7 @@
<script lang="ts" setup>
import { isVirtualCol } from 'nocodb-sdk'
import { inject, onKeyStroke, onMounted, provide } from '#imports'
import {
ActiveViewInj,
ChangePageInj,
IsFormInj,
IsGridInj,
MetaInj,
PaginationDataInj,
ReloadViewDataInj
} from "~/components";
import { ActiveViewInj, ChangePageInj, IsFormInj, IsGridInj, MetaInj, PaginationDataInj, ReloadViewDataInj } from '~/components'
import Smartsheet from '~/components/tabs/Smartsheet.vue'
import useViewData from '~/composables/useViewData'

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

@ -3,6 +3,8 @@
<template>
<v-toolbar dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom" style="z-index: 7">
<SmartsheetToolbarFieldsMenu :show-system-fields="false" />
<SmartsheetToolbarColumnFilterMenu />
<!-- <SmartsheetToolbarSortListMenu /> -->
</v-toolbar>
</template>

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

@ -1,8 +1,12 @@
import type { TableType } from 'nocodb-sdk'
import type { FormType, GalleryType, GridType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#app'
export default function () {
export default function (
view: Ref<(GridType | FormType | GalleryType) & { id?: string }> | undefined,
meta: Ref<TableType> | undefined,
isPublic = false,
) {
const fields = ref<
{
order?: number
@ -11,7 +15,6 @@ export default function () {
fk_column_id?: string
}[]
>()
let viewId: string
const filterQuery = ref('')
const filteredFieldList = computed(() => {
@ -22,11 +25,12 @@ export default function () {
const { $api } = useNuxtApp()
const loadViewColumns = async (meta: Ref<TableType>, _viewId: string, isPublic = false) => {
viewId = _viewId
const loadViewColumns = async () => {
if (!meta || !view) return
let order = 1
if (viewId) {
const data = await $api.dbViewColumn.list(viewId)
if (view?.value?.id) {
const data = await $api.dbViewColumn.list(view?.value?.id as string)
const fieldById: Record<string, any> = data.reduce((o: Record<string, any>, f: any) => {
f.show = !!f.show
return {
@ -52,9 +56,9 @@ export default function () {
const sync = async (field: any, index: number) => {
if (field.id) {
await $api.dbViewColumn.update(viewId, field.id, field)
await $api.dbViewColumn.update(view?.value?.id as string, field.id, field)
} else {
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(viewId, field)) as any
if (fields.value) fields.value[index] = (await $api.dbViewColumn.create(view?.value?.id as string, field)) as any
}
}

77
packages/nc-gui-v2/composables/useViewFilters.ts

@ -0,0 +1,77 @@
import type { FilterType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
export default function (view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined, parentId?: string) {
const filters = ref<(FilterType & { status?: 'update' | 'delete' })[]>([])
const { $api } = useNuxtApp()
const loadFilters = async () => {
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string)
}
}
const sync = async (_nested = false) => {
for (const [i, filter] of Object.entries(filters.value)) {
if (filter.status === 'delete') {
await $api.dbTableFilter.delete(filter.id as string)
} else if (filter.status === 'update') {
await $api.dbTableFilter.update(filter.id as string, {
...filter,
fk_parent_id: parentId,
})
} else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})) as any
}
}
}
const deleteFilter = async (filter: FilterType, i: number) => {
// 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')
// // },
}
const saveOrUpdate = async (filter: FilterType, i: number) => {
// 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')
// // },
}
return { filters, loadFilters, sync, deleteFilter, saveOrUpdate }
}

24
packages/nc-gui-v2/composables/useViewSorts.ts

@ -0,0 +1,24 @@
import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
export default function (view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }>) {
const sorts = ref<SortType[]>([])
const { $api } = useNuxtApp()
const loadSorts = async () => {
sorts.value = (await $api.dbTableSort.list(view?.value?.id as string)) as any[]
}
const sync = async (sort: SortType, i: number) => {
if (!sorts?.value) return
if (sort.id) {
await $api.dbTableSort.update(sort.id, sort)
} else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
}
}
return { sorts, loadSorts, sync }
}

54
packages/nc-gui-v2/utils/filterUtils.ts

@ -0,0 +1,54 @@
export const 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',
},
]

9
packages/nocodb-sdk/src/lib/Api.ts

@ -2096,12 +2096,13 @@ export class Api<
* @tags DB table filter
* @name Read
* @request GET:/api/v1/db/meta/views/{viewId}/filters
* @response `200` `FilterListType`
* @response `200` `(FilterType)[]` OK
*/
read: (viewId: string, params: RequestParams = {}) =>
this.request<FilterListType, any>({
this.request<FilterType[], any>({
path: `/api/v1/db/meta/views/${viewId}/filters`,
method: 'GET',
format: 'json',
...params,
}),
@ -2176,10 +2177,10 @@ export class Api<
* @tags DB table filter
* @name ChildrenRead
* @request GET:/api/v1/db/meta/filters/{filterGroupId}/children
* @response `200` `FilterType` OK
* @response `200` `(FilterType)[]` OK
*/
childrenRead: (filterGroupId: string, params: RequestParams = {}) =>
this.request<FilterType, any>({
this.request<FilterType[], any>({
path: `/api/v1/db/meta/filters/${filterGroupId}/children`,
method: 'GET',
format: 'json',

17
scripts/sdk/swagger.json

@ -1946,7 +1946,17 @@
"operationId": "db-table-filter-read",
"responses": {
"200": {
"$ref": "#/components/responses/FilterList"
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Filter"
}
}
}
}
}
},
"tags": [
@ -2104,7 +2114,10 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Filter"
"type": "array",
"items": {
"$ref": "#/components/schemas/Filter"
}
}
}
}

Loading…
Cancel
Save