Browse Source

Merge branch 'develop' into enhancement/formula

pull/2706/head
Wing-Kam Wong 2 years ago
parent
commit
5510235839
  1. 2
      .github/workflows/release-pr.yml
  2. 2
      packages/nc-gui/assets/style/style.css
  3. 23
      packages/nc-gui/components/FeedbackForm.vue
  4. 6
      packages/nc-gui/components/ReleaseInfo.vue
  5. 4
      packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js
  6. 39
      packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue
  7. 85
      packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue
  8. 22
      packages/nc-gui/components/project/spreadsheet/components/SharedViewsList.vue
  9. 56
      packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue
  10. 102
      packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue
  11. 9
      packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue
  12. 5
      packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue
  13. 3
      packages/nc-gui/components/project/spreadsheet/components/editColumn/LookupOptions.vue
  14. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualCell/LookupCell.vue
  15. 9
      packages/nc-gui/components/project/spreadsheet/public/XcTable.vue
  16. 14
      packages/nc-gui/components/utils/DlgTableCreate.vue
  17. 422
      packages/nc-gui/lang/ko.json
  18. 2
      packages/nc-gui/package-lock.json
  19. 52
      packages/nc-gui/plugins/projectLoader.js
  20. 6
      packages/nc-gui/store/app.js
  21. 11
      packages/nc-gui/store/settings.js
  22. 2
      packages/nc-lib-gui/package.json
  23. 1
      packages/noco-docs/content/en/developer-resources/rest-apis.md
  24. 2
      packages/noco-docs/content/en/engineering/builds-and-releases.md
  25. 2
      packages/noco-docs/content/en/setup-and-usages/column-operations.md
  26. 4
      packages/nocodb-sdk/package-lock.json
  27. 2
      packages/nocodb-sdk/package.json
  28. 1
      packages/nocodb-sdk/src/lib/Api.ts
  29. 20
      packages/nocodb/package-lock.json
  30. 4
      packages/nocodb/package.json
  31. 2
      packages/nocodb/src/lib/db/sql-data-mapper/lib/BaseModel.ts
  32. 154
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  33. 5
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  34. 6
      packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts
  35. 1
      packages/nocodb/src/lib/meta/api/filterApis.ts
  36. 13
      packages/nocodb/src/lib/meta/api/swagger/helpers/templates/params.ts
  37. 7
      packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts
  38. 14
      packages/nocodb/src/lib/meta/api/tableApis.ts
  39. 13
      packages/nocodb/src/lib/meta/api/userApi/userApis.ts
  40. 17
      packages/nocodb/src/lib/meta/api/utilApis.ts
  41. 8
      packages/nocodb/src/lib/meta/api/viewApis.ts
  42. 20
      packages/nocodb/src/lib/meta/helpers/PagedResponse.ts
  43. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  44. 38
      packages/nocodb/src/lib/migrations/v2/nc_018_add_meta_in_view.ts
  45. 31
      packages/nocodb/src/lib/models/Filter.ts
  46. 35
      packages/nocodb/src/lib/models/View.ts
  47. 4
      scripts/cypress/integration/common/1c_sql_view.js
  48. 2
      scripts/cypress/integration/common/1e_meta_sync.js
  49. 2
      scripts/cypress/integration/common/1e_pg_meta_sync.js
  50. 4
      scripts/cypress/integration/common/3a_filter_sort_fields_operations.js
  51. 25
      scripts/cypress/integration/common/4b_table_view_share.js
  52. 4
      scripts/cypress/integration/common/4e_form_view_share.js
  53. 10
      scripts/cypress/integration/common/4f_grid_view_share.js
  54. 8
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  55. 4
      scripts/cypress/integration/common/6f_attachments.js
  56. 2
      scripts/cypress/integration/common/9a_QuickTest.js
  57. 1
      scripts/cypress/support/page_objects/mainPage.js

2
.github/workflows/release-pr.yml

@ -84,7 +84,7 @@ jobs:
- uses: peter-evans/commit-comment@v2
with:
body: |
The PR changes have been deployed. Pleae run the following command to verify:
The PR changes have been deployed. Please run the following command to verify:
```
docker run -d -p 8888:8080 nocodb/nocodb-timely:${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.target_tag }}
```

2
packages/nc-gui/assets/style/style.css

