多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

423 lines
14 KiB

feat: Improved UI (#6222) * feat: Improved ui (#6156) * refactor: revert Signed-off-by: Pranav C <pranavxc@gmail.com> feat: shared base Signed-off-by: Pranav C <pranavxc@gmail.com> fix: remove duplicate import statement Signed-off-by: Pranav C <pranavxc@gmail.com> fix: disable starred & license menu Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix airtable wait issue Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable mysql in ci Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix checkbox order for sqlite Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: disable quick tests Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: fix dbType env variable for CI Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: workspace API access error fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable SQLite CI CD Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: use DB_TYPE env variable Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: enable SQLite UT Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: isHub cleanup Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: add check for EE Timezone spec Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: cleanup Signed-off-by: Pranav C <pranavxc@gmail.com> chore: cleanup Signed-off-by: Pranav C <pranavxc@gmail.com> test: EE check fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: test correction Signed-off-by: Pranav C <pranavxc@gmail.com> chore: sync latest changes Signed-off-by: Pranav C <pranavxc@gmail.com> test: set EE=false Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> test: set NC Edition to community in workflow file Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> chore: update sdk build command Signed-off-by: Pranav C <pranavxc@gmail.com> refactor: i18n and other changes Signed-off-by: Pranav C <pranavxc@gmail.com> feat: new ui Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: sync tests Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: lint Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: shared view/base related bugs Signed-off-by: Pranav C <pranavxc@gmail.com> * test: checkbox verification sort order fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix sqlite reset Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable selfhosted runners Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: table ops (draft) Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * Docs: screenshots for table-operations.md * refactor: introduce missing buttons Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: get all fields Signed-off-by: Pranav C <pranavxc@gmail.com> * test: UT fix- new data API response Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: EE is false Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: webhook lookup as string in CE Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: include created_at and updated_at Signed-off-by: Pranav C <pranavxc@gmail.com> * test: fix UT newDataAPI response for PG Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: separate api for webhook related plugins Signed-off-by: Pranav C <pranavxc@gmail.com> * test: msyql filter corrections Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: mysql group by test corrections Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix datatype for rating field in groupby spec for pg Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: kanban datatype correction Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: column edit for mysql- rating field Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: misc fixes Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable 4 workers Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: enable 2 workers per shard only Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: table CRUD * Rename table-operations.md to table-crud.md * Create column-crud.md * docs: row CRUD * Rename row.md to row-crud.md * docs: project crud * docs: toolbar (skeleton) * refactor: single page UI and bug fixes Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: sync tests playwright Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: add missing dependency Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: single page ui, test corrections Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: tests Signed-off-by: Pranav C <pranavxc@gmail.com> * test: project rename test correction Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: remove only Signed-off-by: Pranav C <pranavxc@gmail.com> * test: remove wrong import statement Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: delete option not visible in project context menu Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: move ws access within isEE() Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: fix groupby * test: groupby fix Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: signup & landing page * docs: project crud * docs: project-crud misc * docs: toolbar fields * docs: toolbar / filters * docs: toolbar / group by * docs: toolbar / sort * docs: toolbar / row height * docs: filters additional options * docs: file re-order Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: add links to column types * docs: code snippets * docs: links * docs: lookup * docs: rollup * docs: formula * docs: primary key * docs: display value * docs: development setup * docs: swagger * fix(nc-gui): encodeURIComponent for row id - closes: #6202 * docs: language * docs: expanded record * docs: import airtable * docs: airtable * docs: webhook * docs: revert file rename Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * docs: account settings * docs: audit * docs: meta management * docs: project settings * docs: shared base * docs: shared view * docs: meta sync * docs: team-auth * docs: views * docs: fix URL * docs: URL corrections * fix: shared base, view related bugs Signed-off-by: Pranav C <pranavxc@gmail.com> * test: EE check for WSaccess Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * test: exclude EE tests Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> * fix: missing project delete closes #6215 Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: merge existing project meta if found closes #6216 Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: merge existing project meta if found closes #6216 Signed-off-by: Pranav C <pranavxc@gmail.com> --------- Signed-off-by: Pranav C <pranavxc@gmail.com> Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: DarkPhoenix2704 <anbarasun123@gmail.com> Co-authored-by: Wing-Kam Wong <wingkwong.code@gmail.com> * refactor: docs and other bug fixes Signed-off-by: Pranav C <pranavxc@gmail.com> * feat: populate default project on super admin signup Signed-off-by: Pranav C <pranavxc@gmail.com> * fix: include created project details in signup response if avail, missing Dockerfile Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: use custom function for resolving ts path aliases Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: add missing generate script Signed-off-by: Pranav C <pranavxc@gmail.com> * chore: webpack build correction - ts path resolve Signed-off-by: Pranav C <pranavxc@gmail.com> --------- Signed-off-by: Pranav C <pranavxc@gmail.com> Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: mertmit <mertmit99@gmail.com> Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com> Co-authored-by: DarkPhoenix2704 <anbarasun123@gmail.com> Co-authored-by: Wing-Kam Wong <wingkwong.code@gmail.com>
1 year ago
import { type ColumnType, type SelectOptionsType, UITypes, type ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { GROUP_BY_VARS, ref, storeToRefs, useApi, useProject } from '#imports'
import type { Group, GroupNestedIn, Row } from '#imports'
export const useViewGroupBy = (view: Ref<ViewType | undefined>, where?: ComputedRef<string | undefined>) => {
const { api } = useApi()
const { project } = storeToRefs(useProject())
const { sharedView, fetchSharedViewData } = useSharedView()
const meta = inject(MetaInj)
const { gridViewCols } = useGridViewColumnOrThrow()
const groupBy = computed<{ column: ColumnType; sort: string; order?: number }[]>(() => {
const tempGroupBy: { column: ColumnType; sort: string; order?: number }[] = []
Object.values(gridViewCols.value).forEach((col) => {
if (col.group_by) {
const column = meta?.value.columns?.find((f) => f.id === col.fk_column_id)
if (column) {
tempGroupBy.push({
column,
sort: col.group_by_sort || 'asc',
order: col.group_by_order || 1,
})
}
}
})
tempGroupBy.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
return tempGroupBy
})
const isGroupBy = computed(() => !!groupBy.value.length)
const { isUIAllowed } = useUIPermission()
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false))
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const groupByLimit = 10
const rootGroup = ref<Group>({
key: 'root',
color: 'root',
count: 0,
column: {} as any,
nestedIn: [],
paginationData: { page: 1, pageSize: groupByLimit },
nested: true,
children: [],
root: true,
})
async function groupWrapperChangePage(page: number, groupWrapper?: Group) {
groupWrapper = groupWrapper || rootGroup.value
if (!groupWrapper) return
groupWrapper.paginationData.page = page
await loadGroups(
{
offset: (page - 1) * (groupWrapper.paginationData.pageSize || groupByLimit),
} as any,
groupWrapper,
)
}
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
row: { ...row },
oldRow: { ...row },
rowMeta: {},
}))
const valueToTitle = (value: string, col: ColumnType) => {
if (col.uidt === UITypes.Checkbox) {
return value ? GROUP_BY_VARS.TRUE : GROUP_BY_VARS.FALSE
}
return value ?? GROUP_BY_VARS.NULL
}
const colors = ref(enumColor.light)
const nextGroupColor = ref(colors.value[0])
const getNextColor = () => {
const tempColor = nextGroupColor.value
const index = colors.value.indexOf(nextGroupColor.value)
if (index === colors.value.length - 1) {
nextGroupColor.value = colors.value[0]
} else {
nextGroupColor.value = colors.value[index + 1]
}
return tempColor
}
const findKeyColor = (key?: string, col?: ColumnType): string => {
if (col) {
switch (col.uidt) {
case UITypes.MultiSelect: {
const keys = key?.split(',') || []
const colors = []
for (const k of keys) {
const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === k)
if (option) {
colors.push(option.color)
}
}
return colors.join(',')
}
case UITypes.SingleSelect: {
const option = (col.colOptions as SelectOptionsType).options?.find((o) => o.title === key)
if (option) {
return option.color || getNextColor()
}
return 'gray'
}
case UITypes.Checkbox: {
if (key) {
return themeColors.success
}
return themeColors.error
}
default:
return key ? getNextColor() : 'gray'
}
}
return key ? getNextColor() : 'gray'
}
const calculateNestedWhere = (nestedIn: GroupNestedIn[], existing = '') => {
return nestedIn.reduce((acc, curr) => {
if (curr.key === GROUP_BY_VARS.NULL) {
acc += `${acc.length ? '~and' : ''}(${curr.title},blank)`
} else if (curr.column_uidt === UITypes.Checkbox) {
acc += `${acc.length ? '~and' : ''}(${curr.title},${curr.key === GROUP_BY_VARS.TRUE ? 'checked' : 'notchecked'})`
} else if ([UITypes.Date, UITypes.DateTime].includes(curr.column_uidt as UITypes)) {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,exactDate,${curr.key})`
} else {
acc += `${acc.length ? '~and' : ''}(${curr.title},eq,${curr.key})`
}
return acc
}, existing)
}
async function loadGroups(params: any = {}, group?: Group) {
group = group || rootGroup.value
if (!project?.value?.id || !view.value?.id || !view.value?.fk_model_id || !group) return
if (groupBy.value.length === 0) {
group.children = []
return
}
if (group.nestedIn.length > groupBy.value.length) return
if (group.nestedIn.length === 0) nextGroupColor.value = colors.value[0]
const groupby = groupBy.value[group.nestedIn.length]
const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value)
if (!groupby || !groupby.column.column_name) return
if (isPublic.value && !sharedView.value?.uuid) {
return
}
const response = !isPublic.value
? await api.dbViewRow.groupBy('noco', project.value.id, view.value.fk_model_id, view.value.id, {
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByLimit),
limit: group.paginationData.pageSize ?? groupByLimit,
...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: `${nestedWhere}`,
sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`,
column_name: groupby.column.column_name,
} as any)
: await api.public.dataGroupBy(sharedView.value!.uuid!, {
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByLimit),
limit: group.paginationData.pageSize ?? groupByLimit,
...params,
where: nestedWhere,
sort: `${groupby.sort === 'desc' ? '-' : ''}${groupby.column.title}`,
column_name: groupby.column.column_name,
sortsArr: sorts.value,
filtersArr: nestedFilters.value,
})
const tempList: Group[] = response.list.reduce((acc: Group[], curr: Record<string, any>) => {
const keyExists = acc.find((a) => a.key === valueToTitle(curr[groupby.column.column_name!], groupby.column))
if (keyExists) {
keyExists.count += +curr.count
keyExists.paginationData = { page: 1, pageSize: groupByLimit, totalRows: keyExists.count }
return acc
}
if (groupby.column.title && groupby.column.column_name && groupby.column.uidt) {
acc.push({
key: valueToTitle(curr[groupby.column.column_name!], groupby.column),
column: groupby.column,
count: +curr.count,
color: findKeyColor(curr[groupby.column.column_name!], groupby.column),
nestedIn: [
...group!.nestedIn,
{
title: groupby.column.title,
column_name: groupby.column.column_name!,
key: valueToTitle(curr[groupby.column.column_name!], groupby.column),
column_uidt: groupby.column.uidt,
},
],
paginationData: { page: 1, pageSize: groupByLimit, totalRows: +curr.count },
nested: group!.nestedIn.length < groupBy.value.length - 1,
})
}
return acc
}, [])
if (!group.children) group.children = []
for (const temp of tempList) {
const keyExists = group.children?.find((a) => a.key === temp.key)
if (keyExists) {
temp.paginationData = {
page: keyExists.paginationData.page || temp.paginationData.page,
pageSize: keyExists.paginationData.pageSize || temp.paginationData.pageSize,
totalRows: temp.count,
}
temp.color = keyExists.color
// update group
Object.assign(keyExists, temp)
continue
}
group.children.push(temp)
}
// clear rest of the children
group.children = group.children.filter((c) => tempList.find((t) => t.key === c.key))
if (group.count <= (group.paginationData.pageSize ?? groupByLimit)) {
group.children.sort((a, b) => {
const orderA = tempList.findIndex((t) => t.key === a.key)
const orderB = tempList.findIndex((t) => t.key === b.key)
return orderA - orderB
})
}
group.paginationData = response.pageInfo
// to cater the case like when querying with a non-zero offset
// the result page may point to the target page where the actual returned data don't display on
const expectedPage = Math.max(1, Math.ceil(group.paginationData.totalRows! / group.paginationData.pageSize!))
if (expectedPage < group.paginationData.page!) {
await groupWrapperChangePage(expectedPage, group)
}
}
async function loadGroupData(group: Group, force = false) {
if (!project?.value?.id || !view.value?.id || !view.value?.fk_model_id) return
if (group.children && !force) return
if (!group.paginationData) {
group.paginationData = { page: 1, pageSize: groupByLimit }
}
const nestedWhere = calculateNestedWhere(group.nestedIn, where?.value)
const query = {
offset: ((group.paginationData.page ?? 0) - 1) * (group.paginationData.pageSize ?? groupByLimit),
limit: group.paginationData.pageSize ?? groupByLimit,
where: `${nestedWhere}`,
}
const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id, view.value.fk_model_id, view.value.id, {
...query,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
} as any)
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query })
group.count = response.pageInfo.totalRows ?? 0
group.rows = formatData(response.list)
group.paginationData = response.pageInfo
}
const loadGroupPage = async (group: Group, p: number) => {
if (!group.paginationData) {
group.paginationData = { page: 1, pageSize: groupByLimit }
}
group.paginationData.page = p
await loadGroupData(group, true)
}
const refreshNested = (group?: Group, nestLevel = 0) => {
group = group || rootGroup.value
if (!group) return
if (nestLevel < groupBy.value.length) {
group.nested = true
} else {
group.nested = false
}
if (group.nested) {
if (group?.rows) {
group.rows = []
}
} else {
if (group?.children) {
group.children = []
}
}
if (nestLevel > groupBy.value.length) return
for (const child of group.children || []) {
refreshNested(child, nestLevel + 1)
}
}
watch(
() => groupBy.value.length,
async () => {
if (groupBy.value.length > 0) {
rootGroup.value.paginationData = { page: 1, pageSize: groupByLimit }
rootGroup.value.column = {} as any
await loadGroups()
refreshNested()
nextTick(() => reloadViewDataHook?.trigger())
}
},
{ immediate: true },
)
const findGroupByNestedIn = (nestedIn: GroupNestedIn[], group?: Group, nestLevel = 0): Group => {
group = group || rootGroup.value
if (nestLevel >= nestedIn.length) return group
const child = group.children?.find((g) => g.key === nestedIn[nestLevel].key)
if (child) {
if (child.nested) {
return findGroupByNestedIn(nestedIn, child, nestLevel + 1)
}
return child
}
return group
}
const parentGroup = (group: Group) => {
const parent = findGroupByNestedIn(group.nestedIn.slice(0, -1))
return parent
}
const modifyCount = (group: Group, countEffect: number) => {
if (!group) return
group.count += countEffect
// remove group if count is 0
if (group.count === 0) {
const parent = parentGroup(group)
if (parent) {
parent.children = parent.children?.filter((c) => c.key !== group.key)
}
}
if (group.root) return
modifyCount(parentGroup(group), countEffect)
}
const findGroupForRow = (row: Row, group?: Group, nestLevel = 0): { found: boolean; group: Group } => {
group = group || rootGroup.value
if (group.nested) {
const child = group.children?.find((g) => {
if (!groupBy.value[nestLevel].column.title) return undefined
return g.key === valueToTitle(row.row[groupBy.value[nestLevel].column.title!], groupBy.value[nestLevel].column)
})
if (child) {
return findGroupForRow(row, child, nestLevel + 1)
}
return { found: false, group }
}
return { found: true, group }
}
const redistributeRows = (group?: Group) => {
group = group || rootGroup.value
if (!group) return
if (!group.nested && group.rows) {
group.rows.forEach((row) => {
const properGroup = findGroupForRow(row)
if (properGroup.found) {
if (properGroup.group !== group) {
if (properGroup.group) {
properGroup.group.rows?.push(row)
modifyCount(properGroup.group, 1)
}
if (group) {
group.rows?.splice(group!.rows.indexOf(row), 1)
modifyCount(group, -1)
}
}
} else {
if (group) {
group.rows?.splice(group!.rows.indexOf(row), 1)
modifyCount(group, -1)
}
if (properGroup.group?.children) loadGroups({}, properGroup.group)
}
})
} else {
group.children?.forEach((g) => redistributeRows(g))
}
}
return { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows }
}