Browse Source

Merge branch 'develop' into feat/attachments

pull/4931/head
Wing-Kam Wong 2 years ago
parent
commit
b370ff45a9
  1. 7
      .github/workflows/release-nightly-dev.yml
  2. 3
      packages/nc-gui/components/shared-view/Grid.vue
  3. 45
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  4. 11
      packages/nc-gui/components/smartsheet/toolbar/ShareView.vue
  5. 15
      packages/nc-gui/composables/useKanbanViewStore.ts
  6. 30
      packages/nc-gui/composables/useSharedView.ts
  7. 4
      packages/nc-gui/composables/useTable.ts
  8. 7
      packages/nc-gui/layouts/shared-view.vue
  9. 18
      packages/nocodb-sdk/src/lib/Api.ts
  10. 14
      packages/nocodb/src/lib/meta/api/columnApis.ts
  11. 12
      packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts
  12. 2
      packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts
  13. 38
      scripts/sdk/swagger.json
  14. 26
      tests/playwright/tests/viewKanban.spec.ts

7
.github/workflows/release-nightly-dev.yml

@ -12,6 +12,11 @@ jobs:
set-tag:
runs-on: 'ubuntu-latest'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
ref: ${{ github.ref }}
- name: set-tag
id: tag-step
run: |
@ -21,7 +26,7 @@ jobs:
TAG_NAME=${CURRENT_DATE}-${CURRENT_TIME}
IS_DAILY='Y'
# Get current version
CURRENT_VERSION=$(curl -fs https://docs.nocodb.com/releases | grep article | grep div | grep h2 | grep 'id\="[^"]*' -o | cut -c 5-)
CURRENT_VERSION=$(cat ./packages/nocodb/package.json | jq -r ".version")
# Set the tag
if [[ ${{ github.event_name }} == 'workflow_dispatch' ]]; then
IS_DAILY='N'

3
packages/nc-gui/components/shared-view/Grid.vue

@ -23,7 +23,7 @@ const { signedIn } = useGlobal()
const { loadProject } = useProject()
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
const { isLocked } = useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
const reloadEventHook = createEventHook()
@ -33,6 +33,7 @@ provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)
if (signedIn.value) {
try {

45
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -27,7 +27,7 @@ const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const reloadViewMetaHook = inject(ReloadViewMetaHookInj)!
const reloadViewMetaHook = inject(ReloadViewMetaHookInj, undefined)!
const rootFields = inject(FieldsInj)
@ -84,11 +84,31 @@ const onMove = (_event: { moved: { newIndex: number } }) => {
$e('a:fields:reorder')
}
const coverOptions = computed<SelectProps['options']>(() => {
const filterFields =
fields.value
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
}) ?? []
return [{ value: null, label: 'No Image' }, ...filterFields]
})
const coverImageColumnId = computed({
get: () =>
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) && activeView.value?.view
? (activeView.value?.view as GalleryType).fk_cover_image_col_id
: undefined,
get: () => {
const fk_cover_image_col_id =
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) && activeView.value?.view
? (activeView.value?.view as GalleryType).fk_cover_image_col_id
: undefined
// check if `fk_cover_image_col_id` is in `coverOptions`
// e.g. in share view, users may not share the cover image column
if (coverOptions.value?.find((o) => o.value === fk_cover_image_col_id)) return fk_cover_image_col_id
// set to `No Image`
return null
},
set: async (val) => {
if (
(activeView.value?.type === ViewTypes.GALLERY || activeView.value?.type === ViewTypes.KANBAN) &&
@ -108,24 +128,11 @@ const coverImageColumnId = computed({
})
;(activeView.value.view as KanbanType).fk_cover_image_col_id = val
}
reloadViewMetaHook.trigger()
reloadViewMetaHook?.trigger()
}
},
})
const coverOptions = computed<SelectProps['options']>(() => {
const filterFields =
fields.value
?.filter((el) => el.fk_column_id && metaColumnById.value[el.fk_column_id].uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.fk_column_id,
label: field.title,
}
}) ?? []
return [{ value: null, label: 'No Image' }, ...filterFields]
})
const getIcon = (c: ColumnType) =>
h(isVirtualCol(c) ? resolveComponent('SmartsheetHeaderVirtualCellIcon') : resolveComponent('SmartsheetHeaderCellIcon'), {
columnMeta: c,

11
packages/nc-gui/components/smartsheet/toolbar/ShareView.vue

@ -86,10 +86,17 @@ const genShareLink = async () => {
if (!view.value?.id) return
const response = (await $api.dbViewShare.create(view.value.id)) as SharedView
const meta = isString(response.meta) ? JSON.parse(response.meta) : response.meta
shared.value = { ...response, meta }
if (shared.value.type === ViewTypes.KANBAN) {
const { groupingFieldColumn } = useKanbanViewStoreOrThrow()
shared.value.meta = { ...shared.value.meta, groupingFieldColumn: groupingFieldColumn.value }
await updateSharedViewMeta(true)
}
passwordProtected.value = !!shared.value.password && shared.value.password !== ''
showShareModel = true
@ -133,7 +140,7 @@ async function saveTheme() {
// const saveTransitionDuration = useDebounceFn(updateSharedViewMeta, 1000, { maxWait: 2000 })
async function updateSharedViewMeta() {
async function updateSharedViewMeta(silentMessage = false) {
try {
const meta = shared.value.meta && isString(shared.value.meta) ? JSON.parse(shared.value.meta) : shared.value.meta
@ -141,7 +148,7 @@ async function updateSharedViewMeta() {
meta,
})
message.success(t('msg.success.updated'))
if (!silentMessage) message.success(t('msg.success.updated'))
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}

15
packages/nc-gui/composables/useKanbanViewStore.ts

@ -112,7 +112,10 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
let res
if (isPublic.value) {
res = await fetchSharedViewGroupedData(groupingFieldColumn!.value!.id!)
res = await fetchSharedViewGroupedData(groupingFieldColumn!.value!.id!, {
sortsArr: sorts.value,
filtersArr: nestedFilters.value,
})
} else {
res = await api.dbViewRow.groupedDataList(
'noco',
@ -138,6 +141,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
if (stackTitle === null) {
where = `(${groupingField.value},is,null)`
}
const response = !isPublic.value
? await api.dbViewRow.list('noco', project.value.id!, meta.value!.id!, viewMeta.value!.id!, {
...params,
@ -145,7 +149,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where,
})
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value })
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, offset: params.offset })
formattedData.value.set(stackTitle, [...formattedData.value.get(stackTitle)!, ...formatData(response.list)])
}
@ -155,9 +159,12 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
kanbanMetaData.value = isPublic.value
? (sharedView.value?.view as KanbanType)
: await $api.dbView.kanbanRead(viewMeta.value.id)
// set groupingField
groupingFieldColumn.value =
(meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {}
groupingFieldColumn.value = !isPublic.value
? (meta.value.columns as ColumnType[]).filter((f) => f.id === kanbanMetaData.value.fk_grp_col_id)[0] || {}
: ((typeof sharedView.value?.meta === 'string' ? JSON.parse(sharedView.value?.meta) : sharedView.value?.meta)
.groupingFieldColumn! as ColumnType)
groupingField.value = groupingFieldColumn.value.title!

30
packages/nc-gui/composables/useSharedView.ts

@ -70,6 +70,7 @@ export function useSharedView() {
meta.value = { ...viewMeta.model }
let order = 1
meta.value!.columns = [...viewMeta.model.columns]
.filter((c) => c.show)
.map((c) => ({ ...c, order: order++ }))
@ -92,16 +93,27 @@ export function useSharedView() {
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
}
const fetchSharedViewData = async ({ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] }) => {
const fetchSharedViewData = async ({
sortsArr,
filtersArr,
offset,
}: {
sortsArr: SortType[]
filtersArr: FilterType[]
offset?: number
}) => {
if (!sharedView.value) return
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
if (!offset) {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit
offset = (page - 1) * pageSize
}
const { data } = await $api.public.dataList(
sharedView.value.uuid!,
{
offset: (page - 1) * pageSize,
offset,
filterArrJson: JSON.stringify(filtersArr ?? nestedFilters.value),
sortArrJson: JSON.stringify(sortsArr ?? sorts.value),
} as any,
@ -114,7 +126,10 @@ export function useSharedView() {
return data
}
const fetchSharedViewGroupedData = async (columnId: string, params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
const fetchSharedViewGroupedData = async (
columnId: string,
{ sortsArr, filtersArr }: { sortsArr: SortType[]; filtersArr: FilterType[] },
) => {
if (!sharedView.value) return
const page = paginationData.value.page || 1
@ -125,9 +140,8 @@ export function useSharedView() {
columnId,
{
offset: (page - 1) * pageSize,
filterArrJson: JSON.stringify(nestedFilters.value),
sortArrJson: JSON.stringify(sorts.value),
...params,
filterArrJson: JSON.stringify(filtersArr ?? nestedFilters.value),
sortArrJson: JSON.stringify(sortsArr ?? sorts.value),
} as any,
{
headers: {

4
packages/nc-gui/composables/useTable.ts

@ -1,5 +1,5 @@
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import {
Modal,
SYSTEM_COLUMNS,
@ -84,7 +84,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void, baseId?
async onOk() {
try {
const meta = (await getMeta(table.id as string, true)) as TableType
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord)
const relationColumns = meta?.columns?.filter((c) => c.uidt === UITypes.LinkToAnotherRecord && !isSystemColumn(c))
if (relationColumns?.length) {
const refColMsgs = await Promise.all(

7
packages/nc-gui/layouts/shared-view.vue

@ -1,7 +1,10 @@
<script lang="ts" setup>
import { navigateTo, useEventListener, useRouter } from '#imports'
const { isLoading, currentVersion } = useGlobal()
const { isLoading, appInfo } = useGlobal()
const { sharedView } = useSharedView()
const router = useRouter()
onMounted(() => {
@ -43,7 +46,7 @@ export default {
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" @click="navigateTo('/')">
<a-tooltip placement="bottom">
<template #title>
{{ currentVersion }}
{{ appInfo.version }}
</template>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</a-tooltip>

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

@ -2264,6 +2264,7 @@ export class Api<
*
* @tags DB table column
* @name Delete
* @summary Column Delete
* @request DELETE:/api/v1/db/meta/columns/{columnId}
* @response `200` `void` OK
*/
@ -2274,6 +2275,22 @@ export class Api<
...params,
}),
/**
* No description
*
* @tags DB Table Column
* @name Get
* @summary Column Get
* @request GET:/api/v1/db/meta/columns/{columnId}
* @response `200` `void` OK
*/
get: (columnId: string, params: RequestParams = {}) =>
this.request<void, any>({
path: `/api/v1/db/meta/columns/${columnId}`,
method: 'GET',
...params,
}),
/**
* No description
*
@ -3576,6 +3593,7 @@ export class Api<
where?: string;
/** Query params for nested data */
nested?: any;
offset?: number;
},
params: RequestParams = {}
) =>

14
packages/nocodb/src/lib/meta/api/columnApis.ts

@ -101,6 +101,10 @@ async function createHmAndBtColumn(
}
}
export async function columnGet(req: Request, res: Response) {
res.json(await Column.get({ colId: req.params.columnId }));
}
export async function columnAdd(
req: Request<any, any, ColumnReqType & { uidt: UITypes }>,
res: Response<TableType>
@ -1811,21 +1815,31 @@ async function createColumnIndex({
}
const router = Router({ mergeParams: true });
router.post(
'/api/v1/db/meta/tables/:tableId/columns/',
metaApiMetrics,
ncMetaAclMw(columnAdd, 'columnAdd')
);
router.patch(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnUpdate, 'columnUpdate')
);
router.delete(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnDelete, 'columnDelete')
);
router.get(
'/api/v1/db/meta/columns/:columnId',
metaApiMetrics,
ncMetaAclMw(columnGet, 'columnGet')
);
router.post(
'/api/v1/db/meta/columns/:columnId/primary',
metaApiMetrics,

12
packages/nocodb/src/lib/meta/api/publicApis/publicDataExportApis.ts

@ -28,16 +28,26 @@ async function exportExcel(req: Request, res: Response) {
}
const model = await view.getModelWithInfo();
await view.getColumns();
const { offset, dbRows, elapsed } = await getDbRows(model, view, req);
const fields = req.query.fields as string[];
const data = XLSX.utils.json_to_sheet(dbRows, { header: fields });
const data = XLSX.utils.json_to_sheet(
dbRows.map((o: Record<string, any>) =>
Object.fromEntries(fields.map((f) => [f, o[f]]))
),
{ header: fields }
);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, data, view.title);
const buf = XLSX.write(wb, { type: 'base64', bookType: 'xlsx' });
res.set({
'Access-Control-Expose-Headers': 'nc-export-offset',
'nc-export-offset': offset,

2
packages/nocodb/src/lib/meta/api/publicApis/publicMetaApis.ts

@ -39,8 +39,6 @@ export async function viewMetaGet(req: Request, res: Response) {
// todo: return only required props
delete view['password'];
// const columnsById = c;
view.model.columns = view.columns
.filter((c) => {
const column = view.model.columnsById[c.fk_column_id];

38
scripts/sdk/swagger.json

@ -1972,8 +1972,7 @@
"project_id": {
"type": "string"
},
"meta": {
}
"meta": {}
}
}
}
@ -2105,7 +2104,7 @@
]
},
"delete": {
"summary": "",
"summary": "Column Delete",
"operationId": "db-table-column-delete",
"responses": {
"200": {
@ -2115,6 +2114,18 @@
"tags": [
"DB table column"
]
},
"get": {
"summary": "Column Get",
"operationId": "db-table-column-get",
"responses": {
"200": {
"description": "OK"
}
},
"tags": [
"DB Table Column"
]
}
},
"/api/v1/db/meta/columns/{columnId}/primary": {
@ -2194,8 +2205,7 @@
"order": {
"type": "number"
},
"meta": {
},
"meta": {},
"title": {
"type": "string"
},
@ -3877,6 +3887,13 @@
"in": "query",
"name": "nested",
"description": "Query params for nested data"
},
{
"schema": {
"type": "number"
},
"in": "query",
"name": "offset"
}
],
"responses": {
@ -7492,8 +7509,7 @@
"number"
]
},
"meta": {
}
"meta": {}
},
"required": [
"table_name",
@ -7591,8 +7607,7 @@
"uuid": {
"type": "string"
},
"meta": {
},
"meta": {},
"show_system_fields": {
"type": "boolean"
},
@ -7835,8 +7850,7 @@
"$ref": "#/components/schemas/Column"
}
},
"meta": {
}
"meta": {}
},
"required": [
"table_name",
@ -10042,4 +10056,4 @@
}
}
}
}
}

26
tests/playwright/tests/viewKanban.spec.ts

@ -301,4 +301,30 @@ test.describe('View', () => {
count: [1, 25, 25, 25, 25, 25],
});
});
test('Kanban shared view operations', async ({ page }) => {
test.slow();
await dashboard.viewSidebar.createKanbanView({
title: 'Film Kanban',
});
await dashboard.viewSidebar.verifyView({
title: 'Film Kanban',
index: 1,
});
// Share view
await toolbar.fields.toggle({ title: 'Rating' });
await toolbar.clickShareView();
const sharedLink = await toolbar.shareView.getShareLink();
await toolbar.shareView.close();
// sign-out
await dashboard.signOut();
// Open shared view & verify stack count
await page.goto(sharedLink);
const kanban = dashboard.kanban;
await kanban.verifyStackCount({ count: 6 });
});
});

Loading…
Cancel
Save