@ -365,7 +365,7 @@ html {
/* sorting and filter */
.menu-filter-dropdown {
.menu-filter-dropdown:not(.nested) {
max-height: 500px;
overflow-y: auto;
}

23
packages/nc-gui/components/FeedbackForm.vue

@ -1,14 +1,8 @@
<template>
<div v-if="!feedbackFormHidden" class="nc-feedback-form-wrapper">
<v-icon class="nc-close-icon" large @click="feedbackFormHidden = true"> mdi-close-circle-outline </v-icon>
<div v-if="!feedbackFormIsHidden" class="nc-feedback-form-wrapper">
<v-icon class="nc-close-icon" large @click="feedbackFormIsHidden = true"> mdi-close-circle-outline </v-icon>
<iframe
src="https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true"
width="100%"
height="500"
frameborder="0"
marginheight="0"
marginwidth="0"
<iframe :src="feedbackFormUrl" width="100%" height="500" frameborder="0" marginheight="0" marginwidth="0"
>Loading
</iframe>
</div>
@ -19,14 +13,17 @@
export default {
name: 'FeedbackForm',
computed: {
feedbackFormHidden: {
feedbackFormIsHidden: {
get() {
return this.$store.state.settings.feedbackFormHidden;
return this.$store.state.settings.feedbackForm.isHidden;
},
set(val) {
this.$store.commit('settings/MutFeedbackFormHidden', val);
set(isHidden) {
this.$store.commit('settings/MutFeedbackForm', { ...this.$store.state.settings.feedbackForm, isHidden });
},
},
feedbackFormUrl() {
return this.$store.state.settings.feedbackForm.url;
},
},
};
</script>

6
packages/nc-gui/components/ReleaseInfo.vue

@ -42,9 +42,9 @@ export default {
get() {
return (
!this.loading &&
this.$store.state.app.releaseVersion &&
this.$store.state.app.currentVersion &&
this.$store.state.app.latestRelease &&
this.$store.state.app.releaseVersion !== this.$store.state.app.latestRelease &&
this.$store.state.app.currentVersion !== this.$store.state.app.latestRelease &&
this.$store.state.app.latestRelease !== this.$store.state.app.hiddenRelease
);
},
@ -53,7 +53,7 @@ export default {
},
},
releaseVersion() {
return this.$store.state.app.releaseVersion;
return this.$store.state.app.latestRelease;
},
},
mounted() {

4
packages/nc-gui/components/import/templateParsers/ExcelTemplateAdapter.js

@ -53,7 +53,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
for (let i = 0; i < this.wb.SheetNames.length; i++) {
const columnNamePrefixRef = { id: 0 }
const sheet = this.wb.SheetNames[i]
let tn = (sheet || 'table').replace(/\W/g, '_').trim()
let tn = (sheet || 'table').replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_').trim()
while (tn in tableNamePrefixRef) {
tn = `${tn}${++tableNamePrefixRef[tn]}`
@ -91,7 +91,7 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
// const colLen = Math.max()
for (let col = 0; col < rows[0].length; col++) {
let cn = ((columnNameRowExist && rows[0] && rows[0][col] && rows[0][col].toString().trim()) ||
`field_${col + 1}`).replace(/\W/g, '_').trim()
`field_${col + 1}`).replace(/[` ~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/g, '_').trim()
while (cn in columnNamePrefixRef) {
cn = `${cn}${++columnNamePrefixRef[cn]}`

39
packages/nc-gui/components/project/spreadsheet/RowsXcDataTable.vue

@ -1124,10 +1124,10 @@ export default {
.map(c => rowObj[c.title])
.join('___');
if (!id) {
return this.$toast.info("Delete not allowed for table which doesn't have primary Key").goAway(3000);
const successfulDeletion = await this.deleteRowById(id);
if (!successfulDeletion) {
return;
}
await this.$api.dbViewRow.delete('noco', this.projectName, this.meta.id, this.selectedView.id, id);
}
this.data.splice(this.rowContextMenu.index, 1);
this.syncCount();
@ -1138,7 +1138,6 @@ export default {
},
async deleteSelectedRows() {
let row = this.rowLength;
// let success = 0
while (row--) {
try {
const { row: rowObj, rowMeta } = this.data[row];
@ -1151,10 +1150,10 @@ export default {
.map(c => rowObj[c.title])
.join('___');
if (!id) {
return this.$toast.info("Delete not allowed for table which doesn't have primary Key").goAway(3000);
const successfulDeletion = await this.deleteRowById(id);
if (!successfulDeletion) {
continue;
}
await this.$api.dbViewRow.delete('noco', this.projectName, this.meta.id, this.selectedView.id, id);
}
this.data.splice(row, 1);
} catch (e) {
@ -1164,6 +1163,32 @@ export default {
this.syncCount();
},
async deleteRowById(id) {
try {
if (!id) {
this.$toast.info("Delete not allowed for table which doesn't have primary Key").goAway(3000);
return false;
}
const res = await this.$api.dbViewRow.delete('noco', this.projectName, this.meta.id, this.selectedView.id, id);
if (res?.message) {
this.$toast
.info(
`<div style="padding:10px 4px">Unable to delete row with ID ${id} because of the following:
<br><br>${res.message.join('<br>')}<br><br>
Clear the data first & try again</div>`
)
.goAway(5000);
return false;
}
} catch (e) {
this.$toast.error(`Failed to delete row : ${e.message}`).goAway(3000);
return false;
}
return true;
},
async clearCellValue() {
const { col, colIndex, row, index } = this.rowContextMenu;
if (row[col.title] === null) {

85
packages/nc-gui/components/project/spreadsheet/components/ColumnFilter.vue

@ -1,13 +1,16 @@
<template>
<div class="backgroundColor pa-2 menu-filter-dropdown" :style="{ width: nested ? '100%' : '530px' }">
<div
class="backgroundColor pa-2 menu-filter-dropdown"
:class="{ nested }"
:style="{ width: nested ? '100%' : '630px' }"
>
<div class="grid" @click.stop>
<template v-for="(filter, i) in filters" dense>
<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">
<template v-if="filter.is_group">
<v-icon
v-if="!filter.readOnly"
:key="i + '_3'"
:key="i + '_1'"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter, i)"
@ -15,7 +18,11 @@
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 + '_2'"
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']"
@ -31,9 +38,12 @@
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select>
</div>
<span :key="i + '_3'" style="grid-column: span 3" />
</template>
<div v-if="filter.is_group" :key="i + '_4'" style="grid-column: span 5; padding: 6px" class="elevation-4">
<column-filter
v-if="filter.id || shared"
v-if="filter.id || shared || !autoApply"
ref="nestedFilter"
v-model="filter.children"
:parent-id="filter.id"
@ -50,19 +60,19 @@
<template v-else>
<v-icon
v-if="!filter.readOnly"
:key="i + '_3'"
:key="i + '_5'"
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>
<span v-else :key="i + '_5'" />
<span v-if="!i" :key="i + '_6'" class="caption d-flex align-center">{{ $t('labels.where') }}</span>
<v-select
v-else
:key="i + '_4'"
:key="i + '_6'"
v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']"
@ -80,7 +90,7 @@
</v-select>
<field-list-auto-complete-dropdown
:key="i + '_6'"
:key="i + '_7'"
v-model="filter.fk_column_id"
class="caption nc-filter-field-select"
:columns="columns"
@ -90,12 +100,12 @@
/>
<v-select
:key="'k' + i"
v-if="filter && filter.fk_column_id"
:key="i + '_8'"
v-model="filter.comparison_op"
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select"
:items="filterComparisonOp(filter)"
:placeholder="$t('labels.operation')"
v-show="filter && filter.fk_column_id"
solo
flat
style="max-width: 120px"
@ -110,20 +120,20 @@
<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" />
<span v-else :key="i + '_8'" />
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="i + '_5'" />
<v-checkbox
v-else-if="types[filter.field] === 'boolean'"
:key="i + '_7'"
:key="i + '_9'"
v-model="filter.value"
dense
:disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)"
/>
<v-text-field
v-else
:key="i + '_7'"
v-else-if="filter && filter.fk_column_id"
:key="i + '_9'"
v-model="filter.value"
v-show="filter && filter.fk_column_id"
solo
flat
hide-details
@ -133,6 +143,7 @@
@click.stop
@input="saveOrUpdate(filter, i)"
/>
<span v-else :key="i + '_9'" />
</template>
</template>
</template>
@ -143,6 +154,11 @@
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</v-btn>
<v-btn v-if="!webHook" small class="elevation-0 grey--text my-3" @click.stop="addFilterGroup">
<v-icon small color="grey"> mdi-plus </v-icon>
Add Filter Group
<!-- todo: add i18n {{ $t('activity.addFilterGroup') }}-->
</v-btn>
<slot />
</div>
</template>
@ -281,11 +297,22 @@ export default {
},
},
watch: {
async viewId(v) {
viewId: {
async handler(v) {
if (v) {
await this.loadFilter();
}
},
immediate: true,
},
hookId: {
async handler(v) {
if (v) {
await this.loadFilter();
}
},
immediate: true,
},
filters: {
handler(v) {
this.$emit('input', v && v.filter(f => (f.fk_column_id && f.comparison_op) || f.is_group));
@ -293,9 +320,6 @@ export default {
deep: true,
},
},
created() {
this.loadFilter();
},
methods: {
filterComparisonOp(f) {
return this.comparisonOp.filter(op => {
@ -332,11 +356,15 @@ export default {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId,
children: undefined,
status: undefined,
});
} else {
await this.$api.dbTableFilter.update(filter.id, {
...filter,
fk_parent_id: this.parentId,
children: undefined,
status: undefined,
});
}
} else if (this.hookId || hookId) {
@ -346,6 +374,7 @@ export default {
await this.$api.dbTableWebhookFilter.create(this.hookId || hookId, {
...filter,
fk_parent_id: this.parentId,
status: undefined,
})
);
} else {
@ -355,6 +384,7 @@ export default {
await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId,
status: undefined,
})
);
}
@ -362,9 +392,11 @@ export default {
}
if (this.$refs.nestedFilter) {
for (const nestedFilter of this.$refs.nestedFilter) {
if (nestedFilter.parentId) {
await nestedFilter.applyChanges(true);
}
}
}
this.loadFilter();
if (!nested) {
this.$emit('updated');
@ -373,12 +405,12 @@ export default {
async loadFilter() {
let filters = [];
if (this.viewId && this._isUIAllowed('filterSync')) {
filters = this.parentId
filters = this.nested
? await this.$api.dbTableFilter.childrenRead(this.parentId)
: await this.$api.dbTableFilter.read(this.viewId);
}
if (this.hookId && this._isUIAllowed('filterSync')) {
filters = this.parentId
filters = this.nested
? await this.$api.dbTableFilter.childrenRead(this.parentId)
: await this.$api.dbTableWebhookFilter.read(this.hookId);
}
@ -401,6 +433,7 @@ export default {
parentId: this.parentId,
is_group: true,
status: 'update',
logical_op: 'and',
});
this.filters = this.filters.slice();
const index = this.filters.length - 1;
@ -468,4 +501,8 @@ export default {
column-gap: 6px;
row-gap: 6px;
}
.nc-filter-value-select {
min-width: 100px;
}
</style>

22
packages/nc-gui/components/project/spreadsheet/components/SharedViewsList.vue

@ -18,6 +18,10 @@
<!--Password-->
{{ $t('labels.password') }}
</th>
<th class="caption grey--text">
<!-- TODO: i18n -->
Download Allowed
</th>
<th class="caption grey--text">
<!--Actions-->
{{ $t('labels.actions') }}
@ -46,6 +50,11 @@
</v-icon>
</template>
</td>
<td class="caption text-center">
<template v-if="'meta' in currentView">
<span>{{ renderAllowCSVDownload(currentView) }}</span>
</template>
</td>
<td class="caption">
<v-icon small @click="copyLink(currentView)"> mdi-content-copy </v-icon>
<v-icon small @click="deleteLink(currentView.id)"> mdi-delete-outline </v-icon>
@ -80,6 +89,11 @@
</v-icon>
</template>
</td>
<td class="caption text-center">
<template v-if="'meta' in link">
<span>{{ renderAllowCSVDownload(link) }}</span>
</template>
</td>
<td class="caption">
<v-icon small @click="copyLink(link)"> mdi-content-copy </v-icon>
<v-icon small @click="deleteLink(link.id)"> mdi-delete-outline </v-icon>
@ -173,6 +187,14 @@ export default {
}
return `/nc/${viewType}/${view.uuid}`;
},
renderAllowCSVDownload(view) {
if (view.type === ViewTypes.GRID) {
view.meta = view.meta && typeof view.meta === 'string' ? JSON.parse(view.meta) : view.meta;
return view.meta.allowCSVDownload ? '✔' : '❌';
} else {
return 'N/A';
}
},
},
};
</script>

56
packages/nc-gui/components/project/spreadsheet/components/SortListMenu.vue

@ -1,7 +1,7 @@
<template>
<v-menu offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-badge :value="sortList && sortList.length" color="primary" dot overlap>
<v-badge :value="validSortsExist" color="primary" dot overlap>
<v-btn
v-t="['c:sort']"
class="nc-sort-menu-btn px-2 nc-remove-border"
@ -10,7 +10,7 @@
text
outlined
:class="{
'primary lighten-5 grey--text text--darken-3': sortList && sortList.length,
'primary lighten-5 grey--text text--darken-3': validSortsExist,
}"
v-on="on"
>
@ -37,13 +37,11 @@
@change="saveOrUpdate(sort, i)"
/>
<v-select
v-if="sort && sort.fk_column_id"
:key="i + 'sel2'"
v-model="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="[
{ text: 'A -> Z', value: 'asc' },
{ text: 'Z -> A', value: 'desc' },
]"
:items="renderSortDirection(sort)"
:label="$t('labels.operation')"
solo
flat
@ -98,6 +96,12 @@ export default {
icon: getUIDTIcon(c.uidt),
}));
},
validSortsExist() {
return this.sortList && this.sortList.filter(sort => sort.fk_column_id !== null).length > 0;
},
columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {});
},
},
watch: {
value(v) {
@ -160,6 +164,44 @@ export default {
this.$emit('updated');
this.$e('a:sort:delete');
},
renderSortDirection(sort) {
// TODO: handle single / multiple select when reordering is available
// [
// { text: 'First Last', value: 'asc' },
// { text: 'Last First', value: 'desc' },
// ]
switch (this.columnsById[sort.fk_column_id].uidt) {
case UITypes.Year:
case UITypes.Number:
case UITypes.Decimal:
case UITypes.Rating:
case UITypes.Count:
case UITypes.AutoNumber:
case UITypes.Time:
case UITypes.Currency:
case UITypes.Percent:
case UITypes.Duration:
case UITypes.PhoneNumber:
case UITypes.Date:
case UITypes.DateTime:
case UITypes.CreateTime:
case UITypes.LastModifiedTime:
return [
{ text: '1 → 9', value: 'asc' },
{ text: '9 → 1', value: 'desc' },
];
case UITypes.Checkbox:
return [
{ text: '▢ → ✓', value: 'asc' },
{ text: '✓ → ▢', value: 'desc' },
];
default:
return [
{ text: 'A → Z', value: 'asc' },
{ text: 'Z → A', value: 'desc' },
];
}
},
},
};
</script>
@ -167,7 +209,7 @@ export default {
<style scoped>
.sort-grid {
display: grid;
grid-template-columns: 22px auto 100px;
grid-template-columns: 22px auto 120px;
column-gap: 6px;
row-gap: 6px;
}

102
packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue

@ -316,19 +316,26 @@
<v-icon small class="pointer" @click="copyShareUrlToClipboard"> mdi-content-copy </v-icon>
</div>
<v-switch v-model="passwordProtect" dense @change="onPasswordProtectChange">
<template #label>
<!-- Restrict access with a password -->
<span v-show="!passwordProtect" class="caption">
{{ $t('msg.info.beforeEnablePwd') }}
</span>
<!-- Access is password restricted -->
<span v-show="passwordProtect" class="caption">
{{ $t('msg.info.afterEnablePwd') }}
</span>
</template>
</v-switch>
<v-expansion-panels v-model="advanceOptionsPanel" class="mx-auto" flat>
<v-expansion-panel>
<v-expansion-panel-header hide-actions>
<v-spacer />
<span class="grey--text caption"
>More Options
<v-icon color="grey" small>
mdi-chevron-{{ advanceOptionsPanel === 0 ? 'up' : 'down' }}
</v-icon></span
>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-checkbox
v-model="passwordProtect"
class="caption"
:label="$t('msg.info.beforeEnablePwd')"
hide-details
dense
@change="onPasswordProtectChange"
/>
<div v-if="passwordProtect" class="d-flex flex-column align-center justify-center">
<v-text-field
v-model="shareLink.password"
@ -354,6 +361,18 @@
{{ $t('placeholder.password.save') }}
</v-btn>
</div>
<v-checkbox
v-if="selectedView && selectedView.type === viewTypes.GRID"
v-model="allowCSVDownload"
class="caption"
label="Allow Download"
hide-details
dense
@change="onAllowCSVDownloadChange"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-container>
</v-card>
</v-dialog>
@ -410,6 +429,7 @@ export default {
queryParams: Object,
},
data: () => ({
advanceOptionsPanel: false,
webhookSliderModal: false,
codeSnippetModal: false,
drag: false,
@ -425,6 +445,7 @@ export default {
searchQueryVal: '',
showShareLinkPassword: false,
passwordProtect: false,
allowCSVDownload: true,
sharedViewPassword: '',
overAdvShieldIcon: false,
overShieldIcon: false,
@ -611,6 +632,9 @@ export default {
this.saveShareLinkPassword();
}
},
onAllowCSVDownloadChange() {
this.saveAllowCSVDownload();
},
async saveShareLinkPassword() {
try {
await this.$api.dbViewShare.update(this.shareLink.id, {
@ -632,6 +656,27 @@ export default {
this.$e('a:view:share:enable-pwd');
},
async saveAllowCSVDownload() {
try {
const meta =
this.shareLink.meta && typeof this.shareLink.meta === 'string'
? JSON.parse(this.shareLink.meta)
: this.shareLink.meta;
meta.allowCSVDownload = this.allowCSVDownload;
await this.$api.dbViewShare.update(this.shareLink.id, {
meta,
});
this.$toast.success('Successfully updated').goAway(3000);
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000);
}
if (this.allowCSVDownload) {
this.$e('a:view:share:enable-csv-download');
} else {
this.$e('a:view:share:disable-csv-download');
}
},
async loadViews() {
// this.viewsList = await this.sqlOp(
// {
@ -725,34 +770,12 @@ export default {
this.$e('a:view:delete', { view: view.type });
},
async genShareLink() {
// const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias },
// 'createSharedViewLink',
// {
// model_name: this.table,
// // meta: this.meta,
// query_params: {
// where: this.concatenatedXWhere,
// sort: this.sort,
// fields: Object.keys(this.showFields)
// .filter(f => this.showFields[f])
// .join(','),
// showFields: this.showFields,
// fieldsOrder: this.fieldsOrder,
// extraViewParams: this.extraViewParams,
// selectedViewId: this.selectedViewId,
// columnsWidth: this.columnsWidth
// },
// view_name: this.selectedView.title,
// type: this.selectedView.type,
// show_as: this.selectedView.show_as,
// password: this.sharedViewPassword
// }
// ])
const shared = await this.$api.dbViewShare.create(this.selectedViewId);
shared.meta = shared.meta && typeof shared.meta === 'string' ? JSON.parse(shared.meta) : shared.meta;
// todo: url
this.shareLink = shared;
this.passwordProtect = shared.password !== null;
this.allowCSVDownload = shared.meta.allowCSVDownload;
this.showShareModel = true;
},
copyView(view, i) {
@ -903,4 +926,7 @@ export default {
opacity: 0.5;
background: grey;
}
.mx-auto .v-expansion-panel {
background: var(--v-backgroundColor-base);
}
</style>

9
packages/nc-gui/components/project/spreadsheet/components/VirtualHeaderCell.vue

@ -101,7 +101,7 @@
</div>
</template>
<script>
import { UITypes } from 'nocodb-sdk';
import { UITypes, substituteColumnIdWithAliasInFormula } from 'nocodb-sdk';
import { getUIDTIcon } from '../helpers/uiTypes';
import EditVirtualColumn from '~/components/project/spreadsheet/components/EditVirtualColumn';
@ -198,7 +198,12 @@ export default {
} else if (this.type === 'lk') {
return `'${this.childColumn.title}' from '${this.childTable}' (${this.childColumn.uidt})`;
} else if (this.type === 'formula') {
return `Formula - ${this.column.colOptions.formula}`;
const formula = substituteColumnIdWithAliasInFormula(
this.column.colOptions.formula,
this.meta.columns,
this.column.colOptions.formula_raw
);
return `Formula - ${formula}`;
} else if (this.type === 'rl') {
return `'${this.childColumn.title}' of '${this.childTable}' (${this.childColumn.uidt})`;
}

5
packages/nc-gui/components/project/spreadsheet/components/editColumn/FormulaOptions.vue

@ -135,7 +135,10 @@ export default {
examples: formulas[fn].examples,
})),
...this.meta.columns
.filter(c => !this.column || this.column.id !== c.id)
.filter(
c =>
!this.column || (this.column.id !== c.id && !(c.uidt === UITypes.LinkToAnotherRecord && c.system === 1))
)
.map(c => ({
text: c.title,
type: 'column',

3
packages/nc-gui/components/project/spreadsheet/components/editColumn/LookupOptions.vue

@ -75,7 +75,8 @@ export default {
col: c.colOptions,
column: c,
...this.tables.find(t => t.id === c.colOptions.fk_related_model_id),
}));
}))
.filter(table => table.col.fk_related_model_id === table.id && !table.mm);
return refTables;
},

2
packages/nc-gui/components/project/spreadsheet/components/virtualCell/LookupCell.vue

@ -141,7 +141,7 @@ export default {
},
localValueObj() {},
localValue() {
return this.value && (Array.isArray(this.value) ? this.value : [this.value]);
return this.value && (Array.isArray(this.value) ? this.value : [this.value]).filter(v => v !== null);
},
queryParams() {},
},

9
packages/nc-gui/components/project/spreadsheet/public/XcTable.vue

@ -46,7 +46,8 @@
@input="loadTableData"
/>
<csv-export-import
<more-actions
v-if="allowCSVDownload"
:is-view="isView"
:query-params="{ ...queryParams, showFields, fieldsOrder }"
:public-view-id="$route.params.id"
@ -128,12 +129,12 @@ import FieldsMenu from '../components/FieldsMenu';
import SortListMenu from '../components/SortListMenu';
import ColumnFilterMenu from '../components/ColumnFilterMenu';
import XcGridView from '../views/GridView';
import CsvExportImport from '~/components/project/spreadsheet/components/MoreActions';
import MoreActions from '~/components/project/spreadsheet/components/MoreActions';
export default {
name: 'XcTable',
components: {
CsvExportImport,
MoreActions,
XcGridView,
ColumnFilterMenu,
SortListMenu,
@ -236,6 +237,7 @@ export default {
rowContextMenu: null,
modelName: null,
tableMeta: null,
allowCSVDownload: true,
}),
computed: {
concatenatedXWhere() {
@ -414,6 +416,7 @@ export default {
this.sorts = this.viewMeta.sorts;
this.viewName = this.viewMeta.title;
this.client = this.viewMeta.client;
this.allowCSVDownload = JSON.parse(this.viewMeta.meta).allowCSVDownload;
} catch (e) {
if (e.response && e.response.status === 404) {
this.notFound = true;

14
packages/nc-gui/components/utils/DlgTableCreate.vue

@ -23,7 +23,7 @@
persistent-hint
dense
hide-details1
:rules="[validateTableName, validateDuplicateAlias]"
:rules="[validateTableName, validateDuplicateAlias, validateLength]"
:hint="$t('msg.info.enterTableName')"
class="mt-4 caption nc-table-name"
/>
@ -231,6 +231,18 @@ export default {
(this.tables || []).every(t => t.table_name.toLowerCase() !== (v || '').toLowerCase()) || 'Duplicate table name'
);
},
validateLength(v) {
let tableNameLengthLimit = 255;
const sqlClientType = this.$store.getters['project/GtrClientType'];
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
return v.length <= tableNameLengthLimit || `Table name exceeds ${tableNameLengthLimit} characters`;
},
onCreateBtnClick() {
this.$emit('create', {
...this.table,

422
packages/nc-gui/lang/ko.json

@ -1,84 +1,84 @@
{
"general": {
"home": "",
"load": "",
"open": "열려있는",
"close": "닫",
"home": "",
"load": "불러오기",
"open": "열",
"close": "닫",
"yes": "네",
"no": "아니요",
"ok": "확인",
"and": "그리고",
"or": "또는",
"add": "추가하다",
"edit": "편집하다",
"remove": "제거하다",
"save": "구하다",
"add": "추가",
"edit": "편집",
"remove": "제거",
"save": "저장",
"cancel": "취소",
"submit": "제출하다",
"submit": "제출",
"create": "생성",
"insert": "끼워 넣다",
"insert": "삽입",
"delete": "삭제",
"update": "업데이트",
"rename": "이름 바꾸기",
"reload": "재 장전",
"reload": "다시 불러오기",
"reset": "초기화",
"install": "설치",
"show": "보여 주다",
"hide": "숨",
"show": "표시",
"hide": "숨기기",
"showAll": "모두 표시",
"hideAll": "모두 숨기기",
"showMore": "자세히 보기",
"showOptions": " 옵션",
"showOptions": "보기 옵션",
"hideOptions": "옵션 숨기기",
"showMenu": "메뉴 표시",
"hideMenu": "메뉴 숨기기",
"addAll": "모두 추가하십시오",
"addAll": "모두 추가",
"removeAll": "모두 제거",
"signUp": "가입하기",
"signIn": "로그인",
"signOut": "로그 아웃하십시오",
"signOut": "로그아웃",
"required": "필수의",
"preferred": "우선의",
"mandatory": "필수적인",
"loading": "...에 적재 ...",
"loading": "불러오는 중 ...",
"title": "제목",
"upload": "업로드",
"download": "... 다운로드하기",
"download": "다운로드",
"default": "기본",
"more": "",
"more": "기타",
"less": "더 적은",
"event": "이벤트",
"condition": "상태",
"after": "후에",
"before": "전에",
"search": "검색",
"notification": "공고",
"notification": "알림",
"reference": "참조",
"function": "함수"
"function": "기능"
},
"objects": {
"project": "프로젝트",
"projects": "프로젝트",
"table": "테이블",
"tables": "테이블",
"field": "",
"field": "필드",
"fields": "필드",
"column": "열",
"columns": "열",
"page": "페이지",
"pages": "페이지",
"record": "기록",
"records": "기록",
"webhook": "웹후의",
"webhooks": "WebHooks.",
"view": "보다",
"views": "보기",
"record": "레코드",
"records": "레코드",
"webhook": "웹",
"webhooks": "웹훅",
"view": "",
"views": "",
"viewType": {
"grid": "그리드",
"gallery": "갤러리",
"form": "양식",
"kanban": "Kanban",
"calendar": "달력"
"form": "",
"kanban": "칸반",
"calendar": "캘린더"
},
"user": "사용자",
"users": "사용자",
@ -86,7 +86,7 @@
"roles": "역할",
"roleType": {
"owner": "소유자",
"creator": "창조자",
"creator": "생성자",
"editor": "편집자",
"commenter": "해설자",
"viewer": "뷰어"
@ -94,14 +94,14 @@
},
"datatype": {
"ID": "ID",
"ForeignKey": "외 키",
"SingleLineText": "단일 줄 텍스트",
"ForeignKey": "외 키",
"SingleLineText": " 줄 텍스트",
"LongText": "긴 텍스트",
"Attachment": "부",
"Attachment": "부",
"Checkbox": "체크 박스",
"MultiSelect": "다중 선택",
"SingleSelect": "단일 선택",
"Collaborator": "협력자",
"Collaborator": "동료",
"Date": "날짜",
"Year": "년도",
"Time": "시간",
@ -112,44 +112,44 @@
"Decimal": "소수",
"Currency": "통화",
"Percent": "퍼센트",
"Duration": "지속",
"Rating": "평가",
"Duration": "기간",
"Rating": "등급",
"Formula": "공식",
"Rollup": "롤업",
"Count": "세다",
"Count": "카운트",
"Lookup": "조회",
"DateTime": "날짜 시간",
"CreateTime": "시간을 만드십시오",
"LastModifiedTime": "마지막 수정 된 시간",
"DateTime": "일시",
"CreateTime": "생성시간",
"LastModifiedTime": "최종수정시간",
"AutoNumber": "자동번호",
"Barcode": "바코드",
"Button": "단추",
"Button": "버튼",
"Password": "비밀번호",
"relationProperties": {
"noAction": "조치없음",
"cascade": "종속",
"restrict": "얽매다",
"setNull": "null.",
"setDefault": "기본값으로 설정"
"cascade": "캐스케이드",
"restrict": "제한",
"setNull": "null 설정",
"setDefault": "기본값 설정"
}
},
"filterOperation": {
"isEqual": "~는 같다",
"isNotEqual": "같지 않다",
"isLike": "처럼",
"isNot like": "아닙니다",
"isEmpty": "비었다",
"isNotEmpty": "비어 있지 않습니다",
"isNull": "null입니다",
"isNotNull": "null이 아닙니다"
"isEqual": "is equal",
"isNotEqual": "is not equal",
"isLike": "is like",
"isNot like": "is not like",
"isEmpty": "is empty",
"isNotEmpty": "is not empty",
"isNull": "is null",
"isNotNull": "is not null"
},
"title": {
"newProj": "새 프로젝트",
"myProject": "내 프로젝트",
"formTitle": "양식 제목",
"collabView": "협업보기",
"lockedView": "잠긴보기",
"personalView": "개인적인 견해",
"collabView": "공동작업 뷰",
"lockedView": "\b잠긴 뷰",
"personalView": "개인",
"appStore": "앱 스토어",
"teamAndAuth": "팀 및 인증",
"rolesUserMgmt": "역할 및 사용자 관리",
@ -159,32 +159,32 @@
"projMeta": "프로젝트 메타 데이터",
"metaMgmt": "메타 관리",
"metadata": "메타 데이터",
"exportImportMeta": "내보내기 / 가져 오기 메타 데이터",
"exportImportMeta": "메타 데이터 내보내기/가져오기 ",
"uiACL": "UI 액세스 제어",
"metaOperations": "메타 데이터 작업",
"audit": "사",
"audit": "사",
"auditLogs": "감사 로그",
"sqlMigrations": "SQL 마이그레이션",
"dbCredentials": "데이터베이스 자격 증명",
"advancedParameters": "SSL 및 고급 매개 변수",
"headCreateProject": "프로젝트 만들기 | nocodb.",
"headLogin": "로그인 | NocoDB.",
"resetPassword": "비밀번호 재설정",
"teamAndSettings": "Team & Settings",
"apiDocs": "API Docs",
"importFromAirtable": "Import From Airtable"
"headCreateProject": "프로젝트 만들기 | nocoDB",
"headLogin": "로그인 | NocoDB",
"resetPassword": "비밀번호 재설정",
"teamAndSettings": "팀 & 설정",
"apiDocs": "API 문서",
"importFromAirtable": "Airtable에서 불러오기"
},
"labels": {
"notifyVia": "통지를 통지합니다",
"notifyVia": "공지",
"projName": "프로젝트 이름",
"tableName": " 이름",
"viewName": "이름을 봅니다",
"tableName": "테이블 이름",
"viewName": "이름 보기",
"viewLink": "링크 보기",
"columnName": "열 이름",
"columnType": "열 유형",
"roleName": "역할 이름",
"roleDescription": "역할 설명",
"databaseType": "데이터베이스 입력하십시오",
"databaseType": "데이터베이스 입력",
"lengthValue": "길이 / 값",
"dbType": "데이터베이스 유형",
"sqliteFile": "SQLite 파일",
@ -192,53 +192,53 @@
"port": "포트 번호",
"username": "사용자 이름",
"password": "비밀번호",
"schemaName": "Schema name",
"schemaName": "스키마 이름",
"action": "동작",
"actions": "행위",
"operation": "작업",
"operationType": "작 유형",
"operationSubType": "작 하위 유형",
"operationType": "작 유형",
"operationSubType": "작 하위 유형",
"description": "설명",
"authentication": "증",
"authentication": "증",
"token": "토큰",
"where": "어디에",
"cache": "은닉처",
"where": "위치",
"cache": "캐시",
"chat": "채팅",
"email": "이메일",
"storage": "저장",
"storage": "스토리지",
"uiAcl": "UI-ACL",
"models": "모델",
"syncState": "동기화 상태",
"created": "만들어진",
"created": "생성",
"sqlOutput": "SQL 출력",
"addOption": "옵션 추가",
"aggregateFunction": "집계 기능",
"aggregateFunction": "집합 함수",
"dbCreateIfNotExists": "데이터베이스 : 존재하지 않는 경우 생성",
"clientKey": "클라이언트 키",
"clientCert": "클라이언트 Cert.",
"serverCA": "서버 CA.",
"requriedCa": "필수 -CA",
"serverCA": "서버 CA",
"requriedCa": "필수 CA",
"requriedIdentity": "필수 - IDENTITY",
"inflection": {
"tableName": "편집 - 표 이름",
"columnName": "편집 - 열 이름"
},
"community": {
"starUs1": "",
"starUs2": "우리는 github에 있습니다",
"bookDemo": "무료 데모 예약하십시오",
"getAnswered": "귀하의 질문에 답변 해주십시오",
"starUs1": "",
"starUs2": "Github",
"bookDemo": "무료 데모 예약",
"getAnswered": "디스코드",
"joinDiscord": "디스코드 참가",
"joinCommunity": "Join NocoDB Community",
"joinReddit": "가입 /r/NocoDB",
"followNocodb": "NocoDB 팔로우 하세요"
"joinCommunity": "NocoDB 커뮤니티 참가",
"joinReddit": "Join /r/NocoDB",
"followNocodb": "NocoDB 팔로우"
},
"docReference": "문서 참조",
"selectUserRole": "사용자 역할을 선택하십시오",
"childTable": "하위 테이블",
"childColumn": "아이 칼럼",
"docReference": "참조 문서",
"selectUserRole": "사용자 역할을 선택하세요",
"childTable": "자식 테이블",
"childColumn": "자식 컬럼",
"onUpdate": "업데이트 시 ",
"onDelete": "삭제"
"onDelete": "삭제"
},
"activity": {
"createProject": "프로젝트 생성",
@ -250,46 +250,46 @@
"restartProject": "프로젝트 재시작",
"deleteProject": "프로젝트 삭제",
"refreshProject": "프로젝트 새로 고침",
"saveProject": "프로젝트를 저장하다",
"saveProject": "저장",
"createProjectExtended": {
"extDB": "외부 데이터베이스에 <br>을 연결하여 작성하십시오",
"excel": "Excel에서 프로젝트 만들기",
"template": "템플릿에서 프로젝트를 만듭니다"
"extDB": "외부 데이터베이스 연결",
"excel": "엑셀에서 프로젝트 만들기",
"template": "템플릿에서 프로젝트 만들기"
},
"OkSaveProject": "OK 및 저장 프로젝트",
"OkSaveProject": "확인 및 프로젝트 저장",
"upgrade": {
"available": "업그레이드 가능",
"releaseNote": "릴리즈 노트",
"howTo": "업그레이드 방법?"
"howTo": "어떻게 업그레이드 하나요?"
},
"translate": "도움말 번역",
"account": {
"authToken": "인증 토큰 복사하십시오",
"authToken": "인증 토큰 복사",
"swagger": "Swagger API Doc.",
"projInfo": "프로젝트 정보 복사하십시오",
"projInfo": "프로젝트 정보 복사",
"themes": "테마"
},
"sort": "종류",
"addSort": "정렬 옵션 추가하십시오",
"addSort": "정렬 옵션 추가",
"filter": "필터",
"addFilter": "필터 추가하십시오",
"share": "공유하다",
"addFilter": "필터 추가",
"share": "공유",
"shareBase": {
"disable": "공유베이스 비활성화합니다",
"enable": "링크가있는 사람",
"link": "공유 기본 링크"
"disable": "공유 베이스 비활성화",
"enable": "링크가 있는 모든 사용자",
"link": "공유 링크"
},
"invite": "초대",
"inviteMore": "더 많은 것을 초대하십시오",
"inviteTeam": "팀 초대하십시오",
"inviteToken": "토큰을 초대하십시오",
"newUser": "새로운 사용자",
"inviteMore": "더 많은 사람 초대",
"inviteTeam": "팀 초대",
"inviteToken": "초대 토큰",
"newUser": "사용자 추가",
"editUser": "사용자 편집",
"deleteUser": "사용자를 프로젝트에서 제거하십시오",
"resendInvite": "초대 이메일을 다시 보내십시오",
"copyInviteURL": "초대 URL 복사",
"deleteUser": "사용자 제거",
"resendInvite": "초대 이메일 재전송",
"copyInviteURL": "초대 URL 복사",
"newRole": "새로운 역할",
"reloadRoles": "다시로드 역할",
"reloadRoles": "역할 다시 불러오기",
"nextPage": "다음 페이지",
"prevPage": "이전 페이지",
"nextRecord": "다음 기록",
@ -299,16 +299,16 @@
"refreshTable": "테이블 새로 고침",
"renameTable": "테이블 이름 바꾸기",
"deleteTable": "테이블 삭제",
"addField": "이 표에 새 필드를 추가하십시오",
"setPrimary": "기본값으로 설정하십시오",
"addRow": " 추가하십시오",
"saveRow": "행 저장하십시오",
"insertRow": " 삽입하십시오",
"addField": "테이블에 새 필드 추가",
"setPrimary": "Primary value로 설정",
"addRow": "행 추가",
"saveRow": "행 저장",
"insertRow": "행 삽입",
"deleteRow": "행 삭제",
"deleteSelectedRow": "선택한 행 삭제합니다",
"importExcel": "수입 Excel",
"importCSV": "Import CSV",
"downloadCSV": "CSV 다운로드하십시오",
"deleteSelectedRow": "선택한 행 삭제",
"importExcel": "엑셀 가져오기",
"importCSV": "CSV 가져오기",
"downloadCSV": "CSV 다운로드",
"uploadCSV": "CSV 업로드",
"import": "가져오기",
"importMetadata": "메타 데이터 가져오기",
@ -316,24 +316,24 @@
"clearMetadata": "메타 데이터 지우기",
"exportToFile": "파일로 내보내기",
"changePwd": "암호 변경",
"createView": "보기 생성",
"shareView": "보기 공유",
"listSharedView": "공유보기 목록",
"ListView": "보기 목록",
"copyView": "보기 복사",
"renameView": "보기 이름 변경",
"deleteView": "보기 삭제",
"createGrid": "그리드보기를 만듭니다",
"createGallery": "갤러리보기 만들기",
"createCalendar": "캘린더보기를 만듭니다",
"createKanban": "Kanban보기를 만드십시오",
"createForm": "양식보기를 만듭니다",
"createView": " 생성",
"shareView": " 공유",
"listSharedView": "공유 목록",
"ListView": " 목록",
"copyView": " 복사",
"renameView": " 이름 변경",
"deleteView": " 삭제",
"createGrid": "그리드 뷰 생성",
"createGallery": "갤러리 뷰 생성",
"createCalendar": "캘린더 뷰 생성",
"createKanban": "칸반 뷰 생성",
"createForm": "폼 뷰 생성",
"showSystemFields": "시스템 필드 표시",
"copyUrl": "URL 복사",
"openTab": "새 탭을 엽니 다",
"iFrame": "복사 Embeddable HTML 코드를 복사하십시오",
"addWebhook": "새로운 웹 호크를 추가하십시오",
"newToken": "새 토큰 추가하십시오",
"copyUrl": "URL 복사",
"openTab": "새 탭 열기",
"iFrame": "임베드 가능한 HTML 코드 복사",
"addWebhook": "새 웹훅 추가",
"newToken": "새 토큰 추가",
"exportZip": "zip 파일 내보내기",
"importZip": "zip 파일 가져오기",
"metaSync": "지금 동기화",
@ -341,32 +341,32 @@
"previewAs": "미리보기",
"resetReview": "미리보기 재설정",
"testDbConn": "데이터베이스 연결 테스트",
"removeDbFromEnv": "환경에서 데이터베이스 제거하십시오",
"editConnJson": "연결 편집 JSON.",
"sponsorUs": "우리를 후원하십시오",
"removeDbFromEnv": "환경에서 데이터베이스 제거",
"editConnJson": "연결 JSON 편집",
"sponsorUs": "후원",
"sendEmail": "이메일 보내기"
},
"tooltip": {
"saveChanges": "변경 사항 저장하다",
"xcDB": "새 프로젝트를 만듭니다",
"extDB": "MySQL, PostgreSQL, SQL Server 및 SQLite 지원합니다",
"saveChanges": "변경 사항 저장",
"xcDB": "새 프로젝트 생성",
"extDB": "MySQL, PostgreSQL, SQL Server 및 SQLite 지원",
"apiRest": "REST API를 통해 액세스 가능",
"apiGQL": "Graphql Apis를 통해 액세스 가능",
"theme": {
"dark": "그것은 검은 색으로옵니다 (^ ⇧b)",
"light": "검은 색으로 들어 오는가? (^ ⇧b)"
"dark": "현재 다크 테마입니다(^ ⇧b)",
"light": "다크 테마로 바꿀까요?(^ ⇧b)"
},
"addTable": "새 테이블 추가하십시오",
"inviteMore": "더 많은 사용자 초대하십시오",
"toggleNavDraw": "네비게이션 서랍을 토글합니다",
"reloadApiToken": "API 토큰을 다시로드하십시오",
"addTable": "새 테이블 추가",
"inviteMore": "더 많은 사용자 초대",
"toggleNavDraw": "네비게이션 바 토글",
"reloadApiToken": "API 토큰 다시 불러오기",
"generateNewApiToken": "새 API 토큰 생성",
"addRole": "새로운 역할 추가하십시오",
"reloadList": "Reload List.",
"addRole": "새로운 역할 추가",
"reloadList": "목록 다시 불러오기",
"metaSync": "동기화 메타 데이터",
"sqlMigration": "다시로드 이전",
"sqlMigration": "마이그레이션 다시 불러오기",
"updateRestart": "업데이트 및 다시 시작",
"cancelReturn": "취소 및 되돌리기",
"cancelReturn": "취소",
"exportMetadata": "메타 테이블에서 메타 디렉토리로 모든 메타 데이터를 내보냅니다.",
"importMetadata": "모든 메타 데이터를 메타 디렉토리에서 메타 테이블로 가져옵니다.",
"clearMetadata": "메타 테이블에서 모든 메타 데이터를 지우십시오.",
@ -383,19 +383,19 @@
"save": "비밀번호 저장",
"confirm": "새 암호를 확인합니다"
},
"searchProjectTree": "검색 테이블",
"searchFields": "검색 필드",
"searchColumn": "검색 {검색} 컬럼",
"searchApps": "검색 앱 검색",
"searchModels": "검색 모델",
"searchProjectTree": "테이블 검색",
"searchFields": "필드 검색",
"searchColumn": "{검색} 컬럼 검색",
"searchApps": "앱 검색",
"searchModels": "모델 검색",
"noItemsFound": "제품을 찾지 못했습니다",
"defaultValue": "기본값",
"filterByEmail": "전자 메일로 필터링합니다"
"filterByEmail": "전자 메일로 필터링"
},
"msg": {
"info": {
"footerInfo": "페이지 당 행",
"upload": "업로드 할 파일 선택하십시오",
"upload": "업로드 할 파일 선택",
"upload_sub": "또는 끌어서 놓기 파일",
"excelSupport": "지원 : .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "Excel 파일 URL을 입력하십시오",
@ -412,40 +412,40 @@
"deleteProject": "프로젝트를 삭제 하시겠습니까?",
"shareBasePrivate": "공개적으로 공유 할 수있는 ReadOnly Base를 생성합니다",
"shareBasePublic": "이 링크가있는 인터넷의 모든 사람은 볼 수 있습니다",
"userInviteNoSMTP": "아직 메일러를 구성하지 않은 것처럼 보입니다!\n초대장 링크를 복사하여 보냅니다.",
"userInviteNoSMTP": "아직 메일러를 구성하지 않은 것처럼 보입니다! \\ n 초대장 링크를 복사하여 보냅니다.",
"dragDropHide": "여기에서 필드를 드래그 앤 드롭하십시오",
"formInput": "양식 입력 레이블 입력하십시오",
"formHelpText": "도움말 텍스트 추가하십시오",
"onlyCreator": "작성자에게만 표시됩니다",
"formInput": "양식 입력 레이블 입력",
"formHelpText": "도움말 텍스트 추가",
"onlyCreator": "작성자에게만 표시",
"formDesc": "양식 설명 추가",
"beforeEnablePwd": "비밀번호로 액세스 제한",
"afterEnablePwd": "엑세스 비밀번호 제한되어 있습니다.",
"privateLink": "현재 보기가 비공개 링크를 통해 공유됩니다.",
"afterEnablePwd": "엑세스 비밀번호 제한",
"privateLink": "비공개 링크로 현재 뷰 공유",
"privateLinkAdditionalInfo": "비공개 링크가 있는 사용자는 현재 보기에서 표시되는 셀만 볼 수 있습니다.",
"afterFormSubmitted": "양식이 제출 된 후",
"apiOptions": "프로젝트 via에 액세스하십시오",
"submitAnotherForm": "'다른 양식 제출'버튼을 보여줍니다",
"showBlankForm": "5 초 후에 빈 양식을 보여줍니다",
"emailForm": "나에게 이메일을 보내주십시오",
"emailForm": "이메일로 공유",
"showSysFields": "시스템 필드 표시",
"filterAutoApply": "자동 적용",
"showMessage": "이 메시지를 표시하십시오",
"viewNotShared": "현재보기가 공유되지 않습니다!",
"showAllViews": "이 표의 모든 공유보기를 보여줍니다",
"collabView": "편집 권한 이상이있는 공동 작업자는 뷰 구성을 변경할 수 있습니다.",
"showMessage": "아래 메시지 표시",
"viewNotShared": "현재가 공유되지 않습니다!",
"showAllViews": "테이블의 공유된 모든 뷰 표시",
"collabView": "편집 권한 이상의 권한을 보유한 공동 작업자는 뷰 구성을 변경할 수 있습니다.",
"lockedView": "아무도 잠금 해제 될 때까지 뷰 구성을 편집할 수 없습니다.",
"personalView": "보기 구성을 편집 할 수 있습니다. 다른 공동 작업자의 개인 뷰는 기본적으로 숨겨져 있습니다.",
"personalView": "뷰 구성을 편집할 수 있습니다. 다른 공동 작업자의 개인 뷰는 기본적으로 표시되지 않습니다. ",
"ownerDesc": "크리에이터를 추가 / 제거 할 수 있습니다. 및 전체 편집 데이터베이스 구조 및 필드.",
"creatorDesc": "데이터베이스 구조 및 값을 완전히 편집 할 수 있습니다.",
"editorDesc": "레코드를 편집할 수 있지만 데이터베이스 / 필드의 구조를 변경할 수 없습니다.",
"commenterDesc": "레코드를보고 댓글을 줄 수는 있지만 아무 것도 편집 할 수 없습니다.",
"commenterDesc": "레코드를 보고 댓글을 추가할 수 있지만 아무 것도 편집 할 수 없습니다.",
"viewerDesc": "레코드를 볼 수 있지만 아무 것도 편집 할 수 없습니다",
"addUser": "새 사용자 추가",
"staticRoleInfo": "시스템 정의 역할을 편집 할 수 없습니다",
"staticRoleInfo": "시스템 정의 역할을 편집할 수 없습니다",
"exportZip": "프로젝트 메타를 zip 파일로 내보내고 다운로드하십시오.",
"importZip": "프로젝트 메타 zip 파일을 가져와서 다시 시작하십시오.",
"importText": "메타 데이터 zip 파일을 업로드하여 NocoDB 프로젝트를 가져옵니다",
"metaNoChange": "변경되지 않습니다",
"metaNoChange": "변경된 사항이 없습니다. ",
"sqlMigration": "스키마 마이그레이션이 자동으로 생성됩니다. 테이블을 만들고이 페이지를 새로 고칩니다.",
"dbConnectionStatus": "환경 검증",
"dbConnected": "연결 되었습니다.",
@ -455,9 +455,9 @@
},
"sponsor": {
"header": "당신은 우리를 도울 수 있습니다!",
"message": "우리는 nocodb 오픈 소스를 만들기 위해 풀 타임으로 일하는 작은 팀입니다. 우리는 NocodB와 같은 도구가 인터넷의 모든 문제 해결사에게 자유롭게 사용할 수 있어야합니다."
"message": "우리는 nocodb 오픈 소스를 만들기 위해 구성된 소규모 팀입니다. 우리는 NocodB와 같은 도구가 인터넷의 다양한 문제를 해결하는 데 자유롭게 사용할 수 있어야 한다고 믿습니다. "
},
"loginMsg": "NocoDB 로그인하십시오",
"loginMsg": "NocoDB 로그인",
"passwordRecovery": {
"message_1": "가입할 때 사용한 이메일 주소를 입력하십시오.",
"message_2": "이메일을 통해 비밀번호를 재설정할 링크를 보내드리겠습니다.",
@ -466,55 +466,55 @@
"signUp": {
"superAdmin": "당신은 'Super Admin'가 될 것입니다.",
"alreadyHaveAccount": "이미 계정이 있습니까?",
"workEmail": "작업 이메일을 입력하십시오",
"workEmail": "이메일을 입력하십시오",
"enterPassword": "비밀번호를 입력하십시오",
"forgotPassword": "비밀번호를 잊어 버렸습니까 ?",
"dontHaveAccount": "계정이 없습니까?"
},
"addView": {
"grid": "그리드 보기를 추가하십시오",
"gallery": "갤러리 보기를 추가하십시오",
"form": "양식 보기를 추가하십시오",
"kanban": "Kanban 보기를 추가하십시오",
"calendar": "달력 보기를 추가하십시오"
"grid": "그리드 뷰 추가",
"gallery": "갤러리 뷰 추가",
"form": "폼 뷰 추가",
"kanban": "칸반 뷰 추가",
"calendar": "캘린더 뷰 추가"
},
"tablesMetadataInSync": "테이블 메타 데이터가 동기화되어 있습니다",
"addMultipleUsers": "여러 쉼표 (,) 분리 된 이메일을 추가 할 수 있습니다.",
"addMultipleUsers": "쉼표로 분리된 이메일을 여러 개 추가할 수 있습니다.",
"enterTableName": "테이블 이름을 입력하십시오",
"addDefaultColumns": "기본 열 추가하십시오",
"addDefaultColumns": "기본 열 추가",
"tableNameInDb": "테이블 이름은 데이터베이스에 저장된 것입니다"
},
"error": {
"searchProject": "{검색} 검색 결과가 없습니다",
"searchProject": "검색 결과가 없습니다",
"invalidChar": "잘못된 폴더 경로입니다.",
"invalidDbCredentials": "데이터베이스 자격 증명 오류.",
"unableToConnectToDb": "데이터베이스에 연결할 수 없습니다. 데이터베이스 작동을 확인하십시오.",
"userDoesntHaveSufficientPermission": "사용자가 존재하지 않거나 schema를 만들 수있는 충분한 권한이 있습니다.",
"invalidDbCredentials": "데이터베이스 자격 증명 오류",
"unableToConnectToDb": "데이터베이스에 연결할 수 없습니다. 데이터베이스 상태를 확인하십시오.",
"userDoesntHaveSufficientPermission": "사용자가 존재하지 않거나 스키마를 만들 수있는 충분한 권한이 있습니다.",
"dbConnectionStatus": "잘못된 데이터베이스 매개 변수",
"dbConnectionFailed": "연결 실패 :",
"signUpRules": {
"emailReqd": "이메일이 필요합니다",
"emailInvalid": "이메일이 유효해야합니다",
"passwdRequired": "비밀번호가 필요합니다",
"emailReqd": "이메일이 필요합니다.",
"emailInvalid": "이메일이 유효해야합니다.",
"passwdRequired": "비밀번호가 필요합니다.",
"passwdLength": "비밀번호는 8자 이상이어야 합니다."
}
},
"toast": {
"exportMetadata": "프로젝트 메타 데이터가 성공적으로 내 보냈습니다",
"importMetadata": "프로젝트 메타 데이터가 성공적으로 가져온 것입니다",
"clearMetadata": "프로젝트 메타 데이터가 성공적으로 지워졌습니다",
"stopProject": "프로젝트가 성공적으로 중지되었습니다",
"startProject": "프로젝트가 성공적으로 시작되었습니다",
"restartProject": "프로젝트가 성공적으로 다시 시작되었습니다",
"deleteProject": "프로젝트가 성공적으로 삭제되었습니다",
"authToken": "인증 토큰이 클립 보드에 복사되었습니다",
"projInfo": "클립 보드에 복사 된 프로젝트 정보",
"inviteUrlCopy": "복사 된 초대 URL 클립 보드에",
"createView": "성공적으로 생성 된보기",
"formEmailSMTP": "전자 메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화하십시오.",
"collabView": "공동 작업로 성공적으로 전환했습니다",
"lockedView": "잠긴보기로 성공적으로 전환되었습니다",
"futureRelease": "곧 올 것입니다!"
"exportMetadata": "프로젝트 메타 데이터를 성공적으로 내보냈습니다.",
"importMetadata": "프로젝트 메타 데이터를 성공적으로 가져왔습니다.",
"clearMetadata": "프로젝트 메타 데이터를 성공적으로 지웠습니다.",
"stopProject": "프로젝트가 성공적으로 중지되었습니다.",
"startProject": "프로젝트가 성공적으로 시작되었습니다.",
"restartProject": "프로젝트가 성공적으로 다시 시작되었습니다.",
"deleteProject": "프로젝트가 성공적으로 삭제되었습니다.",
"authToken": "인증 토큰이 클립보드에 복사되었습니다.",
"projInfo": "프로젝트 정보가 클립보드에 복사되었습니다. ",
"inviteUrlCopy": "초대 URL 클립보드에 복사되었습니다. ",
"createView": "뷰가 성공적으로 생성되었습니다. ",
"formEmailSMTP": "메일 알림을 사용하려면 App Store에서 SMTP 플러그인을 활성화 하십시오.",
"collabView": "공동 작업로 성공적으로 전환했습니다",
"lockedView": "잠긴로 성공적으로 전환되었습니다",
"futureRelease": "Coming soon!"
}
}
}

2
packages/nc-gui/package-lock.json generated

@ -75,7 +75,7 @@
}
},
"../nocodb-sdk": {
"version": "0.92.3",
"version": "0.92.4",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",

52
packages/nc-gui/plugins/projectLoader.js

@ -1,3 +1,8 @@
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
dayjs.extend(duration)
export default async({ store, redirect, $axios, $toast, $api, route }) => {
// if (!route.path || !route.path.startsWith('/nc/')) { await store.dispatch('plugins/pluginPostInstall', 'Branding') }
if (window.location.search &&
@ -63,13 +68,15 @@ export default async({ store, redirect, $axios, $toast, $api, route }) => {
// fetch latest release info
const fetchReleaseInfo = async() => {
try {
const releaseInfo = await $api.utils.appInfo()
const latestRelease = await $api.utils.appVersion()
if (releaseInfo && latestRelease && releaseInfo.version) {
store.commit('app/MutReleaseVersion', releaseInfo.version)
store.commit('app/MutLatestRelease', latestRelease.releaseVersion || null)
const versionInfo = await $api.utils.appVersion()
if (versionInfo &&
versionInfo.releaseVersion &&
versionInfo.currentVersion &&
!(/[^0-9.]/.test(versionInfo.currentVersion))) {
store.commit('app/MutCurrentVersion', versionInfo.currentVersion)
store.commit('app/MutLatestRelease', versionInfo.releaseVersion)
} else {
store.commit('app/MutReleaseVersion', null)
store.commit('app/MutCurrentVersion', null)
store.commit('app/MutLatestRelease', null)
}
} catch (e) {
@ -77,9 +84,38 @@ export default async({ store, redirect, $axios, $toast, $api, route }) => {
}
}
fetchReleaseInfo().then(() => {
})
fetchReleaseInfo()
setInterval(fetchReleaseInfo, 10 * 60 * 1000)
handleFeedbackForm({ store, $axios })
}
const handleFeedbackForm = async({ store, $axios }) => {
const fetchFeedbackForm = async(now) => {
try {
const { data: feedbackForm } = await $axios.get('/api/v1/feedback_form')
const currentFeedbackForm = store.state.settings.feedbackForm
const isFetchedFormDuplicate = currentFeedbackForm.url === feedbackForm.url
store.commit('settings/MutFeedbackForm', {
url: feedbackForm.url,
lastFormPollDate: now.toISOString(),
createdAt: feedbackForm.created_at,
isHidden: isFetchedFormDuplicate ? currentFeedbackForm.isHidden : false
})
} catch (e) {
// ignore
}
}
const isFirstTimePolling = !store.state.settings.feedbackForm.lastFormPollDate
const now = dayjs()
const lastFormPolledDate = dayjs(store.state.settings.feedbackForm.lastFormPollDate)
if (isFirstTimePolling || (dayjs.duration(now.diff(lastFormPolledDate)).days() > 0)) {
fetchFeedbackForm(now)
}
}
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd

6
packages/nc-gui/store/app.js

@ -1,12 +1,12 @@
export const state = () => ({
releaseVersion: null,
currentVersion: null,
hiddenRelease: null,
latestRelease: null
})
export const mutations = {
MutReleaseVersion(state, releaseVersion) {
state.releaseVersion = releaseVersion
MutCurrentVersion(state, currentVersion) {
state.currentVersion = currentVersion
},
MutHiddenRelease(state, hiddenRelease) {
state.hiddenRelease = hiddenRelease

11
packages/nc-gui/store/settings.js

@ -39,7 +39,12 @@ export const state = () => ({
autoApplyFilter: true,
apiLoading: false,
includeM2M: false,
feedbackFormHidden: false
feedbackForm: {
// eslint-disable-next-line max-len
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true',
createdAt: new Date('2020-01-01T00:00:00.000Z'),
isHidden: false
}
})
export const mutations = {
@ -52,8 +57,8 @@ export const mutations = {
MutToggleLogWindow(state, show) {
state.logWindow = !state.logWindow
},
MutFeedbackFormHidden(state, show) {
state.feedbackFormHidden = show
MutFeedbackForm(state, feedbackForm) {
state.feedbackForm = feedbackForm
},
MutScreensaver(state, show) {
state.screensaver = show

2
packages/nc-lib-gui/package.json

@ -1,6 +1,6 @@
{
"name": "nc-lib-gui",
"version": "0.92.3",
"version": "0.92.4",
"description": "> TODO: description",
"author": "“pranavxc” <pranavxc@gmail.com>",
"homepage": "https://gitlab.com/xgenecloud-ts/xgenecloud-ts#readme",

1
packages/noco-docs/content/en/developer-resources/rest-apis.md

@ -179,6 +179,7 @@ Currently, the default value for {orgs} is <b>noco</b>. Users will be able to ch
| offset | o | Offset for pagination (SQL offset value) | 0 | 20 |
| sort | s | Sort by column name, Use `-` as prefix for descending sort | | column_name |
| fields | f | Required column names in result | * | column_name1,column_name2 |
| shuffle | r | Shuffle the result for pagination | 0 | 1 (Only allow 0 or 1. Other values would see it as 0) |
<!--
| fields1 | f1 | Required column names in child result | * | column_name1,column_name2 |

2
packages/noco-docs/content/en/engineering/builds-and-releases.md

@ -23,7 +23,7 @@ When a non-draft Pull Request is created, reopened or synchronized, a timely bui
The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely/tags) for the full list). Once the image is ready, Github bot will add a comment with the command in the pull request. The tag would be `<NOCODB_CURRENT_VERSION>-pr-<PR_NUMBER>-<YYYYMMDD>-<HHMM>`.
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
![image](https://user-images.githubusercontent.com/35857179/178413534-84b29afb-7a52-437b-a5df-b842b397390b.png)
## Executables

2
packages/noco-docs/content/en/setup-and-usages/column-operations.md

@ -38,7 +38,7 @@ Tip: You can create different grid views with different fields shown in each vie
## Sort
Sorting allows you to order contents alphabetically (A -> Z) / (Z -> A) (OR) in ascending / descending order. NocoDB allows nested sorting. You can choose column fields & order in which to apply nested sorting. Lookup, Formula, Nested Data are also supported in Sorting.
Sorting allows you to order contents alphabetically (A → Z) / (Z → A) (OR) in ascending / descending order. NocoDB allows nested sorting. You can choose column fields & order in which to apply nested sorting. Lookup, Formula, Nested Data are also supported in Sorting.
<img src="https://user-images.githubusercontent.com/86527202/144435903-84ed8e81-64ec-45e5-a045-9a993238c78c.png" width="75%"/>

4
packages/nocodb-sdk/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb-sdk",
"version": "0.92.3",
"version": "0.92.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb-sdk",
"version": "0.92.3",
"version": "0.92.4",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",

2
packages/nocodb-sdk/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb-sdk",
"version": "0.92.3",
"version": "0.92.4",
"description": "NocoDB SDK",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",

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

@ -724,6 +724,7 @@ export class HttpClient<SecurityDataType = unknown> {
formData.append(key, property);
} else if (typeof property === 'object' && property !== null) {
if (Array.isArray(property)) {
// eslint-disable-next-line functional/no-loop-statement
for (const prop of property) {
formData.append(`${key}[]`, prop);
}

20
packages/nocodb/package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "nocodb",
"version": "0.92.3",
"version": "0.92.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nocodb",
"version": "0.92.3",
"version": "0.92.4",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@google-cloud/storage": "^5.7.2",
@ -71,7 +71,7 @@
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.67",
"nc-lib-gui": "0.92.3",
"nc-lib-gui": "0.92.4",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",
@ -159,7 +159,7 @@
}
},
"../nocodb-sdk": {
"version": "0.92.3",
"version": "0.92.4",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
@ -15288,9 +15288,9 @@
}
},
"node_modules/nc-lib-gui": {
"version": "0.92.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.92.3.tgz",
"integrity": "sha512-am6HAc9Yc6BeE2/51UeTGCp66za2gJHyt5MuLLsfijWLRtOKDB1hVBnwBufnzeAWAqrlwoUyQl10kcl1LZ8VNQ==",
"version": "0.92.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.92.4.tgz",
"integrity": "sha512-Sozs8w9fQN0WscoxKDGsMqviM/6wasqaag3dRPZ+SISu+bhOnogWUuZXM8Ivl6+dcj3RkXR0n4llF0Z+NvJW/A==",
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
@ -36649,9 +36649,9 @@
}
},
"nc-lib-gui": {
"version": "0.92.3",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.92.3.tgz",
"integrity": "sha512-am6HAc9Yc6BeE2/51UeTGCp66za2gJHyt5MuLLsfijWLRtOKDB1hVBnwBufnzeAWAqrlwoUyQl10kcl1LZ8VNQ==",
"version": "0.92.4",
"resolved": "https://registry.npmjs.org/nc-lib-gui/-/nc-lib-gui-0.92.4.tgz",
"integrity": "sha512-Sozs8w9fQN0WscoxKDGsMqviM/6wasqaag3dRPZ+SISu+bhOnogWUuZXM8Ivl6+dcj3RkXR0n4llF0Z+NvJW/A==",
"requires": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",

4
packages/nocodb/package.json

@ -1,6 +1,6 @@
{
"name": "nocodb",
"version": "0.92.3",
"version": "0.92.4",
"description": "NocoDB",
"main": "dist/bundle.js",
"repository": "https://github.com/nocodb/nocodb",
@ -157,7 +157,7 @@
"nanoid": "^3.1.20",
"nc-common": "0.0.6",
"nc-help": "0.2.67",
"nc-lib-gui": "0.92.3",
"nc-lib-gui": "0.92.4",
"nc-plugin": "0.1.2",
"ncp": "^2.0.0",
"nocodb-sdk": "file:../nocodb-sdk",

2
packages/nocodb/src/lib/db/sql-data-mapper/lib/BaseModel.ts

@ -1511,6 +1511,7 @@ export interface XcFilter {
condition?: any;
conditionGraph?: any;
limit?: string | number;
shuffle?: string | number;
offset?: string | number;
sort?: string;
fields?: string;
@ -1521,6 +1522,7 @@ export interface XcFilterWithAlias extends XcFilter {
h?: string;
c?: any;
l?: string | number;
r?: string | number;
o?: string | number;
s?: string;
f?: string;

154
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -123,12 +123,13 @@ class BaseModelSqlv2 {
sort?: string | string[];
} = {}
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
const aliasColObjMap = await this.model.getAliasColObjMap();
const sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
const sorts = extractSortsObject(rest?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(
[
@ -178,10 +179,13 @@ class BaseModelSqlv2 {
const qb = this.dbDriver(this.tnPath);
await this.selectObject({ qb });
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
const aliasColObjMap = await this.model.getAliasColObjMap();
let sorts = extractSortsObject(args?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
let sorts = extractSortsObject(rest?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
// todo: replace with view id
if (!ignoreFilterSort && this.viewId) {
await conditionV2(
@ -247,7 +251,7 @@ class BaseModelSqlv2 {
if (!ignoreFilterSort) applyPaginate(qb, rest);
const proto = await this.getProto();
const data = await this.extractRawQueryAndExec(qb);
let data = await this.extractRawQueryAndExec(qb);
return data?.map((d) => {
d.__proto__ = proto;
@ -335,11 +339,15 @@ class BaseModelSqlv2 {
qb.count(`${this.model.primaryKey?.column_name || '*'} as count`);
qb.select(args.column_name);
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
const aliasColObjMap = await this.model.getAliasColObjMap();
const sorts = extractSortsObject(args?.sort, aliasColObjMap);
const sorts = extractSortsObject(rest?.sort, aliasColObjMap);
const filterObj = extractFilterFromXwhere(args?.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(
[
new Filter({
@ -354,12 +362,13 @@ class BaseModelSqlv2 {
qb.groupBy(args.column_name);
if (sorts) await sortV2(sorts, qb, this.dbDriver);
applyPaginate(qb, rest);
return await qb;
let data = await qb;
return data;
}
async multipleHmList({ colId, ids }, args?: { limit?; offset? }) {
async multipleHmList({ colId, ids }, args: { limit?; offset? } = {}) {
try {
const { where, ...rest } = this._getListArgs(args as any);
// todo: get only required fields
// const { cn } = this.hasManyRelations.find(({ tn }) => tn === child) || {};
@ -399,8 +408,8 @@ class BaseModelSqlv2 {
.where(_wherePk(parentTable.primaryKeys, p))
);
// todo: sanitize
query.limit(args?.limit || 20);
query.offset(args?.offset || 0);
query.limit(+rest?.limit || 20);
query.offset(+rest?.offset || 0);
return this.isSqlite ? this.dbDriver.select().from(query) : query;
}),
@ -471,8 +480,9 @@ class BaseModelSqlv2 {
}
}
async hmList({ colId, id }, args?: { limit?; offset? }) {
async hmList({ colId, id }, args: { limit?; offset? } = {}) {
try {
const { where, ...rest } = this._getListArgs(args as any);
// todo: get only required fields
const relColumn = (await this.model.getColumns()).find(
@ -503,8 +513,8 @@ class BaseModelSqlv2 {
.where(_wherePk(parentTable.primaryKeys, id))
);
// todo: sanitize
qb.limit(args?.limit || 20);
qb.offset(args?.offset || 0);
qb.limit(+rest?.limit || 20);
qb.offset(+rest?.offset || 0);
await childModel.selectObject({ qb });
@ -561,7 +571,8 @@ class BaseModelSqlv2 {
}
}
public async multipleMmList({ colId, parentIds }, args?: { limit; offset }) {
public async multipleMmList({ colId, parentIds }, args: { limit?; offset? } = {}) {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -602,8 +613,8 @@ class BaseModelSqlv2 {
.select(this.dbDriver.raw('? as ??', [id, GROUP_COL]));
// todo: sanitize
query.limit(args?.limit || 20);
query.offset(args?.offset || 0);
query.limit(+rest?.limit || 20);
query.offset(+rest?.offset || 0);
return this.isSqlite ? this.dbDriver.select().from(query) : query;
}),
@ -630,7 +641,8 @@ class BaseModelSqlv2 {
return parentIds.map((id) => gs[id] || []);
}
public async mmList({ colId, parentId }, args?: { limit; offset }) {
public async mmList({ colId, parentId }, args: { limit?; offset? } = {}) {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -666,8 +678,8 @@ class BaseModelSqlv2 {
await childModel.selectObject({ qb });
// todo: sanitize
qb.limit(args?.limit || 20);
qb.offset(args?.offset || 0);
qb.limit(+rest?.limit || 20);
qb.offset(+rest?.offset || 0);
const children = await this.extractRawQueryAndExec(qb);
const proto = await (
@ -769,6 +781,7 @@ class BaseModelSqlv2 {
{ colId, pid = null },
args
): Promise<any> {
const { where } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -803,7 +816,7 @@ class BaseModelSqlv2 {
});
const aliasColObjMap = await childTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
return (await qb.first())?.count;
@ -814,6 +827,7 @@ class BaseModelSqlv2 {
{ colId, pid = null },
args
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -852,17 +866,22 @@ class BaseModelSqlv2 {
.orWhereNull(rcn)
);
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
await childModel.selectObject({ qb });
const aliasColObjMap = await childTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
applyPaginate(qb, args);
applyPaginate(qb, rest);
const proto = await childModel.getProto();
let data = await qb;
return (await qb).map((c) => {
return data.map((c) => {
c.__proto__ = proto;
return c;
});
@ -873,6 +892,7 @@ class BaseModelSqlv2 {
{ colId, pid = null },
args
): Promise<any> {
const { where } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -902,7 +922,7 @@ class BaseModelSqlv2 {
});
const aliasColObjMap = await childTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
@ -914,6 +934,7 @@ class BaseModelSqlv2 {
{ colId, pid = null },
args
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -945,17 +966,22 @@ class BaseModelSqlv2 {
).orWhereNull(cn);
});
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
await childModel.selectObject({ qb });
const aliasColObjMap = await childTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
applyPaginate(qb, args);
applyPaginate(qb, rest);
const proto = await childModel.getProto();
let data = await this.extractRawQueryAndExec(qb);
return (await this.extractRawQueryAndExec(qb)).map((c) => {
return data.map((c) => {
c.__proto__ = proto;
return c;
});
@ -966,6 +992,7 @@ class BaseModelSqlv2 {
{ colId, cid = null },
args
): Promise<any> {
const { where } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -996,7 +1023,7 @@ class BaseModelSqlv2 {
.count(`*`, { as: 'count' });
const aliasColObjMap = await parentTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
return (await qb.first())?.count;
@ -1007,6 +1034,7 @@ class BaseModelSqlv2 {
{ colId, cid = null },
args
): Promise<any> {
const { where, ...rest } = this._getListArgs(args as any);
const relColumn = (await this.model.getColumns()).find(
(c) => c.id === colId
);
@ -1038,16 +1066,22 @@ class BaseModelSqlv2 {
).orWhereNull(rcn);
});
if (+rest?.shuffle) {
await this.shuffle({ qb });
}
await parentModel.selectObject({ qb });
const aliasColObjMap = await parentTable.getAliasColObjMap();
const filterObj = extractFilterFromXwhere(args.where, aliasColObjMap);
const filterObj = extractFilterFromXwhere(where, aliasColObjMap);
await conditionV2(filterObj, qb, this.dbDriver);
applyPaginate(qb, args);
applyPaginate(qb, rest);
const proto = await parentModel.getProto();
return (await this.extractRawQueryAndExec(qb)).map((c) => {
let data = await this.extractRawQueryAndExec(qb);
return data.map((c) => {
c.__proto__ = proto;
return c;
});
@ -1227,6 +1261,7 @@ class BaseModelSqlv2 {
const obj: XcFilter = {};
obj.where = args.where || args.w || '';
obj.having = args.having || args.h || '';
obj.shuffle = args.shuffle || args.r || '';
obj.condition = args.condition || args.c || {};
obj.conditionGraph = args.conditionGraph || {};
obj.limit = Math.max(
@ -1242,6 +1277,16 @@ class BaseModelSqlv2 {
return obj;
}
public async shuffle({ qb }: { qb: QueryBuilder }): Promise<void> {
if (this.isMySQL) {
qb.orderByRaw('RAND()');
} else if (this.isPg || this.isSqlite) {
qb.orderByRaw('RANDOM()');
} else if (this.isMssql) {
qb.orderByRaw('NEWID()');
}
}
public async selectObject({ qb }: { qb: QueryBuilder }): Promise<void> {
const res = {};
const columns = await this.model.getColumns();
@ -1380,6 +1425,48 @@ class BaseModelSqlv2 {
}
}
async hasLTARData(rowId, model: Model): Promise<any> {
const res = [];
const LTARColumns = (await model.getColumns()).filter(
(c) => c.uidt === UITypes.LinkToAnotherRecord
);
let i = 0;
for (const column of LTARColumns) {
const colOptions =
(await column.getColOptions()) as LinkToAnotherRecordColumn;
const childColumn = await colOptions.getChildColumn();
const parentColumn = await colOptions.getParentColumn();
const childModel = await childColumn.getModel();
await childModel.getColumns();
const parentModel = await parentColumn.getModel();
await parentModel.getColumns();
let cnt = 0;
if (colOptions.type === RelationTypes.HAS_MANY) {
cnt = +(
await this.dbDriver(childModel.table_name)
.count(childColumn.column_name, { as: 'cnt' })
.where(childColumn.column_name, rowId)
)[0].cnt;
} else if (colOptions.type === RelationTypes.MANY_TO_MANY) {
const mmModel = await colOptions.getMMModel();
const mmChildColumn = await colOptions.getMMChildColumn();
cnt = +(
await this.dbDriver(mmModel.table_name)
.where(`${mmModel.table_name}.${mmChildColumn.column_name}`, rowId)
.count(mmChildColumn.column_name, { as: 'cnt' })
)[0].cnt;
}
if (cnt) {
res.push(
`${i++ + 1}. ${model.title}.${
column.title
} is a LinkToAnotherRecord of ${childModel.title}`
);
}
}
return res;
}
async updateByPk(id, data, trx?, cookie?) {
try {
const updateObj = await this.model.mapAliasToColumn(data);
@ -2217,7 +2304,6 @@ function applyPaginate(
) {
query.offset(offset);
if (!ignoreLimit) query.limit(limit);
return query;
}

5
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts

@ -261,16 +261,17 @@ const parseConditionV2 = async (
filter.comparison_op === 'notempty'
)
filter.value = '';
let field = sanitize(
const _field = sanitize(
customWhereClause
? filter.value
: alias
? `${alias}.${column.column_name}`
: column.column_name
);
let val = customWhereClause ? customWhereClause : filter.value;
const _val = customWhereClause ? customWhereClause : filter.value;
return (qb) => {
let [field, val] = [_field, _val];
switch (filter.comparison_op) {
case 'eq':
qb = qb.where(field, val);

6
packages/nocodb/src/lib/meta/api/dataApis/dataAliasApis.ts

@ -81,7 +81,11 @@ async function dataDelete(req: Request, res: Response) {
viewId: view?.id,
dbDriver: NcConnectionMgrv2.get(base),
});
const message = await baseModel.hasLTARData(req.params.rowId, model);
if (message.length) {
res.json({ message });
return;
}
res.json(await baseModel.delByPk(req.params.rowId, null, req));
}
async function getDataList(model, view: View, req) {

1
packages/nocodb/src/lib/meta/api/filterApis.ts

@ -49,7 +49,6 @@ export async function filterChildrenRead(
) {
try {
const filter = await Filter.parentFilterList({
viewId: req.params.viewId,
parentId: req.params.filterParentId,
});

13
packages/nocodb/src/lib/meta/api/swagger/helpers/templates/params.ts

@ -74,6 +74,19 @@ export const offsetParam = {
example: 0,
};
export const shuffleParam = {
schema: {
type: 'number',
minimum: 0,
maximum: 1,
},
in: 'query',
name: 'shuffle',
description:
'The `shuffle` parameter used for pagination, the response will be shuffled if it is set to 1.',
example: 0,
};
export const columnNameQueryParam = {
schema: {
type: 'string',

7
packages/nocodb/src/lib/meta/api/swagger/helpers/templates/paths.ts

@ -8,6 +8,7 @@ import {
getNestedParams,
limitParam,
offsetParam,
shuffleParam,
referencedRowIdParam,
relationTypeParam,
rowIdParam,
@ -35,6 +36,7 @@ export const getModelPaths = async (ctx: {
sortParam,
whereParam,
limitParam,
shuffleParam,
offsetParam,
...(await getNestedParams(ctx.columns)),
],
@ -200,6 +202,7 @@ export const getModelPaths = async (ctx: {
whereParam,
limitParam,
offsetParam,
shuffleParam,
],
responses: {
'200': {
@ -404,7 +407,7 @@ export const getModelPaths = async (ctx: {
},
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam],
parameters: [limitParam, shuffleParam, offsetParam],
description: '',
},
delete: {
@ -445,7 +448,7 @@ export const getModelPaths = async (ctx: {
},
},
tags: [ctx.tableName],
parameters: [limitParam, offsetParam],
parameters: [limitParam, shuffleParam, offsetParam],
},
},
}

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

@ -141,6 +141,20 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.clientType;
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (req.body.table_name.length > tableNameLengthLimit) {
NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`);
}
req.body.columns = req.body.columns?.map((c) => ({
...getColumnPropsFromUIDT(c as any, base),
cn: c.column_name,

13
packages/nocodb/src/lib/meta/api/userApi/userApis.ts

@ -328,10 +328,9 @@ async function passwordForgot(req: Request<any, any>, res): Promise<any> {
subject: 'Password Reset Link',
text: `Visit following link to update your password : ${
(req as any).ncSiteUrl
}/api/v1/auth/password/reset/${token}.`,
}/auth/password/reset/${token}.`,
html: ejs.render(template, {
resetLink:
(req as any).ncSiteUrl + `/api/v1/auth/password/reset/${token}`,
resetLink: (req as any).ncSiteUrl + `/auth/password/reset/${token}`,
}),
})
);
@ -365,7 +364,7 @@ async function tokenValidate(req, res): Promise<any> {
if (!user || !user.email) {
NcError.badRequest('Invalid reset url');
}
if (user.reset_password_expires < new Date()) {
if (new Date(user.reset_password_expires) < new Date()) {
NcError.badRequest('Password reset url expired');
}
res.json(true);
@ -577,9 +576,7 @@ const mapRoutes = (router) => {
'/api/v1/auth/token/refresh',
ncMetaAclMw(refreshToken, 'refreshToken')
);
router.get(
'/api/v1/auth/password/reset/:tokenId',
catchError(renderPasswordReset)
);
// respond with password reset page
router.get('/auth/password/reset/:tokenId', catchError(renderPasswordReset));
};
export { mapRoutes as userApis };

17
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -45,11 +45,12 @@ export async function appInfo(req: Request, res: Response) {
res.json(result);
}
export async function releaseVersion(_req: Request, res: Response) {
export async function versionInfo(_req: Request, res: Response) {
const result = await axios
.get('https://github.com/nocodb/nocodb/releases/latest')
.then((response) => {
return {
currentVersion: packageVersion,
releaseVersion: response.request.res.responseUrl.replace(
'https://github.com/nocodb/nocodb/releases/tag/',
''
@ -60,6 +61,17 @@ export async function releaseVersion(_req: Request, res: Response) {
res.json(result);
}
export async function feedbackFormGet(_req: Request, res: Response) {
axios
.get('https://nocodb.com/api/v1/feedback_form')
.then((response) => {
res.json(response.data);
})
.catch((e) => {
res.status(500).json({ error: e.message });
});
}
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
@ -142,6 +154,7 @@ export default (router) => {
);
router.get('/api/v1/db/meta/nocodb/info', catchError(appInfo));
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(releaseVersion));
router.get('/api/v1/version', catchError(versionInfo));
router.get('/api/v1/health', catchError(appHealth));
router.get('/api/v1/feedback_form', catchError(feedbackFormGet));
};

8
packages/nocodb/src/lib/meta/api/viewApis.ts

@ -69,9 +69,9 @@ export async function viewDelete(req: Request, res: Response, next) {
res.json(result);
}
async function shareViewPasswordUpdate(req: Request<any, any>, res) {
Tele.emit('evt', { evt_type: 'sharedView:password-updated' });
res.json(await View.passwordUpdate(req.params.viewId, req.body));
async function shareViewUpdate(req: Request<any, any>, res) {
Tele.emit('evt', { evt_type: 'sharedView:updated' });
res.json(await View.update(req.params.viewId, req.body));
}
async function shareViewDelete(req: Request<any, any>, res) {
@ -140,7 +140,7 @@ router.post(
router.patch(
'/api/v1/db/meta/views/:viewId/share',
metaApiMetrics,
ncMetaAclMw(shareViewPasswordUpdate, 'shareViewPasswordUpdate')
ncMetaAclMw(shareViewUpdate, 'shareViewUpdate')
);
router.delete(
'/api/v1/db/meta/views/:viewId/share',

20
packages/nocodb/src/lib/meta/helpers/PagedResponse.ts

@ -1,14 +1,24 @@
import { PaginatedType } from 'nocodb-sdk';
const config: any = {
limitDefault: Math.max(+process.env.DB_QUERY_LIMIT_DEFAULT || 25, 1),
limitMin: Math.max(+process.env.DB_QUERY_LIMIT_MIN || 1, 1),
limitMax: Math.max(+process.env.DB_QUERY_LIMIT_MAX || 1000, 1),
};
export class PagedResponseImpl<T> {
constructor(
list: T[],
{
limit = 25,
offset = 0,
count = null,
}: { limit?: number; offset?: number; count?: number } = {}
args: { limit?: number; offset?: number; count?: number, l?: number, o?: number } = {}
) {
const limit = Math.max(
Math.min(
args.limit || args.l || config.limitDefault,
config.limitMax
),
config.limitMin
);
const offset = Math.max(+(args.offset || args.o) || 0, 0);
const count = args.count ?? null;
this.list = list;
if (count !== null) {
this.pageInfo = { totalRows: +count };

4
packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts

@ -5,6 +5,7 @@ import * as nc_014_alter_column_data_types from './v2/nc_014_alter_column_data_t
import * as nc_015_add_meta_col_in_column_table from './v2/nc_015_add_meta_col_in_column_table';
import * as nc_016_alter_hooklog_payload_types from './v2/nc_016_alter_hooklog_payload_types';
import * as nc_017_add_user_token_version_column from './v2/nc_017_add_user_token_version_column';
import * as nc_018_add_meta_in_view from './v2/nc_018_add_meta_in_view';
// Create a custom migration source class
export default class XcMigrationSourcev2 {
@ -21,6 +22,7 @@ export default class XcMigrationSourcev2 {
'nc_015_add_meta_col_in_column_table',
'nc_016_alter_hooklog_payload_types',
'nc_017_add_user_token_version_column',
'nc_018_add_meta_in_view',
]);
}
@ -44,6 +46,8 @@ export default class XcMigrationSourcev2 {
return nc_016_alter_hooklog_payload_types;
case 'nc_017_add_user_token_version_column':
return nc_017_add_user_token_version_column;
case 'nc_018_add_meta_in_view':
return nc_018_add_meta_in_view;
}
}
}

38
packages/nocodb/src/lib/migrations/v2/nc_018_add_meta_in_view.ts

@ -0,0 +1,38 @@
import Knex from 'knex';
import { MetaTable } from '../../utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.VIEWS, (table) => {
table.text('meta');
});
};
const down = async (knex) => {
await knex.schema.alterTable(MetaTable.VIEWS, (table) => {
table.dropColumns('meta');
});
};
export { up, down };
/**
* @copyright Copyright (c) 2021, Xgene Cloud Ltd
*
* @author Wing-Kam Wong <wingkwong.code@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

31
packages/nocodb/src/lib/models/Filter.ts

@ -104,7 +104,15 @@ export default class Filter {
if (filter?.children?.length) {
await Promise.all(
filter.children.map((f) =>
this.insert({ ...f, fk_parent_id: row.id }, ncMeta)
this.insert(
{
...f,
fk_parent_id: row.id,
[filter.fk_hook_id ? 'fk_hook_id' : 'fk_view_id']:
filter.fk_hook_id ? filter.fk_hook_id : filter.fk_view_id,
},
ncMeta
)
)
);
}
@ -373,6 +381,7 @@ export default class Filter {
};
await deleteRecursively(filter);
}
static async deleteAllByHook(hookId: string, ncMeta = Noco.ncMeta) {
const filter = await this.getFilterObject({ hookId }, ncMeta);
@ -421,7 +430,9 @@ export default class Filter {
});
await NocoCache.setList(CacheScope.FILTER_EXP, [viewId], filterObjs);
}
return filterObjs?.map((f) => new Filter(f));
return filterObjs
?.filter((f) => !f.fk_parent_id)
?.map((f) => new Filter(f));
}
static async rootFilterListByHook(
@ -443,36 +454,28 @@ export default class Filter {
static async parentFilterList(
{
viewId,
parentId,
}: {
viewId: any;
parentId: any;
},
ncMeta = Noco.ncMeta
) {
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [
viewId,
parentId,
]);
let filterObjs = await NocoCache.getList(CacheScope.FILTER_EXP, [parentId]);
if (!filterObjs.length) {
filterObjs = await ncMeta.metaList2(null, null, MetaTable.FILTER_EXP, {
condition: {
fk_parent_id: parentId,
fk_view_id: viewId,
// fk_view_id: viewId,
},
orderBy: {
order: 'asc',
},
});
await NocoCache.setList(
CacheScope.FILTER_EXP,
[viewId, parentId],
filterObjs
);
await NocoCache.setList(CacheScope.FILTER_EXP, [parentId], filterObjs);
}
return filterObjs?.map((f) => new Filter(f));
}
static async parentFilterListByHook(
{
hookId,

35
packages/nocodb/src/lib/models/View.ts

@ -42,6 +42,7 @@ export default class View implements ViewType {
project_id?: string;
base_id?: string;
show_system_fields?: boolean;
meta?: any;
constructor(data: View) {
Object.assign(this, data);
@ -614,7 +615,31 @@ export default class View implements ViewType {
viewId
);
}
if (!view.meta) {
const defaultMeta = {
allowCSVDownload: true,
};
// get existing cache
const key = `${CacheScope.VIEW}:${view.id}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
// update data
o.meta = JSON.stringify(defaultMeta);
// set cache
await NocoCache.set(key, o);
}
// set meta
await ncMeta.metaUpdate(
null,
null,
MetaTable.VIEWS,
{
meta: JSON.stringify(defaultMeta),
},
viewId
);
view.meta = defaultMeta;
}
return view;
}
@ -675,6 +700,7 @@ export default class View implements ViewType {
lock_type?: string;
password?: string;
uuid?: string;
meta?: any;
},
ncMeta = Noco.ncMeta
) {
@ -684,14 +710,19 @@ export default class View implements ViewType {
'show_system_fields',
'lock_type',
'password',
'meta',
'uuid',
]);
updateObj.meta = JSON.stringify(updateObj.meta);
// get existing cache
const key = `${CacheScope.VIEW}:${viewId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
if (o) {
// update data
o = { ...o, ...updateObj };
o = {
...o,
...updateObj,
};
if (o.is_default) {
await NocoCache.set(`${CacheScope.VIEW}:${o.fk_model_id}:default`, o);
}

4
scripts/cypress/integration/common/1c_sql_view.js

@ -54,7 +54,7 @@ export const genTest = (apiType, dbType) => {
mainPage.unhideField(`ZipCode`);
// Column operations: Sort
mainPage.sortField("Name", "Z -> A");
mainPage.sortField("Name", "Z A");
mainPage
.getCell(`Name`, 1)
.contains("ZACHARY HITE")
@ -114,7 +114,7 @@ export const genTest = (apiType, dbType) => {
mainPage.unhideField("FilmInfo");
// Column operations: Sort
mainPage.sortField("FirstName", "Z -> A");
mainPage.sortField("FirstName", "Z A");
mainPage
.getCell(`FirstName`, 1)
.contains("ZERO")

2
scripts/cypress/integration/common/1e_meta_sync.js

@ -148,7 +148,7 @@ export const genTest = (apiType, dbType) => {
cy.openTableTab("Table1", 9);
mainPage.hideField("Col1");
mainPage.sortField("Col1", "Z -> A");
mainPage.sortField("Col1", "9 → 1");
mainPage.filterField(`Col1`, ">=", "5");
cy.get(".nc-grid-row").should("have.length", 5);
cy.closeTableTab("Table1");

2
scripts/cypress/integration/common/1e_pg_meta_sync.js

@ -156,7 +156,7 @@ export const genTest = (apiType, dbType) => {
cy.openTableTab("Table1", 9);
mainPage.hideField("Col1");
mainPage.sortField("Col1", "Z -> A");
mainPage.sortField("Col1", "9 → 1");
mainPage.filterField(`Col1`, ">=", "5");
cy.get(".nc-grid-row").should("have.length", 5);
cy.closeTableTab("Table1");

4
scripts/cypress/integration/common/3a_filter_sort_fields_operations.js

@ -140,7 +140,7 @@ export const genTest = (apiType, dbType) => {
describe(`Sort operations`, () => {
it("Enable sort", () => {
mainPage.sortField("Country", "Z -> A");
mainPage.sortField("Country", "Z A");
// Sort menu operations (Country Column, Z->A)
// cy.get(".nc-sort-menu-btn").click();
@ -151,7 +151,7 @@ export const genTest = (apiType, dbType) => {
// ).click();
// cy.get(".nc-sort-dir-select div").first().click();
// cy.get(
// '.menuable__content__active .v-list-item:contains("Z -> A")'
// '.menuable__content__active .v-list-item:contains("Z A")'
// ).click();
cy.contains("Zambia").should("exist");

25
scripts/cypress/integration/common/4b_table_view_share.js

@ -5,25 +5,23 @@ let storedURL = "";
let linkText = "";
const generateLinkWithPwd = () => {
// cy.get(".v-navigation-drawer__content > .container")
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
mainPage.shareView().click({ force: true });
cy.wait(3000);
cy.snipActiveModal("Modal_ShareView");
// enable checkbox & feed pwd, save
cy.getActiveModal()
.find('[role="switch"][type="checkbox"]')
.click({ force: true });
cy.getActiveModal().find('button:contains("More Options")').click({ force: true });
cy.getActiveModal().find('[role="checkbox"][type="checkbox"]').first().then(($el) => {
if (!$el.prop("checked")) {
cy.wrap($el).click({ force: true });
cy.getActiveModal().find('input[type="password"]').type("1");
cy.snipActiveModal("Modal_ShareView_Password");
cy.getActiveModal().find('button:contains("Save password")').click();
cy.toastWait("Successfully updated");
}
});
// copy link text, visit URL
cy.getActiveModal()
@ -93,6 +91,11 @@ export const genTest = (apiType, dbType) => {
cy.get("body")
.find(".v-dialog.v-dialog--active")
.should("not.exist");
// Verify Download as CSV is here
cy.get(".nc-actions-menu-btn").click();
cy.snipActiveMenu("Menu_ActionsMenu");
cy.get(`.menuable__content__active .v-list-item span:contains("Download as CSV")`).should("exist");
});
it("Delete view", () => {

4
scripts/cypress/integration/common/4e_form_view_share.js

@ -97,7 +97,9 @@ export const genTest = (apiType, dbType) => {
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
mainPage.shareView().click({ force: true });
cy.wait(5000);
cy.snipActiveModal("Modal_ShareView");

10
scripts/cypress/integration/common/4f_grid_view_share.js

@ -20,7 +20,9 @@ export const genTest = (apiType, dbType) => {
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined
cy.getActiveModal()
@ -86,7 +88,7 @@ export const genTest = (apiType, dbType) => {
.contains("Address1")
.click();
mainPage.hideField("Address2");
mainPage.sortField("District", "Z -> A");
mainPage.sortField("District", "Z A");
mainPage.filterField("Address", "is like", "Ab");
generateViewLink("combined");
cy.log(viewURL["combined"]);
@ -207,8 +209,8 @@ export const genTest = (apiType, dbType) => {
});
it(`Share ${viewType.toUpperCase()} view : Enable sort`, () => {
// Sort menu operations (Country Column, Z->A)
mainPage.sortField("District", "Z -> A");
// Sort menu operations (Country Column, ZA)
mainPage.sortField("District", "Z A");
mainPage
.getCell("District", 1)
.contains("West Bengali")

8
scripts/cypress/integration/common/4f_pg_grid_view_share.js

@ -20,7 +20,9 @@ export const genTest = (apiType, dbType) => {
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined
cy.getActiveModal()
@ -86,7 +88,7 @@ export const genTest = (apiType, dbType) => {
.contains("Address1")
.click();
mainPage.hideField("Address2");
mainPage.sortField("Address", "Z -> A");
mainPage.sortField("Address", "Z A");
mainPage.filterField("Address", "is like", "Ab");
generateViewLink("combined");
cy.log(viewURL["combined"]);
@ -205,7 +207,7 @@ export const genTest = (apiType, dbType) => {
it(`Share ${viewType.toUpperCase()} view : Enable sort`, () => {
// Sort menu operations (Country Column, Z->A)
mainPage.sortField("Address", "Z -> A");
mainPage.sortField("Address", "Z A");
mainPage
.getCell("Address", 1)
.contains("669 Firozabad Loop")

4
scripts/cypress/integration/common/6f_attachments.js

@ -66,7 +66,9 @@ export const genTest = (apiType, dbType) => {
// .find(".v-list > .v-list-item")
// .contains("Share View")
// .click();
mainPage.shareView().click();
mainPage.shareView().click({ force: true });
cy.wait(5000);
// copy link text, visit URL
cy.getActiveModal()

2
scripts/cypress/integration/common/9a_QuickTest.js

@ -330,7 +330,7 @@ export const genTest = (apiType, dbType, testMode) => {
.contains('Name')
.should("exist");
cy.get(".nc-sort-dir-select").eq(0)
.contains('A -> Z')
.contains('A Z')
.should("exist");
cy.get(".nc-sort-menu-btn").click();

1
scripts/cypress/support/page_objects/mainPage.js

@ -245,6 +245,7 @@ export class _mainPage {
};
shareView = () => {
cy.wait(3000);
return cy.get(".nc-btn-share-view");
};

Loading…
Cancel
Save