Browse Source

Merge pull request #2552 from nocodb/enhancement/disable-download-csv-on-shared-view

enhancement: allow disabling download csv on a shared view
pull/2682/head
mertmit 2 years ago committed by GitHub
parent
commit
4f46a688af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      packages/nc-gui/components/project/spreadsheet/components/SharedViewsList.vue
  2. 102
      packages/nc-gui/components/project/spreadsheet/components/SpreadsheetNavDrawer.vue
  3. 9
      packages/nc-gui/components/project/spreadsheet/public/XcTable.vue
  4. 8
      packages/nocodb/src/lib/meta/api/viewApis.ts
  5. 4
      packages/nocodb/src/lib/migrations/XcMigrationSourcev2.ts
  6. 38
      packages/nocodb/src/lib/migrations/v2/nc_018_add_meta_in_view.ts
  7. 35
      packages/nocodb/src/lib/models/View.ts
  8. 25
      scripts/cypress/integration/common/4b_table_view_share.js
  9. 4
      scripts/cypress/integration/common/4e_form_view_share.js
  10. 4
      scripts/cypress/integration/common/4f_grid_view_share.js
  11. 4
      scripts/cypress/integration/common/4f_pg_grid_view_share.js
  12. 4
      scripts/cypress/integration/common/6f_attachments.js
  13. 1
      scripts/cypress/support/page_objects/mainPage.js

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

