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. 152
      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. 33
      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-->
{{ $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>

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

@ -316,44 +316,63 @@
<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>
<div v-if="passwordProtect" class="d-flex flex-column align-center justify-center">
<v-text-field
v-model="shareLink.password"
autocomplete="new-password"
browser-autocomplete="new-password"
class="password-field mr-2 caption"
style="max-width: 230px"
:type="showShareLinkPassword ? 'text' : 'password'"
:hint="$t('placeholder.password.enter')"
persistent-hint
dense
solo
flat
>
<template #append>
<v-icon small @click="showShareLinkPassword = !showShareLinkPassword">
{{ showShareLinkPassword ? 'visibility_off' : 'visibility' }}
</v-icon>
</template>
</v-text-field>
<v-btn color="primary" class="caption" small @click="saveShareLinkPassword">
<!-- Save password -->
{{ $t('placeholder.password.save') }}
</v-btn>
</div>
<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"
autocomplete="new-password"
browser-autocomplete="new-password"
class="password-field mr-2 caption"
style="max-width: 230px"
:type="showShareLinkPassword ? 'text' : 'password'"
:hint="$t('placeholder.password.enter')"
persistent-hint
dense
solo
flat
>
<template #append>
<v-icon small @click="showShareLinkPassword = !showShareLinkPassword">
{{ showShareLinkPassword ? 'visibility_off' : 'visibility' }}
</v-icon>
</template>
</v-text-field>
<v-btn color="primary" class="caption" small @click="saveShareLinkPassword">
<!-- Save password -->
{{ $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/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;

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',

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/>.
*
*/

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);
}

33
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('input[type="password"]').type("1");
cy.snipActiveModal("Modal_ShareView_Password");
cy.getActiveModal().find('button:contains("Save password")').click();
cy.toastWait("Successfully updated");
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");

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

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