mirror of
https://github.com/nocodb/nocodb.git
synced 2026-02-01 23:38:21 +00:00
chore: sync
This commit is contained in:
@@ -20,9 +20,7 @@ const emit = defineEmits(['update:modelValue', 'back'])
|
||||
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
const baseURL = $api.instance.defaults.baseURL
|
||||
|
||||
const { $state, $poller } = useNuxtApp()
|
||||
const { $poller } = useNuxtApp()
|
||||
|
||||
const workspace = useWorkspace()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ColumnType, GalleryType, KanbanType, LookupType } from 'nocodb-sdk'
|
||||
import { ProjectRoles, UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
|
||||
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
|
||||
import Draggable from 'vuedraggable'
|
||||
|
||||
import type { SelectProps } from 'ant-design-vue'
|
||||
@@ -11,7 +11,7 @@ const meta = inject(MetaInj, ref())
|
||||
|
||||
const reloadViewDataHook = inject(ReloadViewDataHookInj, undefined)!
|
||||
|
||||
const { isMobileMode, user } = useGlobal()
|
||||
const { isMobileMode } = useGlobal()
|
||||
|
||||
const { isUIAllowed } = useRoles()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ const currentVersion = ref<any>(null)
|
||||
|
||||
// Load managed app info and current version
|
||||
const loadManagedApp = async () => {
|
||||
if (!(base.value as any)?.managed_app_id || !base.value?.fk_workspace_id) return
|
||||
if (!base.value?.managed_app_id || !base.value?.managed_app_master || !base.value?.fk_workspace_id) return
|
||||
|
||||
try {
|
||||
const response = await $api.internal.getOperation(base.value.fk_workspace_id, base.value.id!, {
|
||||
@@ -27,7 +27,7 @@ const loadManagedApp = async () => {
|
||||
|
||||
// Load current version info
|
||||
const loadCurrentVersion = async () => {
|
||||
if (!base.value?.managed_app_version_id || !base.value?.fk_workspace_id) return
|
||||
if (!base.value?.managed_app_version_id || !base.value?.managed_app_master || !base.value?.fk_workspace_id) return
|
||||
|
||||
try {
|
||||
// Get version details from versions list
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -1,6 +1,7 @@
|
||||
import { arrFlatMap } from 'nocodb-sdk';
|
||||
import type { DBQueryClient } from '~/dbQueryClient/types';
|
||||
import type { XKnex } from '~/db/CustomKnex';
|
||||
import type { Knex, XKnex } from '~/db/CustomKnex';
|
||||
import type { PagedResponseImpl } from '~/helpers/PagedResponse';
|
||||
|
||||
export abstract class GenericDBQueryClient implements DBQueryClient {
|
||||
temporaryTableRaw({
|
||||
@@ -45,4 +46,32 @@ export abstract class GenericDBQueryClient implements DBQueryClient {
|
||||
|
||||
abstract concat(fields: string[]): string;
|
||||
abstract simpleCast(field: string, asType: string): string;
|
||||
|
||||
generateNestedRowSelectQuery(_param: any): Knex.Raw<any> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
async singleQueryList(
|
||||
_context: any,
|
||||
_ctx: any,
|
||||
): Promise<
|
||||
PagedResponseImpl<Record<string, any>> | Array<Record<string, any>>
|
||||
> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
async singleQueryRead(
|
||||
_context: any,
|
||||
_ctx: any,
|
||||
): Promise<PagedResponseImpl<Record<string, any>>> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async extractColumns(_param: any): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async extractColumn(_param: any): Promise<{
|
||||
isArray?: boolean;
|
||||
}> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ClientType } from 'nocodb-sdk';
|
||||
import type { DBQueryClient as DBQueryClientType } from '~/dbQueryClient/types';
|
||||
import { PGDBQueryClient } from '~/dbQueryClient/pg';
|
||||
import { MySqlDBQueryClient } from '~/dbQueryClient/mysql';
|
||||
import { SqliteDBQueryClient } from '~/dbQueryClient/sqlite';
|
||||
|
||||
export class DBQueryClient {
|
||||
static get(clientType: ClientType) {
|
||||
static get(clientType: ClientType): DBQueryClientType {
|
||||
switch (clientType) {
|
||||
case ClientType.PG: {
|
||||
return new PGDBQueryClient();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
isCreatedOrLastModifiedByCol,
|
||||
isCreatedOrLastModifiedTimeCol,
|
||||
isHiddenCol,
|
||||
isLinksOrLTAR,
|
||||
isOrderCol,
|
||||
isSystemColumn,
|
||||
@@ -341,6 +342,7 @@ const getAst = async (
|
||||
} else if (getHiddenColumn) {
|
||||
isRequested =
|
||||
!isSystemColumn(col) ||
|
||||
((!view || !!view?.show_system_fields) && !isHiddenCol(col, model)) ||
|
||||
(isCreatedOrLastModifiedTimeCol(col) && col.system) ||
|
||||
// include all non-has-many system links(self-link) columns since has-many is part of mm relation and which is not required
|
||||
(isLinksOrLTAR(col) &&
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
!!! Do not edit this file manually !!!
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import type { IntegrationEntry } from '@noco-local-integrations/core';
|
||||
|
||||
export default [
|
||||
|
||||
] as IntegrationEntry[];
|
||||
export default [] as IntegrationEntry[];
|
||||
|
||||
@@ -294,7 +294,7 @@ export class DataAttachmentV3Service {
|
||||
size: fileSize,
|
||||
};
|
||||
processedAttachments.push(processedAttachment);
|
||||
if (supportsThumbnails({ mimetype: mimeType })) {
|
||||
if (supportsThumbnails({ mimetype: mimeType, size: fileSize })) {
|
||||
generateThumbnailAttachments.push(processedAttachment);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -8,6 +8,12 @@ export const isOfficeDocument = (..._args) => {
|
||||
|
||||
export const supportsThumbnails = (attachment: any) => {
|
||||
const mimetype = attachment.mimetype || attachment.mimeType;
|
||||
const size = attachment.size;
|
||||
|
||||
// Skip thumbnail generation if size is missing, not a number, or exceeds limit
|
||||
if (!size || !ncIsNumber(size) || size > getThumbnailMaxSize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!imageMimeTypes.includes(mimetype);
|
||||
};
|
||||
|
||||
@@ -95,3 +95,16 @@ export const NC_DISABLE_GROUP_BY_LIMIT =
|
||||
|
||||
export const NC_DISABLE_GROUP_BY_AGG =
|
||||
process.env.NC_DISABLE_GROUP_BY_AGG === 'true' || false;
|
||||
|
||||
const DEFAULT_THUMBNAIL_MAX_SIZE = 3 * 1024 * 1024;
|
||||
|
||||
export const getThumbnailMaxSize = () => {
|
||||
const envValue = process.env.NC_THUMBNAIL_MAX_SIZE;
|
||||
if (envValue) {
|
||||
const parsed = parseInt(envValue, 10);
|
||||
if (!isNaN(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return DEFAULT_THUMBNAIL_MAX_SIZE;
|
||||
};
|
||||
|
||||
@@ -33,18 +33,17 @@ const nocoExecute = async (
|
||||
dataTree = {},
|
||||
rootArgs = null,
|
||||
): Promise<any> => {
|
||||
// Handle array of resolvers by executing nocoExecute on each and returning a Promise.all
|
||||
// Handle array of resolvers by executing nocoExecute on each sequentially
|
||||
if (Array.isArray(resolverObj)) {
|
||||
return Promise.all(
|
||||
resolverObj.map((resolver, i) =>
|
||||
nocoExecuteSingle(
|
||||
requestObj,
|
||||
resolver,
|
||||
(dataTree[i] = dataTree[i] || {}),
|
||||
rootArgs,
|
||||
),
|
||||
),
|
||||
);
|
||||
const results = [];
|
||||
for (let i = 0; i < resolverObj.length; i++) {
|
||||
const resolver = resolverObj[i];
|
||||
dataTree[i] = dataTree[i] || {};
|
||||
results.push(
|
||||
await nocoExecuteSingle(requestObj, resolver, dataTree[i], rootArgs),
|
||||
);
|
||||
}
|
||||
return results;
|
||||
} else {
|
||||
return nocoExecuteSingle(requestObj, resolverObj, dataTree, rootArgs);
|
||||
}
|
||||
@@ -66,12 +65,12 @@ const nocoExecuteSingle = async (
|
||||
* @param args arguments passed to resolver functions
|
||||
* @returns Promise resolving the nested value
|
||||
*/
|
||||
const extractNested = (
|
||||
const extractNested = async (
|
||||
path: string[],
|
||||
dataTreeObj: any,
|
||||
resolver: ResolverObj = {},
|
||||
args = {},
|
||||
): any => {
|
||||
): Promise<any> => {
|
||||
if (path.length) {
|
||||
const key = path[0];
|
||||
// If key doesn't exist in dataTree, resolve using resolver or create a placeholder
|
||||
@@ -102,23 +101,23 @@ const nocoExecuteSingle = async (
|
||||
}
|
||||
|
||||
// Recursively handle nested arrays or resolve promises
|
||||
return (
|
||||
dataTreeObj[path[0]] instanceof Promise
|
||||
? dataTreeObj[path[0]]
|
||||
: Promise.resolve(dataTreeObj[path[0]])
|
||||
).then((res1) => {
|
||||
if (Array.isArray(res1)) {
|
||||
return Promise.all(
|
||||
res1.map((r) => extractNested(path.slice(1), r, {}, args)),
|
||||
);
|
||||
} else {
|
||||
return res1 !== null && res1 !== undefined
|
||||
? extractNested(path.slice(1), res1, {}, args)
|
||||
: Promise.resolve(null);
|
||||
const res1 = await (dataTreeObj[path[0]] instanceof Promise
|
||||
? dataTreeObj[path[0]]
|
||||
: Promise.resolve(dataTreeObj[path[0]]));
|
||||
|
||||
if (Array.isArray(res1)) {
|
||||
const results = [];
|
||||
for (const r of res1) {
|
||||
results.push(await extractNested(path.slice(1), r, {}, args));
|
||||
}
|
||||
});
|
||||
return results;
|
||||
} else {
|
||||
return res1 !== null && res1 !== undefined
|
||||
? await extractNested(path.slice(1), res1, {}, args)
|
||||
: null;
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve(dataTreeObj); // If path is exhausted, return data tree object
|
||||
return dataTreeObj; // If path is exhausted, return data tree object
|
||||
}
|
||||
};
|
||||
|
||||
@@ -149,17 +148,16 @@ const nocoExecuteSingle = async (
|
||||
dataTree[key] = res[key]; // Store result in dataTree
|
||||
} else {
|
||||
// If nested, extract the nested value using extractNested function
|
||||
res[key] = extractNested(
|
||||
resolverObj?.__proto__?.__columnAliases?.[key]?.path,
|
||||
dataTree,
|
||||
resolverObj,
|
||||
args?.nested?.[key],
|
||||
).then((res1) => {
|
||||
return Promise.resolve(
|
||||
// Flatten the array if it's nested
|
||||
Array.isArray(res1) ? flattenArray(res1) : res1,
|
||||
res[key] = (async () => {
|
||||
const res1 = await extractNested(
|
||||
resolverObj?.__proto__?.__columnAliases?.[key]?.path,
|
||||
dataTree,
|
||||
resolverObj,
|
||||
args?.nested?.[key],
|
||||
);
|
||||
});
|
||||
// Flatten the array if it's nested
|
||||
return Array.isArray(res1) ? flattenArray(res1) : res1;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,35 +168,37 @@ const nocoExecuteSingle = async (
|
||||
: Object.keys(resolverObj);
|
||||
|
||||
const out: any = {}; // Holds the final output
|
||||
const resolPromises = []; // Holds all the promises for asynchronous resolution
|
||||
for (const key of extractKeys) {
|
||||
// Extract the field for each key
|
||||
extractField(key, rootArgs?.nested?.[key]);
|
||||
|
||||
// Handle nested request objects by recursively calling nocoExecute
|
||||
if (requestObj[key] && typeof requestObj[key] === 'object') {
|
||||
res[key] = res[key].then((res1) => {
|
||||
if (res[key]) {
|
||||
const res1 = await res[key];
|
||||
if (Array.isArray(res1)) {
|
||||
// Handle arrays of results by executing nocoExecute on each element
|
||||
return (dataTree[key] = Promise.all(
|
||||
res1.map((r, i) =>
|
||||
nocoExecute(
|
||||
requestObj[key] as XcRequest,
|
||||
r,
|
||||
dataTree?.[key]?.[i],
|
||||
Object.assign(
|
||||
{
|
||||
nestedPage: rootArgs?.nestedPage,
|
||||
limit: rootArgs?.nestedLimit,
|
||||
},
|
||||
rootArgs?.nested?.[key] || {},
|
||||
),
|
||||
// Handle arrays of results by executing nocoExecute on each element sequentially
|
||||
dataTree[key] = dataTree[key] || [];
|
||||
for (let i = 0; i < res1.length; i++) {
|
||||
const r = res1[i];
|
||||
dataTree[key][i] = dataTree[key][i] || {};
|
||||
dataTree[key][i] = await nocoExecute(
|
||||
requestObj[key] as XcRequest,
|
||||
r,
|
||||
dataTree[key][i],
|
||||
Object.assign(
|
||||
{
|
||||
nestedPage: rootArgs?.nestedPage,
|
||||
limit: rootArgs?.nestedLimit,
|
||||
},
|
||||
rootArgs?.nested?.[key] || {},
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
res[key] = dataTree[key];
|
||||
} else if (res1) {
|
||||
// Handle single objects
|
||||
return (dataTree[key] = nocoExecute(
|
||||
res[key] = await nocoExecute(
|
||||
requestObj[key] as XcRequest,
|
||||
res1,
|
||||
dataTree[key],
|
||||
@@ -209,24 +209,19 @@ const nocoExecuteSingle = async (
|
||||
},
|
||||
rootArgs?.nested?.[key] || {},
|
||||
),
|
||||
));
|
||||
);
|
||||
dataTree[key] = res[key];
|
||||
} else {
|
||||
res[key] = res1;
|
||||
}
|
||||
return res1; // Return result if no further nesting
|
||||
});
|
||||
}
|
||||
}
|
||||
// Push resolved promises to resolPromises array
|
||||
|
||||
if (res[key]) {
|
||||
resolPromises.push(
|
||||
(async () => {
|
||||
out[key] = await res[key];
|
||||
})(),
|
||||
);
|
||||
out[key] = await res[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all promises to resolve before returning the final output
|
||||
await Promise.all(resolPromises);
|
||||
|
||||
return out; // Return the final resolved output
|
||||
};
|
||||
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import {
|
||||
ButtonActionsType,
|
||||
checkboxIconList,
|
||||
durationOptions,
|
||||
ratingIconList,
|
||||
UITypes,
|
||||
} from 'nocodb-sdk';
|
||||
|
||||
interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
meta: string | null;
|
||||
options: any;
|
||||
}
|
||||
|
||||
export function transformFieldConfig(field: Field): Field {
|
||||
const newField = { ...field };
|
||||
let metaObj = {} as Record<string, any>;
|
||||
|
||||
try {
|
||||
metaObj = field.meta ? JSON.parse(field.meta) : {};
|
||||
} catch (e) {
|
||||
metaObj = typeof field.meta === 'object' ? field.meta : {};
|
||||
}
|
||||
|
||||
newField.options = newField.options || {};
|
||||
|
||||
switch (field.type) {
|
||||
case UITypes.LongText:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
rich_text: metaObj.richMode || false,
|
||||
ai: metaObj.ai || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.PhoneNumber:
|
||||
case UITypes.URL:
|
||||
case UITypes.Email:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
validation: metaObj.validate || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Number:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
locale_string: metaObj.isLocaleString || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Decimal:
|
||||
case UITypes.Rollup:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
precision: metaObj.precision || 1,
|
||||
locale_string: metaObj.isLocaleString || false,
|
||||
};
|
||||
break;
|
||||
case UITypes.Currency:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
locale: metaObj.currency_locale || 'en-US',
|
||||
code: metaObj.currency_code || 'USD',
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Percent:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
show_as_progress: metaObj.is_progress || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Duration:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
duration_format: durationOptions[metaObj?.duration || 0]?.title,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.DateTime:
|
||||
case UITypes.CreatedTime:
|
||||
case UITypes.LastModifiedBy:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
date_format: metaObj.date_format || 'YYYY/MM/DD',
|
||||
time_format: metaObj.time_format || 'HH:mm:ss',
|
||||
['12hr_format']: metaObj.is12hrFormat || false,
|
||||
display_timezone: metaObj.isDisplayTimezone,
|
||||
timezone: metaObj.timezone,
|
||||
use_same_timezone_for_all: metaObj.useSameTimezoneForAll || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Date:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
date_format: metaObj.date_format || 'YYYY/MM/DD',
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Time:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
time_format: metaObj.time_format || 'HH:mm',
|
||||
['12hr_format']: metaObj.is12hrFormat || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Checkbox:
|
||||
if (metaObj.icon) {
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
icon: checkboxIconList[metaObj?.iconIdx ?? 0].label,
|
||||
color: metaObj.color || '#232323',
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case UITypes.Rating:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
icon: ratingIconList[metaObj?.iconIdx ?? 0].label,
|
||||
max_value: metaObj.max || 5,
|
||||
color: metaObj.color || '#232323',
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Barcode:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
barcode_format: metaObj.barcodeFormat || 'CODE128',
|
||||
barcode_value_field_id: field.options?.barcode_value_field_id,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.User:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
allow_multiple_users: metaObj.is_multi || false,
|
||||
notify_user_when_added: metaObj.notify || false,
|
||||
};
|
||||
break;
|
||||
|
||||
case UITypes.Formula:
|
||||
if (metaObj.display_column_meta) {
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
result: metaObj.display_type
|
||||
? transformFieldConfig({
|
||||
type: metaObj.display_type,
|
||||
meta: metaObj.display_column_meta.meta,
|
||||
options: {},
|
||||
} as Field)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case UITypes.Button: {
|
||||
if (newField.options.type === ButtonActionsType.Ai) {
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
prompt: newField.options.formula || '',
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UITypes.Links:
|
||||
case UITypes.LinkToAnotherRecord:
|
||||
newField.options = {
|
||||
...newField.options,
|
||||
singular: metaObj.singular,
|
||||
plural: metaObj.plural,
|
||||
};
|
||||
}
|
||||
|
||||
delete newField.meta;
|
||||
|
||||
return newField;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'mocha';
|
||||
// @ts-ignore
|
||||
import { expect } from 'chai';
|
||||
import { UITypes, ViewTypes } from 'nocodb-sdk';
|
||||
import { APIContext, UITypes, ViewTypes } from 'nocodb-sdk';
|
||||
import request from 'supertest';
|
||||
import { createProject } from '../../factory/base';
|
||||
import {
|
||||
@@ -9,9 +9,15 @@ import {
|
||||
createLookupColumn,
|
||||
createLtarColumn,
|
||||
createRollupColumn,
|
||||
updateViewColumn,
|
||||
} from '../../factory/column';
|
||||
import { createChildRow, createRow, getRow } from '../../factory/row';
|
||||
import {
|
||||
countRows,
|
||||
createChildRow,
|
||||
createRow,
|
||||
getRow,
|
||||
listRow,
|
||||
} from '../../factory/row';
|
||||
import { listenForJob } from '../../factory/job';
|
||||
import { createTable, getTable } from '../../factory/table';
|
||||
import { createView } from '../../factory/view';
|
||||
import init from '../../init';
|
||||
@@ -114,6 +120,8 @@ function viewRowLocalStaticTests() {
|
||||
},
|
||||
});
|
||||
|
||||
await linkInitTables(context, base);
|
||||
|
||||
console.timeEnd('#### viewRowLocalTests');
|
||||
});
|
||||
|
||||
@@ -600,7 +608,7 @@ function viewRowLocalTests() {
|
||||
.expect(200);
|
||||
expect(ascResponse.body.pageInfo.totalRows).greaterThan(0);
|
||||
expect(JSON.stringify(ascResponse.body.list[0][lookupColumn.title])).equal(
|
||||
JSON.stringify(['ANGELA']),
|
||||
JSON.stringify(['AARON']),
|
||||
);
|
||||
|
||||
const descResponse = await request(context.app)
|
||||
@@ -1186,9 +1194,98 @@ function viewRowLocalTests() {
|
||||
});
|
||||
|
||||
const testFindOneSortedFilteredNestedFieldsDataWithRollup = async (
|
||||
_viewType: ViewTypes,
|
||||
viewType: ViewTypes,
|
||||
) => {
|
||||
// TODO: Implement test logic
|
||||
const rollupColumn = await createRollupColumn(context, {
|
||||
base: base,
|
||||
title: 'Number of rentals',
|
||||
rollupFunction: 'count',
|
||||
table: customerTable,
|
||||
relatedTableName: rentalTable.table_name,
|
||||
relatedTableColumnTitle: 'RentalDate',
|
||||
});
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View',
|
||||
table: customerTable,
|
||||
type: viewType,
|
||||
});
|
||||
|
||||
const viewColumns = await view.getColumns(ctx);
|
||||
const rollupViewColumn = viewColumns.find(
|
||||
(vc) => vc.fk_column_id === rollupColumn.id,
|
||||
);
|
||||
|
||||
await updateViewColumns(context, {
|
||||
view: view,
|
||||
viewColumns: {
|
||||
[rollupViewColumn.id]: { show: true },
|
||||
},
|
||||
});
|
||||
|
||||
const activeColumn = (await customerTable.getColumns(ctx)).find(
|
||||
(c: ColumnType) => c.title === 'Active',
|
||||
);
|
||||
|
||||
const nestedFields = {
|
||||
Rentals: { f: 'RentalDate,ReturnDate' },
|
||||
};
|
||||
|
||||
const nestedFilter = [
|
||||
{
|
||||
fk_column_id: rollupColumn?.id,
|
||||
status: 'create',
|
||||
logical_op: 'and',
|
||||
comparison_op: 'gte',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
is_group: true,
|
||||
status: 'create',
|
||||
logical_op: 'or',
|
||||
children: [
|
||||
{
|
||||
fk_column_id: rollupColumn?.id,
|
||||
status: 'create',
|
||||
logical_op: 'and',
|
||||
comparison_op: 'lte',
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
is_group: true,
|
||||
status: 'create',
|
||||
logical_op: 'and',
|
||||
children: [
|
||||
{
|
||||
logical_op: 'and',
|
||||
fk_column_id: activeColumn?.id,
|
||||
status: 'create',
|
||||
comparison_op: 'eq',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ascResponse = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/find-one`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.query({
|
||||
nested: nestedFields,
|
||||
filterArrJson: JSON.stringify([nestedFilter]),
|
||||
sort: `${rollupColumn.title}`,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Verify rollup column exists and has a value
|
||||
expect(parseInt(ascResponse.body[rollupColumn.title])).to.be.greaterThan(0);
|
||||
|
||||
// Verify nested rentals are returned
|
||||
expect(ascResponse.body).to.have.property('Rentals');
|
||||
};
|
||||
|
||||
it('Find one view sorted filtered view with nested fields data list with a rollup column in customer table GRID', async function () {
|
||||
@@ -1254,8 +1351,62 @@ function viewRowLocalTests() {
|
||||
await testGroupDescSorted(ViewTypes.CALENDAR);
|
||||
});
|
||||
|
||||
const testGroupWithOffset = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testGroupWithOffset = async (viewType: ViewTypes) => {
|
||||
const view = await createView(context, {
|
||||
title: 'View',
|
||||
table: customerTable,
|
||||
type: viewType,
|
||||
});
|
||||
|
||||
const firstNameColumn = customerColumns.find(
|
||||
(col: ColumnType) => col.title === 'FirstName',
|
||||
);
|
||||
|
||||
const rollupColumn = await createRollupColumn(context, {
|
||||
base: base,
|
||||
title: 'Rollup',
|
||||
rollupFunction: 'count',
|
||||
table: customerTable,
|
||||
relatedTableName: rentalTable.table_name,
|
||||
relatedTableColumnTitle: 'RentalDate',
|
||||
});
|
||||
|
||||
const visibleColumns = [firstNameColumn];
|
||||
const sortInfo = `-FirstName, +${rollupColumn.title}`;
|
||||
|
||||
// First get results without offset to know what to expect
|
||||
const responseNoOffset = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/groupby`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.query({
|
||||
fields: visibleColumns.map((c) => c.title),
|
||||
sort: sortInfo,
|
||||
column_name: firstNameColumn.title,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Now get results with offset and verify
|
||||
const response = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${customerTable.id}/views/${view.id}/groupby`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.query({
|
||||
fields: visibleColumns.map((c) => c.title),
|
||||
sort: sortInfo,
|
||||
column_name: firstNameColumn.title,
|
||||
offset: 4,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Verify that offset works - first item with offset=4 should match 5th item without offset
|
||||
if (responseNoOffset.body.list.length > 4) {
|
||||
expect(response.body.list[0]['FirstName']).to.equal(
|
||||
responseNoOffset.body.list[4]['FirstName'],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
it('Groupby desc sorted and with rollup view data list with required columns GALLERY', async function () {
|
||||
@@ -1276,8 +1427,40 @@ function viewRowLocalTests() {
|
||||
//#endregion Group by tests
|
||||
|
||||
//#region Count tests
|
||||
const testCount = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testCount = async (viewType: ViewTypes) => {
|
||||
let calendar_range = {};
|
||||
let table;
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
table = rentalTable;
|
||||
calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
} else {
|
||||
table = customerTable;
|
||||
}
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View ' + viewType,
|
||||
table: table,
|
||||
type: viewType,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
const response = await request(context.app)
|
||||
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/count`)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
// Rental table has 40 rows
|
||||
expect(parseInt(response.body.count)).to.equal(40);
|
||||
} else {
|
||||
// Customer table has 33 rows
|
||||
expect(parseInt(response.body.count)).to.equal(33);
|
||||
}
|
||||
};
|
||||
|
||||
it('Count view data list with required columns', async function () {
|
||||
@@ -1289,8 +1472,44 @@ function viewRowLocalTests() {
|
||||
//#endregion Count tests
|
||||
|
||||
//#region Read/Exist tests
|
||||
const testReadViewRow = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testReadViewRow = async (viewType: ViewTypes) => {
|
||||
let table;
|
||||
let calendar_range = {};
|
||||
const idColumn = 'Id';
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
table = rentalTable;
|
||||
calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
} else {
|
||||
table = customerTable;
|
||||
}
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View ' + viewType,
|
||||
table: table,
|
||||
type: viewType,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
const listResponse = await request(context.app)
|
||||
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}`)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
const row = listResponse.body.list[0];
|
||||
|
||||
const readResponse = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/${row[idColumn]}`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
expect(row[idColumn]).to.equal(readResponse.body[idColumn]);
|
||||
};
|
||||
|
||||
it('Read view row', async function () {
|
||||
@@ -1300,8 +1519,45 @@ function viewRowLocalTests() {
|
||||
await testReadViewRow(ViewTypes.CALENDAR);
|
||||
});
|
||||
|
||||
const testViewRowExists = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testViewRowExists = async (viewType: ViewTypes) => {
|
||||
let table;
|
||||
let calendar_range = {};
|
||||
const idColumn = 'Id';
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
table = rentalTable;
|
||||
calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
} else {
|
||||
table = customerTable;
|
||||
}
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View ' + viewType,
|
||||
table: table,
|
||||
type: viewType,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
// Get first row to test existence
|
||||
const listResponse = await request(context.app)
|
||||
.get(`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}`)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
const row = listResponse.body.list[0];
|
||||
|
||||
const response = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/${row[idColumn]}/exist`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).to.be.true;
|
||||
};
|
||||
|
||||
it('Exist view row : should return true when row exists in view', async function () {
|
||||
@@ -1311,8 +1567,36 @@ function viewRowLocalTests() {
|
||||
await testViewRowExists(ViewTypes.CALENDAR);
|
||||
});
|
||||
|
||||
const testViewRowNotExists = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testViewRowNotExists = async (viewType: ViewTypes) => {
|
||||
let calendar_range = {};
|
||||
let table;
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
table = rentalTable;
|
||||
calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
} else {
|
||||
table = customerTable;
|
||||
}
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View ' + viewType,
|
||||
table: table,
|
||||
type: viewType,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
const response = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/data/noco/${base.id}/${table.id}/views/${view.id}/999999/exist`,
|
||||
)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).to.be.false;
|
||||
};
|
||||
|
||||
it("Exist view row : should return false when row doesn't exist in view", async function () {
|
||||
@@ -1325,15 +1609,80 @@ function viewRowLocalTests() {
|
||||
|
||||
//#region Calendar-specific tests
|
||||
const testCalendarDataApi = async () => {
|
||||
// TODO: Implement test logic
|
||||
const table = rentalTable;
|
||||
const calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View',
|
||||
table: table,
|
||||
type: ViewTypes.CALENDAR,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
const response = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/calendar-data/noco/${base.id}/${table.id}/views/${view.id}`,
|
||||
)
|
||||
.query({
|
||||
from_date: '2005-05-24',
|
||||
to_date: '2005-05-26',
|
||||
next_date: '2005-05-27',
|
||||
prev_date: '2005-05-24',
|
||||
})
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
// Local data has rentals in the 2005-05-24 to 2005-05-26 range
|
||||
expect(response.body.list.length).to.be.greaterThan(0);
|
||||
};
|
||||
|
||||
it('Calendar data', async function () {
|
||||
await testCalendarDataApi();
|
||||
});
|
||||
|
||||
const testCountDatesByRange = async (_viewType: ViewTypes) => {
|
||||
// TODO: Implement test logic
|
||||
const testCountDatesByRange = async (viewType: ViewTypes) => {
|
||||
let calendar_range = {};
|
||||
let expectStatus = 400;
|
||||
|
||||
if (viewType === ViewTypes.CALENDAR) {
|
||||
calendar_range = {
|
||||
fk_from_column_id: rentalColumns.find(
|
||||
(c: ColumnType) => c.title === 'RentalDate',
|
||||
)?.id,
|
||||
};
|
||||
expectStatus = 200;
|
||||
}
|
||||
|
||||
const view = await createView(context, {
|
||||
title: 'View',
|
||||
table: rentalTable,
|
||||
type: viewType,
|
||||
range: calendar_range,
|
||||
});
|
||||
|
||||
const response = await request(context.app)
|
||||
.get(
|
||||
`/api/v1/db/calendar-data/noco/${base.id}/${rentalTable.id}/views/${view.id}/countByDate/`,
|
||||
)
|
||||
.query({
|
||||
from_date: '2005-05-24',
|
||||
to_date: '2005-05-26',
|
||||
next_date: '2005-05-27',
|
||||
prev_date: '2005-05-24',
|
||||
})
|
||||
.set('xc-auth', context.token)
|
||||
.expect(expectStatus);
|
||||
|
||||
if (expectStatus === 200) {
|
||||
expect(response.body).to.have.property('count');
|
||||
expect(response.body.count).to.be.greaterThan(0);
|
||||
} else if (expectStatus === 400) {
|
||||
expect(response.body.msg).to.equal('View is not a calendar view');
|
||||
}
|
||||
};
|
||||
|
||||
it('Count dates by range Calendar', async () => {
|
||||
@@ -1359,13 +1708,130 @@ function viewRowLocalTests() {
|
||||
|
||||
//#region Export tests
|
||||
it('Export csv GRID', async function () {
|
||||
// TODO: Implement test logic
|
||||
const view = await createView(context, {
|
||||
title: 'View',
|
||||
table: customerTable,
|
||||
type: ViewTypes.GRID,
|
||||
});
|
||||
|
||||
// get row count
|
||||
const rowCount = await countRows({
|
||||
base: base,
|
||||
table: customerTable,
|
||||
view: view,
|
||||
});
|
||||
|
||||
// Start export job
|
||||
const jobResponse = await request(context.app)
|
||||
.post(`/api/v2/export/${view.id}/csv`)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
// Verify we got a job ID
|
||||
const jobId = jobResponse.body.id;
|
||||
expect(jobId).to.be.a('string');
|
||||
|
||||
// Wait for job completion
|
||||
const resultData = await listenForJob({
|
||||
context,
|
||||
base_id: base.id,
|
||||
job_id: jobId,
|
||||
});
|
||||
|
||||
// Verify the exported file
|
||||
expect(resultData).to.be.an('object');
|
||||
expect(resultData.url).to.be.a('string');
|
||||
|
||||
const fileUrl = resultData.url;
|
||||
|
||||
// Download the file
|
||||
const fileResponse = await request(context.app)
|
||||
.get(`/${encodeURI(fileUrl)}`)
|
||||
.set('xc-auth', context.token)
|
||||
.expect(200);
|
||||
|
||||
// Check file content
|
||||
expect(fileResponse.headers['content-type']).to.include('text/csv');
|
||||
expect(fileResponse.text).to.be.a('string').and.not.empty;
|
||||
expect(fileResponse.text.split('\n').length).to.equal(rowCount + 1);
|
||||
});
|
||||
//#endregion Export tests
|
||||
|
||||
//#region View column API tests
|
||||
// FIXME: still has cache race condition issue
|
||||
it('Test view column v3 apis', async function () {
|
||||
// TODO: Implement test logic
|
||||
// Use filmTable which was already initialized
|
||||
const view = await createView(context, {
|
||||
title: 'Film View',
|
||||
table: filmTable,
|
||||
type: ViewTypes.GRID,
|
||||
});
|
||||
|
||||
const columns = await filmTable.getColumns(ctx);
|
||||
|
||||
// get rows before hiding columns
|
||||
const listResponse = await request(context.app)
|
||||
.get(`/api/v1/db/data/noco/${base.id}/${filmTable.id}/views/${view.id}`)
|
||||
.set('xc-auth', context.token)
|
||||
.query({ limit: 1 })
|
||||
.expect(200);
|
||||
|
||||
const rows = listResponse.body.list;
|
||||
|
||||
// hide few columns using update view column API
|
||||
const columnsToHide = ['Rating', 'Description', 'ReleaseYear'];
|
||||
|
||||
// generate key value pair of column id and object with show as false
|
||||
const viewColumnsObj: any = columnsToHide.reduce(
|
||||
(acc: any, columnTitle) => {
|
||||
const column = columns.find((c: ColumnType) => c.title === columnTitle);
|
||||
if (column) {
|
||||
acc[column.id] = {
|
||||
show: false,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await updateViewColumns(context, {
|
||||
view,
|
||||
viewColumns: viewColumnsObj,
|
||||
});
|
||||
|
||||
// get rows after update
|
||||
const rowsAfterUpdate = await listRow({
|
||||
base: base,
|
||||
table: filmTable,
|
||||
view,
|
||||
options: {
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// verify column visible in old and hidden in new
|
||||
for (const title of columnsToHide) {
|
||||
expect(rows[0]).to.have.property(title);
|
||||
expect(rowsAfterUpdate[0]).to.not.have.property(title);
|
||||
}
|
||||
|
||||
// get view columns and verify hidden columns
|
||||
const viewColApiRes: any = await getViewColumns(context, {
|
||||
view,
|
||||
});
|
||||
|
||||
for (const colId of Object.keys(viewColApiRes[APIContext.VIEW_COLUMNS])) {
|
||||
const column = columns.find((c: ColumnType) => c.id === colId);
|
||||
if (column && columnsToHide.includes(column.title)) {
|
||||
expect(viewColApiRes[APIContext.VIEW_COLUMNS][colId]).to.have.property(
|
||||
'show',
|
||||
);
|
||||
expect(!!viewColApiRes[APIContext.VIEW_COLUMNS][colId].show).to.be.eq(
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
//#endregion View column API tests
|
||||
}
|
||||
|
||||
@@ -1411,6 +1411,10 @@ export const linkInitTables = async (context: any, base: any) => {
|
||||
id: '23',
|
||||
customerId: '8',
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
customerId: '8',
|
||||
},
|
||||
{
|
||||
id: '24',
|
||||
customerId: '7',
|
||||
@@ -1445,7 +1449,7 @@ export const linkInitTables = async (context: any, base: any) => {
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
customerId: '8',
|
||||
customerId: '12',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
@@ -1463,6 +1467,18 @@ export const linkInitTables = async (context: any, base: any) => {
|
||||
id: '5',
|
||||
customerId: '12',
|
||||
},
|
||||
{
|
||||
id: '29',
|
||||
customerId: '31',
|
||||
},
|
||||
{
|
||||
id: '27',
|
||||
customerId: '32',
|
||||
},
|
||||
{
|
||||
id: '28',
|
||||
customerId: '33',
|
||||
},
|
||||
];
|
||||
|
||||
const linkTo_Customer_MM_Rental_LTAR = (rowId: string, body: any[]) => {
|
||||
|
||||
Reference in New Issue
Block a user