@ -18,6 +18,10 @@
<!--Password--> <!--Password-->
{{ $t('labels.password') }} {{ $t('labels.password') }}
</th> </th>
<th class="caption grey--text">
<!-- TODO: i18n -->
Download Allowed
</th>
<th class="caption grey--text"> <th class="caption grey--text">
<!--Actions--> <!--Actions-->
{{ $t('labels.actions') }} {{ $t('labels.actions') }}
@ -46,6 +50,11 @@
</v-icon> </v-icon>
</template> </template>
</td> </td>
<td class="caption text-center">
<template v-if="'meta' in currentView">
<span>{{ renderAllowCSVDownload(currentView) }}</span>
</template>
</td>
<td class="caption"> <td class="caption">
<v-icon small @click="copyLink(currentView)"> mdi-content-copy </v-icon> <v-icon small @click="copyLink(currentView)"> mdi-content-copy </v-icon>
<v-icon small @click="deleteLink(currentView.id)"> mdi-delete-outline </v-icon> <v-icon small @click="deleteLink(currentView.id)"> mdi-delete-outline </v-icon>
@ -80,6 +89,11 @@
</v-icon> </v-icon>
</template> </template>
</td> </td>
<td class="caption text-center">
<template v-if="'meta' in link">
<span>{{ renderAllowCSVDownload(link) }}</span>
</template>
</td>
<td class="caption"> <td class="caption">
<v-icon small @click="copyLink(link)"> mdi-content-copy </v-icon> <v-icon small @click="copyLink(link)"> mdi-content-copy </v-icon>
<v-icon small @click="deleteLink(link.id)"> mdi-delete-outline </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}`; 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> </script>

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> <v-icon small class="pointer" @click="copyShareUrlToClipboard"> mdi-content-copy </v-icon>
</div> </div>
<v-switch v-model="passwordProtect" dense @change="onPasswordProtectChange"> <v-expansion-panels v-model="advanceOptionsPanel" class="mx-auto" flat>
<template #label> <v-expansion-panel>
<!-- Restrict access with a password --> <v-expansion-panel-header hide-actions>
<span v-show="!passwordProtect" class="caption"> <v-spacer />
{{ $t('msg.info.beforeEnablePwd') }} <span class="grey--text caption"
</span> >More Options
<!-- Access is password restricted --> <v-icon color="grey" small>
<span v-show="passwordProtect" class="caption"> mdi-chevron-{{ advanceOptionsPanel === 0 ? 'up' : 'down' }}
{{ $t('msg.info.afterEnablePwd') }} </v-icon></span
</span> >
</template> </v-expansion-panel-header>
</v-switch> <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"> <div v-if="passwordProtect" class="d-flex flex-column align-center justify-center">
<v-text-field <v-text-field
v-model="shareLink.password" v-model="shareLink.password"
@ -354,6 +361,18 @@
{{ $t('placeholder.password.save') }} {{ $t('placeholder.password.save') }}
</v-btn> </v-btn>
</div> </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-container>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -410,6 +429,7 @@ export default {
queryParams: Object, queryParams: Object,
}, },
data: () => ({ data: () => ({
advanceOptionsPanel: false,
webhookSliderModal: false, webhookSliderModal: false,
codeSnippetModal: false, codeSnippetModal: false,
drag: false, drag: false,
@ -425,6 +445,7 @@ export default {
searchQueryVal: '', searchQueryVal: '',
showShareLinkPassword: false, showShareLinkPassword: false,
passwordProtect: false, passwordProtect: false,
allowCSVDownload: true,
sharedViewPassword: '', sharedViewPassword: '',
overAdvShieldIcon: false, overAdvShieldIcon: false,
overShieldIcon: false, overShieldIcon: false,
@ -611,6 +632,9 @@ export default {
this.saveShareLinkPassword(); this.saveShareLinkPassword();
} }
}, },
onAllowCSVDownloadChange() {
this.saveAllowCSVDownload();
},
async saveShareLinkPassword() { async saveShareLinkPassword() {
try { try {
await this.$api.dbViewShare.update(this.shareLink.id, { await this.$api.dbViewShare.update(this.shareLink.id, {
@ -632,6 +656,27 @@ export default {
this.$e('a:view:share:enable-pwd'); 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() { async loadViews() {
// this.viewsList = await this.sqlOp( // this.viewsList = await this.sqlOp(
// { // {
@ -725,34 +770,12 @@ export default {
this.$e('a:view:delete', { view: view.type }); this.$e('a:view:delete', { view: view.type });
}, },
async genShareLink() { 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); 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 // todo: url
this.shareLink = shared; this.shareLink = shared;
this.passwordProtect = shared.password !== null;
this.allowCSVDownload = shared.meta.allowCSVDownload;
this.showShareModel = true; this.showShareModel = true;
}, },
copyView(view, i) { copyView(view, i) {
@ -903,4 +926,7 @@ export default {
opacity: 0.5; opacity: 0.5;
background: grey; background: grey;
} }
.mx-auto .v-expansion-panel {
background: var(--v-backgroundColor-base);
}
</style> </style>

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

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

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

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_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_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_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 // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -21,6 +22,7 @@ export default class XcMigrationSourcev2 {
'nc_015_add_meta_col_in_column_table', 'nc_015_add_meta_col_in_column_table',
'nc_016_alter_hooklog_payload_types', 'nc_016_alter_hooklog_payload_types',
'nc_017_add_user_token_version_column', '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; return nc_016_alter_hooklog_payload_types;
case 'nc_017_add_user_token_version_column': case 'nc_017_add_user_token_version_column':
return 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/>.
*
*/

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

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

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

@ -5,25 +5,23 @@ let storedURL = "";
let linkText = ""; let linkText = "";
const generateLinkWithPwd = () => { const generateLinkWithPwd = () => {
// cy.get(".v-navigation-drawer__content > .container") mainPage.shareView().click({ force: true });
// .find(".v-list > .v-list-item")
// .contains("Share View") cy.wait(3000);
// .click();
mainPage.shareView().click();
cy.snipActiveModal("Modal_ShareView"); cy.snipActiveModal("Modal_ShareView");
// enable checkbox & feed pwd, save // enable checkbox & feed pwd, save
cy.getActiveModal() cy.getActiveModal().find('button:contains("More Options")').click({ force: true });
.find('[role="switch"][type="checkbox"]') cy.getActiveModal().find('[role="checkbox"][type="checkbox"]').first().then(($el) => {
.click({ force: true }); if (!$el.prop("checked")) {
cy.wrap($el).click({ force: true });
cy.getActiveModal().find('input[type="password"]').type("1"); cy.getActiveModal().find('input[type="password"]').type("1");
cy.snipActiveModal("Modal_ShareView_Password"); cy.snipActiveModal("Modal_ShareView_Password");
cy.getActiveModal().find('button:contains("Save password")').click(); cy.getActiveModal().find('button:contains("Save password")').click();
cy.toastWait("Successfully updated"); cy.toastWait("Successfully updated");
}
});
// copy link text, visit URL // copy link text, visit URL
cy.getActiveModal() cy.getActiveModal()
@ -93,6 +91,11 @@ export const genTest = (apiType, dbType) => {
cy.get("body") cy.get("body")
.find(".v-dialog.v-dialog--active") .find(".v-dialog.v-dialog--active")
.should("not.exist"); .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", () => { 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") // .find(".v-list > .v-list-item")
// .contains("Share View") // .contains("Share View")
// .click(); // .click();
mainPage.shareView().click(); mainPage.shareView().click({ force: true });
cy.wait(5000);
cy.snipActiveModal("Modal_ShareView"); cy.snipActiveModal("Modal_ShareView");

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

@ -20,7 +20,9 @@ export const genTest = (apiType, dbType) => {
// .find(".v-list > .v-list-item") // .find(".v-list > .v-list-item")
// .contains("Share View") // .contains("Share View")
// .click(); // .click();
mainPage.shareView().click(); mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined // wait, as URL initially will be /undefined
cy.getActiveModal() cy.getActiveModal()

4
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") // .find(".v-list > .v-list-item")
// .contains("Share View") // .contains("Share View")
// .click(); // .click();
mainPage.shareView().click(); mainPage.shareView().click({ force: true });
cy.wait(5000);
// wait, as URL initially will be /undefined // wait, as URL initially will be /undefined
cy.getActiveModal() cy.getActiveModal()

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

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

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

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

Loading…
Cancel
Save