Browse Source

Merge branch 'develop' into fix/attachments-gallery

pull/4959/head
Wing-Kam Wong 2 years ago
parent
commit
69358a6114
  1. 2
      packages/nc-gui/lang/ru.json
  2. 184
      packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts
  3. 43
      tests/playwright/pages/Dashboard/Grid/index.ts

2
packages/nc-gui/lang/ru.json

@ -206,7 +206,7 @@
"advancedSettings": "Расширенные настройки", "advancedSettings": "Расширенные настройки",
"codeSnippet": "Сниппет кода", "codeSnippet": "Сниппет кода",
"keyboardShortcut": "Горячие клавиши", "keyboardShortcut": "Горячие клавиши",
"generateRandomName": "Generate Random Name" "generateRandomName": "Сгенерировать случайное имя"
}, },
"labels": { "labels": {
"createdBy": "Автор", "createdBy": "Автор",

184
packages/nocodb/src/lib/version-upgrader/ncAttachmentUpgrader.ts

@ -1,3 +1,4 @@
import { Knex } from 'knex';
import { NcUpgraderCtx } from './NcUpgrader'; import { NcUpgraderCtx } from './NcUpgrader';
import { MetaTable } from '../utils/globals'; import { MetaTable } from '../utils/globals';
import Base from '../models/Base'; import Base from '../models/Base';
@ -29,7 +30,7 @@ function getTnPath(knex: XKnex, tb: Model) {
const schema = (knex as any).searchPath?.(); const schema = (knex as any).searchPath?.();
const clientType = knex.clientType(); const clientType = knex.clientType();
if (clientType === 'mssql' && schema) { if (clientType === 'mssql' && schema) {
return knex.raw('??.??', [schema, tb.table_name]); return knex.raw('??.??', [schema, tb.table_name]).toQuery();
} else if (clientType === 'snowflake') { } else if (clientType === 'snowflake') {
return [ return [
knex.client.config.connection.database, knex.client.config.connection.database,
@ -45,82 +46,133 @@ export default async function ({ ncMeta }: NcUpgraderCtx) {
const bases: BaseType[] = await ncMeta.metaList2(null, null, MetaTable.BASES); const bases: BaseType[] = await ncMeta.metaList2(null, null, MetaTable.BASES);
for (const _base of bases) { for (const _base of bases) {
const base = new Base(_base); const base = new Base(_base);
const knex: XKnex = base.is_meta
// skip if the prodect_id is missing
if (!base.project_id) {
continue;
}
const project = await ncMeta.metaGet2(null, null, MetaTable.PROJECT, {
id: base.project_id,
});
// skip if the project is missing
if (!project) {
continue;
}
const isProjectDeleted = project.deleted;
const knex: Knex = base.is_meta
? ncMeta.knexConnection ? ncMeta.knexConnection
: NcConnectionMgrv2.get(base); : NcConnectionMgrv2.get(base);
const models = await base.getModels(ncMeta); const models = await base.getModels(ncMeta);
for (const model of models) { for (const model of models) {
const updateRecords = []; try {
const columns = await ( // if the table is missing in database, skip
await Model.get(model.id, ncMeta) if (!(await knex.schema.hasTable(getTnPath(knex, model)))) {
).getColumns(ncMeta); continue;
const attachmentColumns = columns }
.filter((c) => c.uidt === UITypes.Attachment)
.map((c) => c.column_name);
if (attachmentColumns.length === 0) {
continue;
}
const primaryKeys = columns.filter((c) => c.pk).map((c) => c.column_name);
const records = await knex(getTnPath(knex, model)).select([
...primaryKeys,
...attachmentColumns,
]);
for (const record of records) {
for (const attachmentColumn of attachmentColumns) {
let attachmentMeta: Array<{
url: string;
}>;
// if parsing failed ignore the cell
try {
attachmentMeta =
typeof record[attachmentColumn] === 'string'
? JSON.parse(record[attachmentColumn])
: record[attachmentColumn];
} catch {}
// if cell data is not an array, ignore it
if (!Array.isArray(attachmentMeta)) {
continue;
}
if (attachmentMeta) { const updateRecords = [];
const newAttachmentMeta = [];
for (const attachment of attachmentMeta) { // get all attachment & primary key columns
if ('url' in attachment && typeof attachment.url === 'string') { // and filter out the columns that are missing in database
const match = attachment.url.match(/^(.*)\/download\/(.*)$/); const columns = await (await Model.get(model.id, ncMeta))
if (match) { .getColumns(ncMeta)
// e.g. http://localhost:8080/download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png .then(async (columns) => {
// match[1] = http://localhost:8080 const filteredColumns = [];
// match[2] = download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png
const path = `download/${match[2]}`; for (const column of columns) {
if (column.uidt !== UITypes.Attachment && !column.pk) continue;
newAttachmentMeta.push({ if (
...attachment, !(await knex.schema.hasColumn(
path, getTnPath(knex, model),
}); column.column_name
} else { ))
// keep it as it is )
newAttachmentMeta.push(attachment); continue;
filteredColumns.push(column);
}
return filteredColumns;
});
const attachmentColumns = columns
.filter((c) => c.uidt === UITypes.Attachment)
.map((c) => c.column_name);
if (attachmentColumns.length === 0) {
continue;
}
const primaryKeys = columns
.filter((c) => c.pk)
.map((c) => c.column_name);
const records = await knex(getTnPath(knex, model)).select();
for (const record of records) {
for (const attachmentColumn of attachmentColumns) {
let attachmentMeta: Array<{
url: string;
}>;
// if parsing failed ignore the cell
try {
attachmentMeta =
typeof record[attachmentColumn] === 'string'
? JSON.parse(record[attachmentColumn])
: record[attachmentColumn];
} catch {}
// if cell data is not an array, ignore it
if (!Array.isArray(attachmentMeta)) {
continue;
}
if (attachmentMeta) {
const newAttachmentMeta = [];
for (const attachment of attachmentMeta) {
if ('url' in attachment && typeof attachment.url === 'string') {
const match = attachment.url.match(/^(.*)\/download\/(.*)$/);
if (match) {
// e.g. http://localhost:8080/download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png
// match[1] = http://localhost:8080
// match[2] = download/noco/xcdb/Sheet-1/title5/ee2G8p_nute_gunray.png
const path = `download/${match[2]}`;
newAttachmentMeta.push({
...attachment,
path,
});
} else {
// keep it as it is
newAttachmentMeta.push(attachment);
}
} }
} }
} const where = primaryKeys
const where = primaryKeys .map((key) => {
.map((key) => { return { [key]: record[key] };
return { [key]: record[key] };
})
.reduce((acc, val) => Object.assign(acc, val), {});
updateRecords.push(
await knex(getTnPath(knex, model))
.update({
[attachmentColumn]: JSON.stringify(newAttachmentMeta),
}) })
.where(where) .reduce((acc, val) => Object.assign(acc, val), {});
); updateRecords.push(
await knex(getTnPath(knex, model))
.update({
[attachmentColumn]: JSON.stringify(newAttachmentMeta),
})
.where(where)
);
}
} }
} }
await Promise.all(updateRecords);
} catch (e) {
// ignore the error related to deleted project
if (!isProjectDeleted) {
throw e;
}
} }
await Promise.all(updateRecords);
} }
} }
} }

43
tests/playwright/pages/Dashboard/Grid/index.ts

@ -308,18 +308,41 @@ export class GridPage extends BasePage {
} }
async copyWithKeyboard() { async copyWithKeyboard() {
await this.get().press((await this.isMacOs()) ? 'Meta+C' : 'Control+C'); // retry to avoid flakiness, until text is copied to clipboard
await this.verifyToast({ message: 'Copied to clipboard' }); //
let text = '';
return this.getClipboardText(); let retryCount = 5;
while (text === '') {
await this.get().press((await this.isMacOs()) ? 'Meta+C' : 'Control+C');
await this.verifyToast({ message: 'Copied to clipboard' });
text = await this.getClipboardText();
// retry if text is empty till count is reached
retryCount--;
if (0 === retryCount) {
break;
}
}
return text;
} }
async copyWithMouse({ index, columnHeader }: CellProps) { async copyWithMouse({ index, columnHeader }: CellProps) {
await this.cell.get({ index, columnHeader }).click({ button: 'right' }); // retry to avoid flakiness, until text is copied to clipboard
await this.get().page().getByTestId('context-menu-item-copy').click(); //
let text = '';
await this.verifyToast({ message: 'Copied to clipboard' }); let retryCount = 5;
while (text === '') {
return this.getClipboardText(); await this.cell.get({ index, columnHeader }).click({ button: 'right' });
await this.get().page().getByTestId('context-menu-item-copy').click();
await this.verifyToast({ message: 'Copied to clipboard' });
text = await this.getClipboardText();
// retry if text is empty till count is reached
retryCount--;
if (0 === retryCount) {
break;
}
}
return text;
} }
} }

Loading…
Cancel
Save