Browse Source

refactor: product analytics

Signed-off-by: Raju Udava <86527202+dstala@users.noreply.github.com>
pull/1795/head
Raju Udava 2 years ago
parent
commit
891e22dc80
  1. 1315
      packages/nc-gui/components/ProjectTreeView.vue
  2. 70
      packages/nc-gui/components/auth/apiTokens.vue
  3. 219
      packages/nc-gui/components/auth/shareOrInviteModal.vue
  4. 537
      packages/nc-gui/components/auth/userManagement.vue
  5. 164
      packages/nc-gui/components/base/shareBase.vue
  6. 1548
      packages/nc-gui/components/createOrEditProject.vue
  7. 102
      packages/nc-gui/components/previewAs.vue
  8. 128
      packages/nc-gui/components/project/appStore.vue
  9. 130
      packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue
  10. 142
      packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue
  11. 7
      packages/nc-gui/components/project/settings/appearance.vue
  12. 264
      packages/nc-gui/components/project/settings/xcMeta.vue
  13. 357
      packages/nc-gui/components/project/spreadsheet/components/columnFilter.vue
  14. 84
      packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue
  15. 555
      packages/nc-gui/components/project/spreadsheet/components/editColumn.vue
  16. 2
      packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue
  17. 476
      packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue
  18. 10
      packages/nc-gui/components/project/spreadsheet/components/extras.vue
  19. 344
      packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue
  20. 5
      packages/nc-gui/components/project/spreadsheet/components/headerCell.vue
  21. 99
      packages/nc-gui/components/project/spreadsheet/components/lockMenu.vue
  22. 18
      packages/nc-gui/components/project/spreadsheet/components/moreActions.vue
  23. 2
      packages/nc-gui/components/project/spreadsheet/components/shareViewMenu.vue
  24. 139
      packages/nc-gui/components/project/spreadsheet/components/sortListMenu.vue
  25. 459
      packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue
  26. 2
      packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue
  27. 1203
      packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue
  28. 776
      packages/nc-gui/components/project/spreadsheet/views/formView.vue
  29. 668
      packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue
  30. 112
      packages/nc-gui/components/project/table.vue
  31. 591
      packages/nc-gui/components/project/tableTabs/webhooks.vue
  32. 84
      packages/nc-gui/components/projectList/createNewProjectBtn.vue
  33. 396
      packages/nc-gui/components/projectTabs.vue
  34. 8
      packages/nc-gui/components/settings/settingsModal.vue
  35. 1384
      packages/nc-gui/components/templates/editor.vue
  36. 136
      packages/nc-gui/components/utils/language.vue
  37. 1
      packages/nc-gui/global.d.ts
  38. 552
      packages/nc-gui/layouts/default.vue
  39. 2
      packages/nc-gui/pages/project/xcdb.vue
  40. 716
      packages/nc-gui/pages/projects/index.vue
  41. 158
      packages/nc-gui/pages/projects/list.vue
  42. 2
      packages/nc-gui/pages/user/authentication/signin.vue
  43. 2
      packages/nc-gui/pages/user/authentication/signup/index.vue
  44. 8
      packages/nc-gui/plugins/tele.js

1315
packages/nc-gui/components/ProjectTreeView.vue

File diff suppressed because it is too large Load Diff

70
packages/nc-gui/components/auth/apiTokens.vue

@ -4,7 +4,7 @@
<v-spacer /> <v-spacer />
<x-btn <x-btn
v-ge="['roles','reload']" v-ge="['roles', 'reload']"
outlined outlined
tooltip="Reload API tokens" tooltip="Reload API tokens"
color="primary" color="primary"
@ -17,11 +17,11 @@
refresh refresh
</v-icon> </v-icon>
<!-- Reload --> <!-- Reload -->
{{ $t('general.reload') }} {{ $t("general.reload") }}
</x-btn> </x-btn>
<x-btn <x-btn
v-if="_isUIAllowed('newUser')" v-if="_isUIAllowed('newUser')"
v-ge="['roles','add new']" v-ge="['roles', 'add new']"
outlined outlined
tooltip="Generate new API token" tooltip="Generate new API token"
color="primary" color="primary"
@ -33,40 +33,43 @@
mdi-plus mdi-plus
</v-icon> </v-icon>
<!--New Token--> <!--New Token-->
{{ $t('activity.newToken') }} {{ $t("activity.newToken") }}
</x-btn> </x-btn>
</v-toolbar> </v-toolbar>
<v-container fluid> <v-container fluid>
<v-simple-table v-if="tokens" dense class="mx-auto caption text-center" style="max-width:700px"> <v-simple-table
v-if="tokens"
dense
class="mx-auto caption text-center"
style="max-width: 700px"
>
<thead> <thead>
<tr class=""> <tr class="">
<th class="caption text-center"> <th class="caption text-center">
<!--Description--> <!--Description-->
{{ $t('labels.description') }} {{ $t("labels.description") }}
</th> </th>
<th class="caption text-center"> <th class="caption text-center">
<!--Token--> <!--Token-->
{{ $t('labels.token') }} {{ $t("labels.token") }}
</th> </th>
<th class="caption text-center"> <th class="caption text-center">
<!--Actions--> <!--Actions-->
{{ $t('labels.action') }} {{ $t("labels.action") }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tr v-if="!tokens.length"> <tr v-if="!tokens.length">
<td colspan="3"> <td colspan="3">
<div <div class="text-center caption grey--text">
class="text-center caption grey--text"
>
No tokens available No tokens available
</div> </div>
</td> </td>
</tr> </tr>
<tr v-for="(token,i) in tokens" :key="i"> <tr v-for="(token, i) in tokens" :key="i">
<td class="caption text-center"> <td class="caption text-center">
{{ token.description }} {{ token.description }}
</td> </td>
@ -80,14 +83,19 @@
<x-icon <x-icon
x-small x-small
icon.class="ml-2" icon.class="ml-2"
:tooltip="`${token.show ?'Hide':'Show' } API token`" :tooltip="`${token.show ? 'Hide' : 'Show'} API token`"
@click="$set(token,'show' ,!token.show)" @click="$set(token, 'show', !token.show)"
> >
{{ token.show ? 'visibility_off' : 'visibility' }} {{ token.show ? "visibility_off" : "visibility" }}
</x-icon> </x-icon>
<!-- <v-spacer></v-spacer>--> <!-- <v-spacer></v-spacer>-->
<x-icon x-small icon.class="ml-2" tooltip="Copy token to clipboard" @click="copyToken(token.token)"> <x-icon
x-small
icon.class="ml-2"
tooltip="Copy token to clipboard"
@click="copyToken(token.token)"
>
mdi-content-copy mdi-content-copy
</x-icon> </x-icon>
<x-icon <x-icon
@ -114,7 +122,7 @@
</v-container> </v-container>
<v-dialog v-model="newTokenDialog" width="400"> <v-dialog v-model="newTokenDialog" width="400">
<v-card class="px-15 py-5 " style="min-height: 100%"> <v-card class="px-15 py-5" style="min-height: 100%">
<h4 class="text-center text-capitalize mt-2 d-100 display-1"> <h4 class="text-center text-capitalize mt-2 d-100 display-1">
<template>Generate Token</template> <template>Generate Token</template>
</h4> </h4>
@ -134,7 +142,7 @@
<v-card-actions class="justify-center"> <v-card-actions class="justify-center">
<x-btn <x-btn
v-ge="['rows','save']" v-ge="['rows', 'save']"
tooltip="Generate new api token" tooltip="Generate new api token"
color="primary" color="primary"
btn.class="mt-5 mb-3 pr-5" btn.class="mt-5 mb-3 pr-5"
@ -165,21 +173,26 @@ export default {
methods: { methods: {
showNewTokenDlg() { showNewTokenDlg() {
this.newTokenDialog = true this.newTokenDialog = true
this.$tele.emit('api-mgmt:token:generate:trigger') this.$e('c:api-token:generate')
}, },
copyToken(token) { copyToken(token) {
copyTextToClipboard(token) copyTextToClipboard(token)
this.$toast.info('Copied to clipboard').goAway(1000) this.$toast.info('Copied to clipboard').goAway(1000)
this.$tele.emit('api-mgmt:token:copy') this.$e('c:api-token:copy')
}, },
async loadApiTokens() { async loadApiTokens() {
this.tokens = (await this.$api.apiToken.list(this.$store.state.project.projectId)) this.tokens = await this.$api.apiToken.list(
this.$store.state.project.projectId
)
}, },
async generateToken() { async generateToken() {
try { try {
this.newTokenDialog = false this.newTokenDialog = false
await this.$api.apiToken.create(this.$store.state.project.projectId, this.tokenObj) await this.$api.apiToken.create(
this.$store.state.project.projectId,
this.tokenObj
)
this.$toast.success('Token generated successfully').goAway(3000) this.$toast.success('Token generated successfully').goAway(3000)
this.tokenObj = {} this.tokenObj = {}
await this.loadApiTokens() await this.loadApiTokens()
@ -188,11 +201,14 @@ export default {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000)
} }
this.$tele.emit('api-mgmt:token:generate:submit') this.$e('a:api-token:generate')
}, },
async deleteToken(item) { async deleteToken(item) {
try { try {
await this.$api.apiToken.delete(this.$store.state.project.projectId, item.token) await this.$api.apiToken.delete(
this.$store.state.project.projectId,
item.token
)
// this.tokens = //await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenDelete', { id: item.id }]) // this.tokens = //await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcApiTokenDelete', { id: item.id }])
this.$toast.success('Token deleted successfully').goAway(3000) this.$toast.success('Token deleted successfully').goAway(3000)
await this.loadApiTokens() await this.loadApiTokens()
@ -201,12 +217,10 @@ export default {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000)
} }
this.$tele.emit('api-mgmt:token:delete') this.$e('a:api-token:delete')
} }
} }
} }
</script> </script>
<style scoped> <style scoped></style>
</style>

219
packages/nc-gui/components/auth/shareOrInviteModal.vue

@ -1,42 +1,41 @@
<template> <template>
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :700" @close="invite_token = null"> <v-dialog
v-model="userEditDialog"
:width="invite_token ? 700 : 700"
@close="invite_token = null"
>
<v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0"> <v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0">
<v-card-title> <v-card-title>
{{ $t('activity.share') }} : {{ $store.getters['project/GtrProjectName'] }} {{ $t("activity.share") }} :
{{ $store.getters["project/GtrProjectName"] }}
<div class="nc-header-border" /> <div class="nc-header-border" />
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<div> <div>
<v-icon small> <v-icon small> mdi-account-outline </v-icon>
mdi-account-outline <template v-if="invite_token"> Copy Invite Token </template>
</v-icon> <template v-else-if="selectedUser.id"> Edit User </template>
<template v-if="invite_token">
Copy Invite Token
</template>
<template v-else-if="selectedUser.id">
Edit User
</template>
<template v-else> <template v-else>
<!-- Invite Team --> <!-- Invite Team -->
{{ $t('activity.inviteTeam') }} {{ $t("activity.inviteTeam") }}
</template> </template>
</div> </div>
<div class="pa-4 nc-invite-container"> <div class="pa-4 nc-invite-container">
<div v-if="invite_token" class="mt-6 align-center"> <div v-if="invite_token" class="mt-6 align-center">
<v-alert <v-alert
v-ripple v-ripple
type="success" type="success"
outlined outlined
class="pointer" class="pointer"
@click="clipboard(inviteUrl); $toast.success('Copied invite url to clipboard').goAway(3000)" @click="
clipboard(inviteUrl);
$toast.success('Copied invite url to clipboard').goAway(3000);
"
> >
<template #append> <template #append>
<v-icon color="green" class="ml-2"> <v-icon color="green" class="ml-2"> mdi-content-copy </v-icon>
mdi-content-copy
</v-icon>
</template> </template>
<div class="ellipsis d-100"> <div class="ellipsis d-100">
{{ inviteUrl }} {{ inviteUrl }}
@ -44,11 +43,15 @@
</v-alert> </v-alert>
<p class="caption grey--text mt-3"> <p class="caption grey--text mt-3">
{{ $t('msg.info.userInviteNoSMTP') }} {{ $t("msg.info.userInviteNoSMTP") }}
<!-- Looks like you have not configured mailer yet! <br>Please copy above --> <!-- Looks like you have not configured mailer yet! <br>Please copy above -->
<!-- invite --> <!-- invite -->
<!-- link and send it to --> <!-- link and send it to -->
{{ invite_token && (invite_token.email || invite_token.emails && invite_token.emails.join(', ')) }}. {{
invite_token &&
(invite_token.email ||
(invite_token.emails && invite_token.emails.join(", ")))
}}.
</p> </p>
<div class="text-right"> <div class="text-right">
@ -64,7 +67,7 @@
mdi-account-multiple-plus-outline mdi-account-multiple-plus-outline
</v-icon> </v-icon>
<!--Invite more--> <!--Invite more-->
{{ $t('activity.inviteMore') }} {{ $t("activity.inviteMore") }}
</x-btn> </x-btn>
</div> </div>
@ -86,12 +89,12 @@
class="caption" class="caption"
:hint="$t('msg.info.addMultipleUsers')" :hint="$t('msg.info.addMultipleUsers')"
label="Email" label="Email"
@input="edited=true" @input="edited = true"
> >
<template #label> <template #label>
<span class="caption"> <span class="caption">
<!-- Email --> <!-- Email -->
{{ $t('labels.email') }} {{ $t("labels.email") }}
</span> </span>
</template> </template>
</v-text-field> </v-text-field>
@ -110,12 +113,12 @@
deletable-chips deletable-chips
@change="edited = true" @change="edited = true"
> >
<template #selection="{item}"> <template #selection="{ item }">
<v-chip small :color="rolesColors[item]"> <v-chip small :color="rolesColors[item]">
{{ item }} {{ item }}
</v-chip> </v-chip>
</template> </template>
<template #item="{item}"> <template #item="{ item }">
<div> <div>
<div>{{ item }}</div> <div>{{ item }}</div>
<div class="mb-2 caption grey--text"> <div class="mb-2 caption grey--text">
@ -129,17 +132,18 @@
</v-form> </v-form>
<div class="text-center mt-0"> <div class="text-center mt-0">
<x-btn <x-btn
v-ge="['rows','save']" v-ge="['rows', 'save']"
:tooltip="$t('tooltip.saveChanges')" :tooltip="$t('tooltip.saveChanges')"
color="primary" color="primary"
btn.class="nc-invite-or-save-btn" btn.class="nc-invite-or-save-btn"
@click="saveUser" @click="saveUser"
> >
<v-icon small left> <v-icon small left>
{{ selectedUser.id ? 'save' : 'mdi-send' }} {{ selectedUser.id ? "save" : "mdi-send" }}
</v-icon> </v-icon>
{{ selectedUser.id ? $t('general.save') : $t('activity.invite') }} {{
selectedUser.id ? $t("general.save") : $t("activity.invite")
}}
</x-btn> </x-btn>
</div> </div>
</template> </template>
@ -154,132 +158,157 @@
</template> </template>
<script> <script>
import { isEmail } from '~/helpers' import { isEmail } from "~/helpers";
import { enumColor } from '~/components/project/spreadsheet/helpers/colors' import { enumColor } from "~/components/project/spreadsheet/helpers/colors";
import ShareBase from '~/components/base/shareBase' import ShareBase from "~/components/base/shareBase";
export default { export default {
name: 'ShareOrInviteModal', name: "ShareOrInviteModal",
components: { ShareBase }, components: { ShareBase },
props: { props: {
value: Boolean value: Boolean,
}, },
data: () => ({ data: () => ({
roles: ['creator', 'editor', 'commenter', 'viewer'], roles: ["creator", "editor", "commenter", "viewer"],
selectedUser: {}, selectedUser: {},
invite_token: null, invite_token: null,
valid: null, valid: null,
emailRules: [ emailRules: [
v => !!v || 'E-mail is required', (v) => !!v || "E-mail is required",
(v) => { (v) => {
const invalidEmails = (v || '').split(/\s*,\s*/).filter(e => !isEmail(e)) const invalidEmails = (v || "")
return !invalidEmails.length || `"${invalidEmails.join(', ')}" - invalid email` .split(/\s*,\s*/)
} .filter((e) => !isEmail(e));
return (
!invalidEmails.length ||
`"${invalidEmails.join(", ")}" - invalid email`
);
},
], ],
roleRules: [ roleRules: [
v => !!v || 'User Role is required', (v) => !!v || "User Role is required",
v => ['creator', 'editor', 'commenter', 'viewer'].includes(v) || 'invalid user role' (v) =>
["creator", "editor", "commenter", "viewer"].includes(v) ||
"invalid user role",
], ],
userList: [], userList: [],
roleDescriptions: {}, roleDescriptions: {},
deleteUserType: '' deleteUserType: "",
}), }),
computed: { computed: {
userEditDialog: { userEditDialog: {
get() { get() {
return this.value return this.value;
}, },
set(v) { set(v) {
this.$emit('input', v) this.$emit("input", v);
} },
}, },
inviteUrl() { inviteUrl() {
return this.invite_token ? `${location.origin}${location.pathname}#/user/authentication/signup/${this.invite_token.invite_token}` : null return this.invite_token
? `${location.origin}${location.pathname}#/user/authentication/signup/${this.invite_token.invite_token}`
: null;
}, },
rolesColors() { rolesColors() {
const colors = this.$store.state.windows.darkTheme ? enumColor.dark : enumColor.light const colors = this.$store.state.windows.darkTheme
? enumColor.dark
: enumColor.light;
return this.roles.reduce((o, r, i) => { return this.roles.reduce((o, r, i) => {
o[r] = colors[i % colors.length] o[r] = colors[i % colors.length];
return o return o;
}, {}) }, {});
}, },
selectedRoles: { selectedRoles: {
get() { get() {
return (this.selectedUser && this.selectedUser.roles ? this.selectedUser.roles.split(',') : []).sort((a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a))[0] return (
this.selectedUser && this.selectedUser.roles
? this.selectedUser.roles.split(",")
: []
).sort(
(a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a)
)[0];
}, },
set(roles) { set(roles) {
if (this.selectedUser) { if (this.selectedUser) {
this.selectedUser.roles = roles // .filter(Boolean).join(',') this.selectedUser.roles = roles; // .filter(Boolean).join(',')
} }
} },
} },
}, },
methods: { methods: {
async saveUser() { async saveUser() {
this.validate = true this.validate = true;
await this.$nextTick() await this.$nextTick();
if (this.loading || !this.$refs.form.validate() || !this.selectedUser) { if (this.loading || !this.$refs.form.validate() || !this.selectedUser) {
return return;
} }
this.$tele.emit(`user-mgmt:add:${this.selectedUser.roles}`) this.$e("a:user:invite", { role: this.selectedUser.roles });
if (!this.edited) { if (!this.edited) {
this.userEditDialog = false this.userEditDialog = false;
} }
try { try {
let data let data;
if (this.selectedUser.id) { if (this.selectedUser.id) {
await this.$api.auth.projectUserUpdate(this.$route.params.project_id, this.selectedUser.id, { await this.$api.auth.projectUserUpdate(
roles: this.selectedUser.roles, this.$route.params.project_id,
email: this.selectedUser.email, this.selectedUser.id,
project_id: this.$route.params.project_id, {
projectName: this.$store.getters['project/GtrProjectName'] roles: this.selectedUser.roles,
}) email: this.selectedUser.email,
project_id: this.$route.params.project_id,
projectName: this.$store.getters["project/GtrProjectName"],
}
);
} else { } else {
data = (await this.$api.auth.projectUserAdd(this.$route.params.project_id, { data = await this.$api.auth.projectUserAdd(
...this.selectedUser, this.$route.params.project_id,
project_id: this.$route.params.project_id, {
projectName: this.$store.getters['project/GtrProjectName'] ...this.selectedUser,
})) project_id: this.$route.params.project_id,
projectName: this.$store.getters["project/GtrProjectName"],
}
);
} }
this.$toast.success('Successfully updated the user details').goAway(3000) this.$toast
this.$emit('saved') .success("Successfully updated the user details")
.goAway(3000);
this.$emit("saved");
if (data && data.invite_token) { if (data && data.invite_token) {
this.invite_token = data this.invite_token = data;
// todo: bring anim // todo: bring anim
// this.simpleAnim() // this.simpleAnim()
return return;
} }
} catch (e) { } catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
} }
this.userEditDialog = false this.userEditDialog = false;
}, },
clickInviteMore() { clickInviteMore() {
this.$tele.emit('user-mgmt:invite-more') this.$e("c:user:invite-more");
this.invite_token = null this.invite_token = null;
this.selectedUser = { roles: 'editor' } this.selectedUser = { roles: "editor" };
}, },
clipboard(str) { clipboard(str) {
const el = document.createElement('textarea') const el = document.createElement("textarea");
el.addEventListener('focusin', e => e.stopPropagation()) el.addEventListener("focusin", (e) => e.stopPropagation());
el.value = str el.value = str;
document.body.appendChild(el) document.body.appendChild(el);
el.select() el.select();
document.execCommand('copy') document.execCommand("copy");
document.body.removeChild(el) document.body.removeChild(el);
this.$tele.emit('user-mgmt:copy-url') this.$e("c:user:copy-url");
} },
} },
} };
</script> </script>
<style scoped> <style scoped></style>
</style>

537
packages/nc-gui/components/auth/userManagement.vue

@ -13,16 +13,14 @@
@keypress.enter="loadUsers" @keypress.enter="loadUsers"
> >
<template #prepend-inner> <template #prepend-inner>
<v-icon small class="mt-1"> <v-icon small class="mt-1"> search </v-icon>
search
</v-icon>
</template> </template>
</v-text-field> </v-text-field>
<v-spacer /> <v-spacer />
<!-- tooltip="Reload roles" --> <!-- tooltip="Reload roles" -->
<x-btn <x-btn
v-ge="['roles','reload']" v-ge="['roles', 'reload']"
outlined outlined
:tooltip="$t('activity.reloadRoles')" :tooltip="$t('activity.reloadRoles')"
color="primary" color="primary"
@ -31,17 +29,15 @@
@click="clickReload" @click="clickReload"
@click.prevent @click.prevent
> >
<v-icon small left> <v-icon small left> refresh </v-icon>
refresh
</v-icon>
<!-- Reload --> <!-- Reload -->
{{ $t('general.reload') }} {{ $t("general.reload") }}
</x-btn> </x-btn>
<!-- tooltip="Add new role" --> <!-- tooltip="Add new role" -->
<x-btn <x-btn
v-if="_isUIAllowed('newUser')" v-if="_isUIAllowed('newUser')"
v-ge="['roles', 'add new']"
class="nc-new-user" class="nc-new-user"
v-ge="['roles','add new']"
outlined outlined
:tooltip="$t('tooltip.addRole')" :tooltip="$t('tooltip.addRole')"
color="primary" color="primary"
@ -49,25 +45,28 @@
:disabled="loading" :disabled="loading"
@click="addUser" @click="addUser"
> >
<v-icon small left> <v-icon small left> mdi-plus </v-icon>
mdi-plus
</v-icon>
<!-- New User --> <!-- New User -->
{{ $t('activity.newUser') }} {{ $t("activity.newUser") }}
</x-btn> </x-btn>
</v-toolbar> </v-toolbar>
<v-card style="height:calc(100% - 38px)" class="elevation-0"> <v-card style="height: calc(100% - 38px)" class="elevation-0">
<v-container style="height: 100%" fluid> <v-container style="height: 100%" fluid>
<v-row style="height:100%"> <v-row style="height: 100%">
<v-col cols="12" class="h-100"> <v-col cols="12" class="h-100">
<v-card class="h-100 elevation-0"> <v-card class="h-100 elevation-0">
<v-row style="height:100%"> <v-row style="height: 100%">
<v-col offset="2" :cols="8" class="h-100" style="overflow-y: auto"> <v-col
offset="2"
:cols="8"
class="h-100"
style="overflow-y: auto"
>
<v-data-table <v-data-table
v-if="users" v-if="users"
dense dense
:headers="[{},{},{},{}]" :headers="[{}, {}, {}, {}]"
hide-default-header hide-default-header
:hide-default-footer="count < limit" :hide-default-footer="count < limit"
:options.sync="options" :options.sync="options"
@ -80,29 +79,25 @@
<tr class="text-left"> <tr class="text-left">
<!-- <th>#</th>--> <!-- <th>#</th>-->
<th class="font-weight-regular caption"> <th class="font-weight-regular caption">
<v-icon small> <v-icon small> mdi-email-outline </v-icon>
mdi-email-outline
</v-icon>
<!-- Email --> <!-- Email -->
{{ $t('labels.email') }} {{ $t("labels.email") }}
</th> </th>
<th class="font-weight-regular caption"> <th class="font-weight-regular caption">
<v-icon small> <v-icon small> mdi-drama-masks </v-icon>
mdi-drama-masks
</v-icon>
<!-- Roles --> <!-- Roles -->
{{ $t('objects.roles') }} {{ $t("objects.roles") }}
</th> </th>
<th class="font-weight-regular caption"> <th class="font-weight-regular caption">
<!-- <v-icon small class="mt-n1">mdi-cursor-default-outline</v-icon>--> <!-- <v-icon small class="mt-n1">mdi-cursor-default-outline</v-icon>-->
<!-- Actions --> <!-- Actions -->
{{ $t('labels.actions') }} {{ $t("labels.actions") }}
</th> </th>
</tr> </tr>
</thead> </thead>
</template> </template>
<template #item="{item}"> <template #item="{ item }">
<tr @click="selectedUser = item"> <tr @click="selectedUser = item">
<td>{{ item.email }}</td> <td>{{ item.email }}</td>
<td> <td>
@ -124,7 +119,11 @@
icon-class="" icon-class=""
color="primary" color="primary"
small small
@click.prevent.stop="invite_token = null; selectedUser = item; userEditDialog = true" @click.prevent.stop="
invite_token = null;
selectedUser = item;
userEditDialog = true;
"
> >
mdi-pencil-outline mdi-pencil-outline
</x-icon> </x-icon>
@ -168,7 +167,12 @@
icon-class="" icon-class=""
color="primary" color="primary"
small small
@click.prevent.stop="clipboard(getInviteUrl(item.invite_token)); $toast.success('Invite url copied to clipboard').goAway(3000)" @click.prevent.stop="
clipboard(getInviteUrl(item.invite_token));
$toast
.success('Invite url copied to clipboard')
.goAway(3000);
"
> >
mdi-content-copy mdi-content-copy
</x-icon> </x-icon>
@ -203,7 +207,11 @@
</v-col> </v-col>
</v-row> </v-row>
<table v-if="false" class="mx-auto users-table" style="min-width:700px"> <table
v-if="false"
class="mx-auto users-table"
style="min-width: 700px"
>
<thead> <thead>
<tr> <tr>
<th /> <th />
@ -215,9 +223,7 @@
<tbody> <tbody>
<tr v-for="user in users" :key="user.email"> <tr v-for="user in users" :key="user.email">
<td> <td>
<v-icon x-large> <v-icon x-large> mdi-account-outline </v-icon>
mdi-account-outline
</v-icon>
</td> </td>
<td class="px-1 py-1" align="top"> <td class="px-1 py-1" align="top">
<v-text-field <v-text-field
@ -233,36 +239,22 @@
<set-list-checkbox-cell v-model="user.roles" :values="roles" /> <set-list-checkbox-cell v-model="user.roles" :values="roles" />
</td> </td>
<td align="middle"> <td align="middle">
<v-icon large> <v-icon large> mdi-close </v-icon>
mdi-close <v-icon large> mdi-save </v-icon>
</v-icon>
<v-icon large>
mdi-save
</v-icon>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<v-icon x-large> <v-icon x-large> mdi-account-outline </v-icon>
mdi-account-outline
</v-icon>
</td> </td>
<td class="px-1 py-1" align="top"> <td class="px-1 py-1" align="top">
<v-text-field <v-text-field solo class="elevation-0" hide-details dense />
solo
class="elevation-0"
hide-details
dense
/>
</td> </td>
<td class="px-1 py-1"> <td class="px-1 py-1">
<set-list-checkbox-cell :values="roles" /> <set-list-checkbox-cell :values="roles" />
</td> </td>
<td align="middle"> <td align="middle">
<v-icon large> <v-icon large> mdi-account-plus </v-icon>
mdi-account-plus
</v-icon>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -278,44 +270,43 @@
/> />
<!-- todo: move to a separate component--> <!-- todo: move to a separate component-->
<v-dialog v-model="userEditDialog" :width="invite_token ? 700 :700" @close="invite_token = null"> <v-dialog
v-model="userEditDialog"
:width="invite_token ? 700 : 700"
@close="invite_token = null"
>
<v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0"> <v-card v-if="selectedUser" style="min-height: 100%" class="elevation-0">
<v-card-title> <v-card-title>
{{ $t('activity.share') }} : {{ $store.getters['project/GtrProjectName'] }} {{ $t("activity.share") }} :
{{ $store.getters["project/GtrProjectName"] }}
<div class="nc-header-border" /> <div class="nc-header-border" />
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<div> <div>
<v-icon small> <v-icon small> mdi-account-outline </v-icon>
mdi-account-outline <template v-if="invite_token"> Copy Invite Token </template>
</v-icon> <template v-else-if="selectedUser.id"> Edit User </template>
<template v-if="invite_token">
Copy Invite Token
</template>
<template v-else-if="selectedUser.id">
Edit User
</template>
<template v-else> <template v-else>
<!-- Invite Team --> <!-- Invite Team -->
{{ $t('activity.inviteTeam') }} {{ $t("activity.inviteTeam") }}
</template> </template>
</div> </div>
<div class="pa-4 nc-invite-container"> <div class="pa-4 nc-invite-container">
<div v-if="invite_token" class="mt-6 align-center"> <div v-if="invite_token" class="mt-6 align-center">
<v-alert <v-alert
v-ripple v-ripple
type="success" type="success"
outlined outlined
class="pointer" class="pointer"
@click="clipboard(inviteUrl); $toast.success('Copied invite url to clipboard').goAway(3000)" @click="
clipboard(inviteUrl);
$toast.success('Copied invite url to clipboard').goAway(3000);
"
> >
<template #append> <template #append>
<v-icon color="green" class="ml-2"> <v-icon color="green" class="ml-2"> mdi-content-copy </v-icon>
mdi-content-copy
</v-icon>
</template> </template>
<div class="ellipsis d-100"> <div class="ellipsis d-100">
{{ inviteUrl }} {{ inviteUrl }}
@ -323,11 +314,15 @@
</v-alert> </v-alert>
<p class="caption grey--text mt-3"> <p class="caption grey--text mt-3">
{{ $t('msg.info.userInviteNoSMTP') }} {{ $t("msg.info.userInviteNoSMTP") }}
<!-- Looks like you have not configured mailer yet! <br>Please copy above --> <!-- Looks like you have not configured mailer yet! <br>Please copy above -->
<!-- invite --> <!-- invite -->
<!-- link and send it to --> <!-- link and send it to -->
{{ invite_token && (invite_token.email || invite_token.emails && invite_token.emails.join(', ')) }}. {{
invite_token &&
(invite_token.email ||
(invite_token.emails && invite_token.emails.join(", ")))
}}.
</p> </p>
<div class="text-right"> <div class="text-right">
@ -343,7 +338,7 @@
mdi-account-multiple-plus-outline mdi-account-multiple-plus-outline
</v-icon> </v-icon>
<!--Invite more--> <!--Invite more-->
{{ $t('activity.inviteMore') }} {{ $t("activity.inviteMore") }}
</x-btn> </x-btn>
</div> </div>
@ -365,12 +360,12 @@
class="caption" class="caption"
:hint="$t('msg.info.addMultipleUsers')" :hint="$t('msg.info.addMultipleUsers')"
label="Email" label="Email"
@input="edited=true" @input="edited = true"
> >
<template #label> <template #label>
<span class="caption"> <span class="caption">
<!-- Email --> <!-- Email -->
{{ $t('labels.email') }} {{ $t("labels.email") }}
</span> </span>
</template> </template>
</v-text-field> </v-text-field>
@ -389,12 +384,12 @@
deletable-chips deletable-chips
@change="edited = true" @change="edited = true"
> >
<template #selection="{item}"> <template #selection="{ item }">
<v-chip small :color="rolesColors[item]"> <v-chip small :color="rolesColors[item]">
{{ item }} {{ item }}
</v-chip> </v-chip>
</template> </template>
<template #item="{item}"> <template #item="{ item }">
<div> <div>
<div>{{ item }}</div> <div>{{ item }}</div>
<div class="mb-2 caption grey--text"> <div class="mb-2 caption grey--text">
@ -408,17 +403,18 @@
</v-form> </v-form>
<div class="text-center mt-0"> <div class="text-center mt-0">
<x-btn <x-btn
v-ge="['rows','save']" v-ge="['rows', 'save']"
:tooltip="$t('tooltip.saveChanges')" :tooltip="$t('tooltip.saveChanges')"
color="primary" color="primary"
btn.class="nc-invite-or-save-btn" btn.class="nc-invite-or-save-btn"
@click="saveUser" @click="saveUser"
> >
<v-icon small left> <v-icon small left>
{{ selectedUser.id ? 'save' : 'mdi-send' }} {{ selectedUser.id ? "save" : "mdi-send" }}
</v-icon> </v-icon>
{{ selectedUser.id ? $t('general.save') : $t('activity.invite') }} {{
selectedUser.id ? $t("general.save") : $t("activity.invite")
}}
</x-btn> </x-btn>
</div> </div>
</template> </template>
@ -434,17 +430,23 @@
</template> </template>
<script> <script>
import FeedbackForm from '@/components/feedbackForm' import FeedbackForm from "@/components/feedbackForm";
import SetListCheckboxCell from '@/components/project/spreadsheet/components/editableCell/setListCheckboxCell' import SetListCheckboxCell from "@/components/project/spreadsheet/components/editableCell/setListCheckboxCell";
import { enumColor } from '@/components/project/spreadsheet/helpers/colors' import { enumColor } from "@/components/project/spreadsheet/helpers/colors";
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel' import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import { isEmail } from '@/helpers' import { isEmail } from "@/helpers";
import ShareBase from '~/components/base/shareBase' import ShareBase from "~/components/base/shareBase";
import XBtn from '~/components/global/xBtn' import XBtn from "~/components/global/xBtn";
export default { export default {
name: 'UserManagement', name: "UserManagement",
components: { XBtn, ShareBase, FeedbackForm, DlgLabelSubmitCancel, SetListCheckboxCell }, components: {
XBtn,
ShareBase,
FeedbackForm,
DlgLabelSubmitCancel,
SetListCheckboxCell,
},
data: () => ({ data: () => ({
validate: false, validate: false,
deleteItem: null, deleteItem: null,
@ -458,184 +460,217 @@ export default {
loading: false, loading: false,
selectedUser: null, selectedUser: null,
roles: [], roles: [],
query: '', query: "",
deleteId: null, deleteId: null,
edited: false, edited: false,
valid: null, valid: null,
emailRules: [ emailRules: [
v => !!v || 'E-mail is required', (v) => !!v || "E-mail is required",
(v) => { (v) => {
const invalidEmails = (v || '').split(/\s*,\s*/).filter(e => !isEmail(e)) const invalidEmails = (v || "")
return !invalidEmails.length || `"${invalidEmails.join(', ')}" - invalid email` .split(/\s*,\s*/)
} .filter((e) => !isEmail(e));
return (
!invalidEmails.length ||
`"${invalidEmails.join(", ")}" - invalid email`
);
},
], ],
roleRules: [ roleRules: [
v => !!v || 'User Role is required', (v) => !!v || "User Role is required",
v => ['creator', 'editor', 'commenter', 'viewer'].includes(v) || 'invalid user role' (v) =>
["creator", "editor", "commenter", "viewer"].includes(v) ||
"invalid user role",
], ],
userList: [], userList: [],
roleDescriptions: {}, roleDescriptions: {},
deleteUserType: '' // [DELETE_FROM_PROJECT, DELETE_FROM_NOCODB] deleteUserType: "", // [DELETE_FROM_PROJECT, DELETE_FROM_NOCODB]
}), }),
computed: { computed: {
roleNames() { roleNames() {
return this.roles.map(r => r.title) return this.roles.map((r) => r.title);
}, },
inviteUrl() { inviteUrl() {
return this.invite_token ? `${location.origin}${location.pathname}#/user/authentication/signup/${this.invite_token.invite_token}` : null return this.invite_token
? `${location.origin}${location.pathname}#/user/authentication/signup/${this.invite_token.invite_token}`
: null;
}, },
rolesColors() { rolesColors() {
const colors = this.$store.state.windows.darkTheme ? enumColor.dark : enumColor.light const colors = this.$store.state.windows.darkTheme
? enumColor.dark
: enumColor.light;
return this.roles.reduce((o, r, i) => { return this.roles.reduce((o, r, i) => {
o[r] = colors[i % colors.length] o[r] = colors[i % colors.length];
return o return o;
}, {}) }, {});
}, },
selectedRoles: { selectedRoles: {
get() { get() {
return (this.selectedUser && this.selectedUser.roles ? this.selectedUser.roles.split(',') : []).sort((a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a))[0] return (
this.selectedUser && this.selectedUser.roles
? this.selectedUser.roles.split(",")
: []
).sort(
(a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a)
)[0];
}, },
set(roles) { set(roles) {
if (this.selectedUser) { if (this.selectedUser) {
this.selectedUser.roles = roles // .filter(Boolean).join(',') this.selectedUser.roles = roles; // .filter(Boolean).join(',')
} }
} },
}, },
selectedUserIndex: { selectedUserIndex: {
get() { get() {
return this.users ? this.users.findIndex(u => u.email === this.selectedUser.email) : -1 return this.users
? this.users.findIndex((u) => u.email === this.selectedUser.email)
: -1;
}, },
set(i) { set(i) {
this.selectedUser = this.users[i] this.selectedUser = this.users[i];
} },
}, },
dialogMessage() { dialogMessage() {
let msg = 'Do you want to remove the user' let msg = "Do you want to remove the user";
if (this.deleteUserType === 'DELETE_FROM_PROJECT') { msg += ' from Project' } else if (this.deleteUserType === 'DELETE_FROM_NOCODB') { msg += ' from NocoDB' } if (this.deleteUserType === "DELETE_FROM_PROJECT") {
msg += '?' msg += " from Project";
return msg } else if (this.deleteUserType === "DELETE_FROM_NOCODB") {
} msg += " from NocoDB";
}
msg += "?";
return msg;
},
}, },
watch: { watch: {
options: { options: {
async handler() { async handler() {
await this.loadUsers() await this.loadUsers();
}, },
deep: true deep: true,
}, },
userEditDialog(v) { userEditDialog(v) {
// if (!v) { this.validate = false } // if (!v) { this.validate = false }
if (v && (this.selectedUser && !this.selectedUser.id)) { if (v && this.selectedUser && !this.selectedUser.id) {
this.$nextTick(() => { this.$nextTick(() => {
setTimeout(() => { setTimeout(() => {
this.$refs.email.$el.querySelector('input').focus() this.$refs.email.$el.querySelector("input").focus();
}, 100) }, 100);
}) });
} }
} },
}, },
async created() { async created() {
this.$eventBus.$on('show-add-user', this.addUser) this.$eventBus.$on("show-add-user", this.addUser);
await this.loadUsers() await this.loadUsers();
await this.loadRoles() await this.loadRoles();
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('show-add-user', this.addUser) this.$eventBus.$off("show-add-user", this.addUser);
}, },
methods: { methods: {
clickReload() { clickReload() {
this.loadUsers() this.loadUsers();
this.$tele.emit('user-mgmt:reload') this.$e("a:user:reload");
}, },
clickDeleteUser(id) { clickDeleteUser(id) {
this.$tele.emit('user-mgmt:delete:trigger') this.$e("c:user:delete");
this.deleteId = id this.deleteId = id;
this.deleteItem = id this.deleteItem = id;
this.showConfirmDlg = true this.showConfirmDlg = true;
this.deleteUserType = 'DELETE_FROM_PROJECT' this.deleteUserType = "DELETE_FROM_PROJECT";
}, },
clickInviteMore() { clickInviteMore() {
this.$tele.emit('user-mgmt:invite-more') this.$e("c:user:invite-more");
this.invite_token = null this.invite_token = null;
this.selectedUser = { roles: 'editor' } this.selectedUser = { roles: "editor" };
}, },
getRole(roles) { getRole(roles) {
return (roles ? roles.split(',') : []).sort((a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a))[0] return (roles ? roles.split(",") : []).sort(
(a, b) => this.roleNames.indexOf(a) - this.roleNames.indexOf(a)
)[0];
}, },
simpleAnim() { simpleAnim() {
const count = 30 const count = 30;
const defaults = { const defaults = {
origin: { y: 0.7 }, origin: { y: 0.7 },
zIndex: 9999999 zIndex: 9999999,
} };
function fire(particleRatio, opts) { function fire(particleRatio, opts) {
window.confetti(Object.assign({}, defaults, opts, { window.confetti(
particleCount: Math.floor(count * particleRatio) Object.assign({}, defaults, opts, {
})) particleCount: Math.floor(count * particleRatio),
})
);
} }
fire(0.25, { fire(0.25, {
spread: 26, spread: 26,
startVelocity: 55 startVelocity: 55,
}) });
fire(0.2, { fire(0.2, {
spread: 60 spread: 60,
}) });
fire(0.35, { fire(0.35, {
spread: 100, spread: 100,
decay: 0.91, decay: 0.91,
scalar: 0.8 scalar: 0.8,
}) });
fire(0.1, { fire(0.1, {
spread: 120, spread: 120,
startVelocity: 25, startVelocity: 25,
decay: 0.92, decay: 0.92,
scalar: 1.2 scalar: 1.2,
}) });
fire(0.1, { fire(0.1, {
spread: 120, spread: 120,
startVelocity: 45 startVelocity: 45,
}) });
}, },
getInviteUrl(token) { getInviteUrl(token) {
return token ? `${location.origin}${location.pathname}#/user/authentication/signup/${token}` : null return token
? `${location.origin}${location.pathname}#/user/authentication/signup/${token}`
: null;
}, },
clipboard(str) { clipboard(str) {
const el = document.createElement('textarea') const el = document.createElement("textarea");
el.addEventListener('focusin', e => e.stopPropagation()) el.addEventListener("focusin", (e) => e.stopPropagation());
el.value = str el.value = str;
document.body.appendChild(el) document.body.appendChild(el);
el.select() el.select();
document.execCommand('copy') document.execCommand("copy");
document.body.removeChild(el) document.body.removeChild(el);
this.$tele.emit('user-mgmt:copy-url') this.$e("c:user:copy-url");
}, },
async resendInvite(id) { async resendInvite(id) {
try { try {
await this.$axios.post('/admin/resendInvite/' + id, { await this.$axios.post(
projectName: this.$store.getters['project/GtrProjectName'] "/admin/resendInvite/" + id,
}, { {
headers: { projectName: this.$store.getters["project/GtrProjectName"],
'xc-auth': this.$store.state.users.token
}, },
params: { {
project_id: this.$route.params.project_id headers: {
"xc-auth": this.$store.state.users.token,
},
params: {
project_id: this.$route.params.project_id,
},
} }
}) );
this.$toast.success('Invite email sent successfully').goAway(3000) this.$toast.success("Invite email sent successfully").goAway(3000);
await this.loadUsers() await this.loadUsers();
} catch (e) { } catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000) this.$toast.error(e.response.data.msg).goAway(3000);
} }
this.$tele.emit('user-mgmt:resend-invite') this.$e("a:user:resend-invite");
}, },
async loadUsers() { async loadUsers() {
try { try {
const { page = 1, itemsPerPage = 20 } = this.options const { page = 1, itemsPerPage = 20 } = this.options;
// const data = (await this.$axios.get('/admin', { // const data = (await this.$axios.get('/admin', {
// headers: { // headers: {
// 'xc-auth': this.$store.state.users.token // 'xc-auth': this.$store.state.users.token
@ -648,26 +683,29 @@ export default {
// } // }
// })).data // })).data
const userData = (await this.$api.auth.projectUserList(this.$store.state.project.projectId, { const userData = await this.$api.auth.projectUserList(
query: { this.$store.state.project.projectId,
limit: itemsPerPage, {
offset: (page - 1) * itemsPerPage, query: {
query: this.query limit: itemsPerPage,
offset: (page - 1) * itemsPerPage,
query: this.query,
},
} }
})) );
this.count = userData.users.pageInfo.totalRows this.count = userData.users.pageInfo.totalRows;
this.users = userData.users.list this.users = userData.users.list;
if (!this.selectedUser && this.users && this.users[0]) { if (!this.selectedUser && this.users && this.users[0]) {
this.selectedUserIndex = 0 this.selectedUserIndex = 0;
} }
} catch (e) { } catch (e) {
console.log(e) console.log(e);
} }
}, },
async loadRoles() { async loadRoles() {
try { try {
this.roles = ['creator', 'editor', 'commenter', 'viewer'] this.roles = ["creator", "editor", "commenter", "viewer"];
// todo: // todo:
// (await this.$axios.get('/admin/roles', { // (await this.$axios.get('/admin/roles', {
@ -682,91 +720,112 @@ export default {
// return role.title // return role.title
// }).filter(role => role !== 'guest') // }).filter(role => role !== 'guest')
} catch (e) { } catch (e) {
console.log(e) console.log(e);
} }
}, },
async deleteUser(id, type) { async deleteUser(id, type) {
try { try {
await this.$api.auth.projectUserRemove(this.$route.params.project_id, id) await this.$api.auth.projectUserRemove(
this.$toast.success(`Successfully removed the user from ${type === 'DELETE_FROM_PROJECT' ? 'project' : 'NocoDB'}`).goAway(3000) this.$route.params.project_id,
await this.loadUsers() id
);
this.$toast
.success(
`Successfully removed the user from ${
type === "DELETE_FROM_PROJECT" ? "project" : "NocoDB"
}`
)
.goAway(3000);
await this.loadUsers();
} catch (e) { } catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000) this.$toast.error(e.response.data.msg).goAway(3000);
} }
}, },
async confirmDelete(hideDialog) { async confirmDelete(hideDialog) {
if (hideDialog) { if (hideDialog) {
this.showConfirmDlg = false this.showConfirmDlg = false;
return return;
} }
await this.deleteUser(this.deleteId, this.deleteUserType) await this.deleteUser(this.deleteId, this.deleteUserType);
this.showConfirmDlg = false this.showConfirmDlg = false;
this.$tele.emit('user-mgmt:delete:submit') this.$e("a:user:delete");
}, },
addUser() { addUser() {
this.invite_token = null this.invite_token = null;
this.selectedUser = { this.selectedUser = {
roles: 'editor' roles: "editor",
} };
this.userEditDialog = true this.userEditDialog = true;
this.$tele.emit('user-mgmt:add-user:trigger') this.$e("c:user:add");
}, },
async inviteUser(item) { async inviteUser(item) {
try { try {
await this.$api.auth.projectUserAdd(this.$route.params.project_id, item) await this.$api.auth.projectUserAdd(
this.$toast.success('Successfully added user to project').goAway(3000) this.$route.params.project_id,
await this.loadUsers() item
);
this.$toast.success("Successfully added user to project").goAway(3000);
await this.loadUsers();
} catch (e) { } catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000) this.$toast.error(e.response.data.msg).goAway(3000);
} }
this.$tele.emit('user-mgmt:invite-user') this.$e("a:user:add");
}, },
async saveUser() { async saveUser() {
this.validate = true this.validate = true;
await this.$nextTick() await this.$nextTick();
if (this.loading || !this.$refs.form.validate() || !this.selectedUser) { if (this.loading || !this.$refs.form.validate() || !this.selectedUser) {
return return;
} }
this.$tele.emit(`user-mgmt:add:${this.selectedUser.roles}`) this.$e("a:user:invite", { role: this.selectedUser.roles });
if (!this.edited) { if (!this.edited) {
this.userEditDialog = false this.userEditDialog = false;
} }
try { try {
let data let data;
if (this.selectedUser.id) { if (this.selectedUser.id) {
await this.$api.auth.projectUserUpdate(this.$route.params.project_id, this.selectedUser.id, { await this.$api.auth.projectUserUpdate(
roles: this.selectedUser.roles, this.$route.params.project_id,
email: this.selectedUser.email, this.selectedUser.id,
project_id: this.$route.params.project_id, {
projectName: this.$store.getters['project/GtrProjectName'] roles: this.selectedUser.roles,
}) email: this.selectedUser.email,
project_id: this.$route.params.project_id,
projectName: this.$store.getters["project/GtrProjectName"],
}
);
} else { } else {
data = (await this.$api.auth.projectUserAdd(this.$route.params.project_id, { data = await this.$api.auth.projectUserAdd(
...this.selectedUser, this.$route.params.project_id,
project_id: this.$route.params.project_id, {
projectName: this.$store.getters['project/GtrProjectName'] ...this.selectedUser,
})) project_id: this.$route.params.project_id,
projectName: this.$store.getters["project/GtrProjectName"],
}
);
} }
this.$toast.success('Successfully updated the user details').goAway(3000) this.$toast
await this.loadUsers() .success("Successfully updated the user details")
.goAway(3000);
await this.loadUsers();
if (data && data.invite_token) { if (data && data.invite_token) {
this.invite_token = data this.invite_token = data;
this.simpleAnim() this.simpleAnim();
return return;
} }
} catch (e) { } catch (e) {
this.$toast.error(e.response.data.msg).goAway(3000) this.$toast.error(e.response.data.msg).goAway(3000);
} }
await this.loadUsers() await this.loadUsers();
} },
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -779,7 +838,8 @@ export default {
background-color: transparent; background-color: transparent;
} }
.search-field.v-text-field > .v-input__control, .search-field.v-text-field > .v-input__control > .v-input__slot { .search-field.v-text-field > .v-input__control,
.search-field.v-text-field > .v-input__control > .v-input__slot {
min-height: auto; min-height: auto;
} }
@ -798,11 +858,11 @@ export default {
.users-table { .users-table {
td:first-child { td:first-child {
width: 50px width: 50px;
} }
td:last-child { td:last-child {
width: 50px width: 50px;
} }
} }
@ -812,7 +872,6 @@ export default {
background: var(--v-backgroundColor-base); background: var(--v-backgroundColor-base);
.v-input .v-input__slot { .v-input .v-input__slot {
background: var(--v-backgroundColorDefault-base); background: var(--v-backgroundColorDefault-base);
} }
} }

164
packages/nc-gui/components/base/shareBase.vue

@ -1,32 +1,29 @@
<template> <template>
<div> <div>
<v-icon color="grey" small> <v-icon color="grey" small> mdi-open-in-new </v-icon>
mdi-open-in-new
</v-icon>
<span class="grey--text caption"> <span class="grey--text caption">
<!-- Shared base link --> <!-- Shared base link -->
{{ $t('activity.shareBase.link') }} {{ $t("activity.shareBase.link") }}
</span> </span>
<div class="nc-container"> <div class="nc-container">
<v-chip v-if="base.uuid" :color="colors[4]" style="" class="rounded pl-1 pr-0 d-100 nc-url-chip pr-3"> <v-chip
v-if="base.uuid"
:color="colors[4]"
style=""
class="rounded pl-1 pr-0 d-100 nc-url-chip pr-3"
>
<div class="nc-url-wrapper d-flex mx-1 align-center d-100"> <div class="nc-url-wrapper d-flex mx-1 align-center d-100">
<span class="nc-url flex-grow-1 caption ">{{ url }}</span> <span class="nc-url flex-grow-1 caption">{{ url }}</span>
<v-spacer /> <v-spacer />
<v-divider vertical /> <v-divider vertical />
<!-- tooltip="reload" --> <!-- tooltip="reload" -->
<x-icon <x-icon :tooltip="$t('general.reload')" @click="recreate">
:tooltip="$t('general.reload')"
@click="recreate"
>
mdi-reload mdi-reload
</x-icon> </x-icon>
<!-- tooltip="copy URL" --> <!-- tooltip="copy URL" -->
<x-icon <x-icon :tooltip="$t('activity.copyUrl')" @click="copyUrl">
:tooltip="$t('activity.copyUrl')"
@click="copyUrl"
>
mdi-content-copy mdi-content-copy
</x-icon> </x-icon>
@ -51,49 +48,47 @@
<div class="d-flex align-center px-2"> <div class="d-flex align-center px-2">
<div> <div>
<v-menu offset-x> <v-menu offset-x>
<template #activator="{on}"> <template #activator="{ on }">
<div class="my-2" v-on="on"> <div class="my-2" v-on="on">
<div class="font-weight-bold nc-disable-shared-base"> <div class="font-weight-bold nc-disable-shared-base">
<span v-if="base.uuid"> <span v-if="base.uuid">
<!-- Anyone with the link --> <!-- Anyone with the link -->
{{ $t('activity.shareBase.enable') }} {{ $t("activity.shareBase.enable") }}
</span> </span>
<span v-else> <span v-else>
<!-- Disable shared base --> <!-- Disable shared base -->
{{ $t('activity.shareBase.disable') }} {{ $t("activity.shareBase.disable") }}
</span> </span>
<v-icon small> <v-icon small> mdi-menu-down-outline </v-icon>
mdi-menu-down-outline
</v-icon>
</div> </div>
</div> </div>
</template> </template>
<v-list dense> <v-list dense>
<v-list-item v-if="!base.uuid" dense @click="createSharedBase('viewer')"> <v-list-item
v-if="!base.uuid"
dense
@click="createSharedBase('viewer')"
>
<v-list-item-title> <v-list-item-title>
<v-icon small class="mr-1"> <v-icon small class="mr-1"> mdi-link-variant </v-icon>
mdi-link-variant
</v-icon>
<span class="caption"> <span class="caption">
<!-- Anyone with the link --> <!-- Anyone with the link -->
{{ $t('activity.shareBase.enable') }} {{ $t("activity.shareBase.enable") }}
</span> </span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="base.uuid" dense @click="disableSharedBase"> <v-list-item v-if="base.uuid" dense @click="disableSharedBase">
<v-list-item-title> <v-list-item-title>
<v-icon small class="mr-1"> <v-icon small class="mr-1"> mdi-link-variant-off </v-icon>
mdi-link-variant-off
</v-icon>
<span class="caption"> <span class="caption">
<!-- Disable shared base --> <!-- Disable shared base -->
{{ $t('activity.shareBase.disable') }} {{ $t("activity.shareBase.disable") }}
</span> </span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<div class=" caption"> <div class="caption">
<template v-if="base.enabled"> <template v-if="base.enabled">
<span v-if="base.roles === 'editor'"> <span v-if="base.roles === 'editor'">
Anyone on the internet with this link can edit Anyone on the internet with this link can edit
@ -101,28 +96,26 @@
</span> </span>
<span v-else-if="base.roles === 'viewer'"> <span v-else-if="base.roles === 'viewer'">
<!-- Anyone on the internet with this link can view --> <!-- Anyone on the internet with this link can view -->
{{ $t('msg.info.shareBasePublic') }} {{ $t("msg.info.shareBasePublic") }}
</span> </span>
</template> </template>
<template v-else> <template v-else>
<!-- Generate publicly shareable readonly base --> <!-- Generate publicly shareable readonly base -->
{{ $t('msg.info.shareBasePrivate') }} {{ $t("msg.info.shareBasePrivate") }}
</template> </template>
</div> </div>
</div> </div>
<v-spacer /> <v-spacer />
<div class="d-flex justify-center" style="width:120px"> <div class="d-flex justify-center" style="width: 120px">
<v-menu v-if="base.uuid" offset-y> <v-menu v-if="base.uuid" offset-y>
<template #activator="{on}"> <template #activator="{ on }">
<div <div
class="text-capitalize my-2 font-weight-bold backgroundColorDefault py-2 px-4 rounded nc-shared-base-role" class="text-capitalize my-2 font-weight-bold backgroundColorDefault py-2 px-4 rounded nc-shared-base-role"
v-on="on" v-on="on"
> >
{{ base.roles || 'Viewer' }} {{ base.roles || "Viewer" }}
<v-icon small> <v-icon small> mdi-menu-down-outline </v-icon>
mdi-menu-down-outline
</v-icon>
</div> </div>
</template> </template>
@ -130,13 +123,13 @@
<v-list-item @click="createSharedBase('editor')"> <v-list-item @click="createSharedBase('editor')">
<v-list-item-title> <v-list-item-title>
<!-- Editor --> <!-- Editor -->
{{ $t('objects.roleType.editor') }} {{ $t("objects.roleType.editor") }}
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="createSharedBase('viewer')"> <v-list-item @click="createSharedBase('viewer')">
<v-list-item-title> <v-list-item-title>
<!-- Viewer --> <!-- Viewer -->
{{ $t('objects.roleType.viewer') }} {{ $t("objects.roleType.viewer") }}
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -148,79 +141,93 @@
</template> </template>
<script> <script>
import colors from '~/mixins/colors' import colors from "~/mixins/colors";
import { copyTextToClipboard } from '~/helpers/xutils' import { copyTextToClipboard } from "~/helpers/xutils";
export default { export default {
name: 'ShareBase', name: "ShareBase",
mixins: [colors], mixins: [colors],
data: () => ({ data: () => ({
base: { base: {
enable: false enable: false,
} },
}), }),
computed: { computed: {
url() { url() {
return this.base && this.base.uuid ? `${this.dashboardUrl}#/nc/base/${this.base.uuid}` : null return this.base && this.base.uuid
} ? `${this.dashboardUrl}#/nc/base/${this.base.uuid}`
: null;
},
}, },
mounted() { mounted() {
this.loadSharedBase() this.loadSharedBase();
}, },
methods: { methods: {
async loadSharedBase() { async loadSharedBase() {
try { try {
// const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [ // const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: 'db' }, 'getSharedBaseLink']) // { dbAlias: 'db' }, 'getSharedBaseLink'])
const sharedBase = (await this.$api.project.sharedBaseGet(this.$store.state.project.projectId)) const sharedBase = await this.$api.project.sharedBaseGet(
this.$store.state.project.projectId
);
this.base = sharedBase || {} this.base = sharedBase || {};
} catch (e) { } catch (e) {
console.log(e) console.log(e);
} }
}, },
async createSharedBase(roles = 'viewer') { async createSharedBase(roles = "viewer") {
try { try {
// const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'createSharedBaseLink', { roles }]) // const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ dbAlias: 'db' }, 'createSharedBaseLink', { roles }])
const sharedBase = (await this.$api.project.sharedBaseUpdate(this.$store.state.project.projectId, { roles })) const sharedBase = await this.$api.project.sharedBaseUpdate(
this.$store.state.project.projectId,
{ roles }
);
this.base = sharedBase || {} this.base = sharedBase || {};
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit(`shared-base:enable:${roles}`) this.$e("a:shared-base:enable", { role: roles });
}, },
async disableSharedBase() { async disableSharedBase() {
try { try {
await this.$api.project.sharedBaseDisable(this.$store.state.project.projectId) await this.$api.project.sharedBaseDisable(
this.base = {} this.$store.state.project.projectId
);
this.base = {};
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit('shared-base:disable') this.$e("a:shared-base:disable");
}, },
async recreate() { async recreate() {
try { try {
const sharedBase = (await this.$api.project.sharedBaseCreate(this.$store.state.project.projectId, { roles: this.base.roles || 'viewer' })) const sharedBase = await this.$api.project.sharedBaseCreate(
this.base = sharedBase || {} this.$store.state.project.projectId,
{ roles: this.base.roles || "viewer" }
);
this.base = sharedBase || {};
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit('shared-base:recreate') this.$e("a:shared-base:recreate");
}, },
copyUrl() { copyUrl() {
copyTextToClipboard(this.url) copyTextToClipboard(this.url);
this.$toast.success('Copied shareable base url to clipboard!').goAway(3000) this.$toast
.success("Copied shareable base url to clipboard!")
.goAway(3000);
this.$tele.emit('shared-base:copy-url') this.$e("c:shared-base:copy-url");
}, },
navigateToSharedBase() { navigateToSharedBase() {
window.open(this.url, '_blank') window.open(this.url, "_blank");
this.$tele.emit('shared-base:open-url') this.$e("c:shared-base:open-url");
}, },
generateEmbeddableIframe() { generateEmbeddableIframe() {
copyTextToClipboard(`<iframe copyTextToClipboard(`<iframe
@ -229,20 +236,19 @@ src="${this.url}?embed"
frameborder="0" frameborder="0"
width="100%" width="100%"
height="700" height="700"
style="background: transparent; border: 1px solid #ddd"></iframe>`) style="background: transparent; border: 1px solid #ddd"></iframe>`);
this.$toast.success('Copied embeddable html code!').goAway(3000) this.$toast.success("Copied embeddable html code!").goAway(3000);
this.$tele.emit('shared-base:copy-embed-frame')
}
}
} this.$e("c:shared-base:copy-embed-frame");
},
},
};
</script> </script>
<style scoped> <style scoped>
.nc-url-wrapper { .nc-url-wrapper {
column-gap: 15px; column-gap: 15px;
width: 100% width: 100%;
} }
.nc-url { .nc-url {
@ -259,6 +265,6 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
} }
/deep/ .nc-url-chip .v-chip__content { /deep/ .nc-url-chip .v-chip__content {
width: 100% width: 100%;
} }
</style> </style>

1548
packages/nc-gui/components/createOrEditProject.vue

File diff suppressed because it is too large Load Diff

102
packages/nc-gui/components/previewAs.vue

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<v-menu offset-y> <v-menu offset-y>
<template #activator="{on}"> <template #activator="{ on }">
<v-btn <v-btn
v-show="isDashboard && _isUIAllowed('previewAs')" v-show="isDashboard && _isUIAllowed('previewAs')"
small small
@ -10,17 +10,13 @@
class="white--text nc-btn-preview" class="white--text nc-btn-preview"
v-on="on" v-on="on"
> >
<v-icon small class="mr-1"> <v-icon small class="mr-1"> mdi-play-circle </v-icon>
mdi-play-circle
</v-icon>
Preview Preview
<v-icon small> <v-icon small> mdi-menu-down </v-icon>
mdi-menu-down
</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list dense> <v-list dense>
<template v-for="(role) in rolesList"> <template v-for="role in rolesList">
<v-list-item <v-list-item
:key="role.title" :key="role.title"
:class="`pointer nc-preview-${role.title}`" :class="`pointer nc-preview-${role.title}`"
@ -37,7 +33,8 @@
<span <span
class="caption text-capitalize" class="caption text-capitalize"
:class="{ 'x-active--text': role.title === previewAs }" :class="{ 'x-active--text': role.title === previewAs }"
>{{ role.title }}</span> >{{ role.title }}</span
>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</template> </template>
@ -45,11 +42,11 @@
<template v-if="previewAs"> <template v-if="previewAs">
<!-- <v-divider></v-divider>--> <!-- <v-divider></v-divider>-->
<v-list-item @click="setPreviewUser(null)"> <v-list-item @click="setPreviewUser(null)">
<v-icon small class="mr-1"> <v-icon small class="mr-1"> mdi-close </v-icon>
mdi-close
</v-icon>
<!-- Reset Preview --> <!-- Reset Preview -->
<span class="caption nc-preview-reset">{{ $t('activity.resetReview') }}</span> <span class="caption nc-preview-reset">{{
$t("activity.resetReview")
}}</span>
</v-list-item> </v-list-item>
</template> </template>
</v-list> </v-list>
@ -63,7 +60,10 @@
:close-on-click="false" :close-on-click="false"
:close-on-content-click="false" :close-on-content-click="false"
> >
<div class="floating-reset-btn white py-1 pr-3 caption primary lighten-2 white--text font-weight-bold d-flex align-center nc-floating-preview-btn" style="overflow-y: hidden"> <div
class="floating-reset-btn white py-1 pr-3 caption primary lighten-2 white--text font-weight-bold d-flex align-center nc-floating-preview-btn"
style="overflow-y: hidden"
>
<v-icon style="cursor: move" color="white" @mousedown="mouseDown"> <v-icon style="cursor: move" color="white" @mousedown="mouseDown">
mdi-drag mdi-drag
</v-icon> </v-icon>
@ -81,7 +81,7 @@
@change="setPreviewUser($event)" @change="setPreviewUser($event)"
> >
<v-radio <v-radio
v-for="(role) in rolesList" v-for="role in rolesList"
:key="role.title" :key="role.title"
:value="role.title" :value="role.title"
color="white" color="white"
@ -89,12 +89,16 @@
:class="`ml-1 nc-floating-preview-${role.title}`" :class="`ml-1 nc-floating-preview-${role.title}`"
> >
<template #label> <template #label>
<span class="white--text caption text-capitalize">{{ role.title }}</span> <span class="white--text caption text-capitalize">{{
role.title
}}</span>
</template> </template>
</v-radio> </v-radio>
</v-radio-group> </v-radio-group>
<v-divider vertical class="mr-2" /> <v-divider vertical class="mr-2" />
<span class="pointer" @click="setPreviewUser(null)"> <v-icon small color="white">mdi-exit-to-app</v-icon> Exit</span> <span class="pointer" @click="setPreviewUser(null)">
<v-icon small color="white">mdi-exit-to-app</v-icon> Exit</span
>
</div> </div>
</div> </div>
</v-menu> </v-menu>
@ -103,65 +107,69 @@
<script> <script>
export default { export default {
name: 'PreviewAs', name: "PreviewAs",
data: () => ({ data: () => ({
roleIcon: { roleIcon: {
owner: 'mdi-account-star', owner: "mdi-account-star",
creator: 'mdi-account-hard-hat', creator: "mdi-account-hard-hat",
editor: 'mdi-account-edit', editor: "mdi-account-edit",
viewer: 'mdi-eye-outline', viewer: "mdi-eye-outline",
commenter: 'mdi-comment-account-outline' commenter: "mdi-comment-account-outline",
}, },
rolesList: [{ title: 'editor' }, { title: 'commenter' }, { title: 'viewer' }], rolesList: [
{ title: "editor" },
{ title: "commenter" },
{ title: "viewer" },
],
position: { position: {
x: 9999, y: 9999 x: 9999,
} y: 9999,
},
}), }),
computed: { computed: {
previewAs: { previewAs: {
get() { get() {
return this.$store.state.users.previewAs return this.$store.state.users.previewAs;
}, },
set(previewAs) { set(previewAs) {
this.$store.commit('users/MutPreviewAs', previewAs) this.$store.commit("users/MutPreviewAs", previewAs);
} },
} },
}, },
mounted() { mounted() {
this.position = { this.position = {
y: window.innerHeight - 100, y: window.innerHeight - 100,
x: window.innerWidth / 2 - 250 x: window.innerWidth / 2 - 250,
} };
window.addEventListener('mouseup', this.mouseUp, false) window.addEventListener("mouseup", this.mouseUp, false);
}, },
beforeDestroy() { beforeDestroy() {
window.removeEventListener('mousemove', this.divMove, true) window.removeEventListener("mousemove", this.divMove, true);
window.removeEventListener('mouseup', this.mouseUp, false) window.removeEventListener("mouseup", this.mouseUp, false);
}, },
methods: { methods: {
setPreviewUser(previewAs) { setPreviewUser(previewAs) {
this.$tele.emit(`preview-as:${previewAs}`) this.$e("a:navdraw:preview", { role: previewAs });
if (!process.env.EE) { if (!process.env.EE) {
this.$toast.info('Available in Enterprise edition').goAway(3000) this.$toast.info("Available in Enterprise edition").goAway(3000);
} else { } else {
this.previewAs = previewAs this.previewAs = previewAs;
window.location.reload() window.location.reload();
} }
}, },
mouseUp() { mouseUp() {
window.removeEventListener('mousemove', this.divMove, true) window.removeEventListener("mousemove", this.divMove, true);
}, },
mouseDown(e) { mouseDown(e) {
window.addEventListener('mousemove', this.divMove, true) window.addEventListener("mousemove", this.divMove, true);
}, },
divMove(e) { divMove(e) {
this.position = { y: e.clientY - 10, x: e.clientX - 18 } this.position = { y: e.clientY - 10, x: e.clientX - 18 };
} },
} },
} };
</script> </script>
<style scoped> <style scoped></style>
</style>

128
packages/nc-gui/components/project/appStore.vue

@ -5,19 +5,31 @@
</h3> </h3>
<v-divider /> <v-divider />
<div class="d-flex h-100 nc-app-store-tab mt-5"> <div class="d-flex h-100 nc-app-store-tab mt-5">
<v-dialog v-model="pluginInstallOverlay" min-width="400px" max-width="700px" min-height="300"> <v-dialog
v-model="pluginInstallOverlay"
min-width="400px"
max-width="700px"
min-height="300"
>
<v-card <v-card
v-if="installPlugin && pluginInstallOverlay" v-if="installPlugin && pluginInstallOverlay"
:dark="$store.state.windows.darkTheme" :dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme" :light="!$store.state.windows.darkTheme"
> >
<app-install :id="installPlugin.id" :default-config="defaultConfig" @close="pluginInstallOverlay = false" @saved="saved()" /> <app-install
:id="installPlugin.id"
:default-config="defaultConfig"
@close="pluginInstallOverlay = false"
@saved="saved()"
/>
</v-card> </v-card>
</v-dialog> </v-dialog>
<dlg-ok-new <dlg-ok-new
v-model="pluginUninstallModal" v-model="pluginUninstallModal"
:heading="`Please click on submit to reset ${resetPluginRef && resetPluginRef.title}`" :heading="`Please click on submit to reset ${
resetPluginRef && resetPluginRef.title
}`"
ok-label="Submit" ok-label="Submit"
type="primary" type="primary"
@ok="confirmResetPlugin" @ok="confirmResetPlugin"
@ -29,7 +41,9 @@
:dark="$store.state.windows.darkTheme" :dark="$store.state.windows.darkTheme"
:light="!$store.state.windows.darkTheme" :light="!$store.state.windows.darkTheme"
> >
<v-card-text> Please confirm to reset {{ resetPluginRef.title }}</v-card-text> <v-card-text>
Please confirm to reset {{ resetPluginRef.title }}
</v-card-text>
<v-card-actions> <v-card-actions>
<v-btn color="primary" @click="confirmResetPlugin"> <v-btn color="primary" @click="confirmResetPlugin">
Yes Yes
@ -43,25 +57,22 @@
<v-container class="h-100 app-container"> <v-container class="h-100 app-container">
<v-row class="d-flex align-stretch"> <v-row class="d-flex align-stretch">
<v-col v-for="(app,i) in filteredApps" :key="i" class="" cols="6"> <v-col v-for="(app, i) in filteredApps" :key="i" class="" cols="6">
<!-- @click="installApp(app)"--> <!-- @click="installApp(app)"-->
<v-card <v-card height="100%" class="elevatio app-item-card">
height="100%" <div class="install-btn">
class="elevatio app-item-card "
>
<div class="install-btn ">
<v-btn <v-btn
v-if="app.parsedInput" v-if="app.parsedInput"
x-small x-small
outlined outlined
class=" caption text-capitalize" class="caption text-capitalize"
@click="installApp(app)" @click="installApp(app)"
> >
<v-icon x-small class="mr-1"> <v-icon x-small class="mr-1">
mdi-pencil mdi-pencil
</v-icon> </v-icon>
{{ $t('general.edit') }} {{ $t("general.edit") }}
</v-btn> </v-btn>
<v-btn <v-btn
v-if="app.parsedInput" v-if="app.parsedInput"
@ -75,7 +86,13 @@
</v-icon> </v-icon>
Reset Reset
</v-btn> </v-btn>
<v-btn v-else x-small outlined class=" caption text-capitalize" @click="installApp(app)"> <v-btn
v-else
x-small
outlined
class="caption text-capitalize"
@click="installApp(app)"
>
<v-icon x-small class="mr-1"> <v-icon x-small class="mr-1">
mdi-plus mdi-plus
</v-icon> </v-icon>
@ -96,36 +113,35 @@
</v-icon> </v-icon>
</v-avatar> </v-avatar>
<div class="flex-grow-1"> <div class="flex-grow-1">
<v-card-title <v-card-title class="title" v-text="app.title" />
class="title "
v-text="app.title"
/>
<v-card-subtitle class="pb-1" v-text="app.description" /> <v-card-subtitle class="pb-1" v-text="app.description" />
<v-card-actions> <v-card-actions>
<div class="d-flex justify-space-between d-100 align-center"> <div
<!-- <v-rating--> class="d-flex justify-space-between d-100 align-center"
<!-- full-icon="mdi-star"--> >
<!-- readonly--> <!-- <v-rating-->
<!-- length="5"--> <!-- full-icon="mdi-star"-->
<!-- size="15"--> <!-- readonly-->
<!-- :value="5"--> <!-- length="5"-->
<!-- />--> <!-- size="15"-->
<!-- :value="5"-->
<!-- />-->
<!-- <span class="subtitles" v-if="app.price && app.price !== 'Free'">${{ app.price }} / mo</span>--> <!-- <span class="subtitles" v-if="app.price && app.price !== 'Free'">${{ app.price }} / mo</span>-->
<!-- <span class="subtitles" v-else>Free</span>--> <!-- <span class="subtitles" v-else>Free</span>-->
</div> </div>
</v-card-actions> </v-card-actions>
<!-- <v-card-actions>--> <!-- <v-card-actions>-->
<!-- <v-btn--> <!-- <v-btn-->
<!-- outlined--> <!-- outlined-->
<!-- rounded--> <!-- rounded-->
<!-- small--> <!-- small-->
<!-- >--> <!-- >-->
<!-- Download--> <!-- Download-->
<!-- </v-btn>--> <!-- </v-btn>-->
<!-- </v-card-actions>--> <!-- </v-card-actions>-->
</div> </div>
</div> </div>
</v-card> </v-card>
@ -203,11 +219,20 @@ export default {
}), }),
computed: { computed: {
filters() { filters() {
return this.apps.reduce((arr, app) => arr.concat(app.tags || []), []).filter((f, i, arr) => i === arr.indexOf(f)).sort() return this.apps
.reduce((arr, app) => arr.concat(app.tags || []), [])
.filter((f, i, arr) => i === arr.indexOf(f))
.sort()
}, },
filteredApps() { filteredApps() {
return this.apps.filter(app => (!this.query.trim() || app.title.toLowerCase().includes(this.query.trim().toLowerCase())) && return this.apps.filter(
(!this.selectedTags.length || this.selectedTags.some(t => app.tags && app.tags.includes(t))) app =>
(!this.query.trim() ||
app.title
.toLowerCase()
.includes(this.query.trim().toLowerCase())) &&
(!this.selectedTags.length ||
this.selectedTags.some(t => app.tags && app.tags.includes(t)))
) )
} }
}, },
@ -227,18 +252,18 @@ export default {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000)
} }
this.$tele.emit(`appstore:reset:${this.resetPluginRef.title}`) this.$e('a:appstore:reset', { app: this.resetPluginRef.title })
}, },
async saved() { async saved() {
this.pluginInstallOverlay = false this.pluginInstallOverlay = false
await this.loadPluginList() await this.loadPluginList()
this.$tele.emit(`appstore:install:submit:${this.installPlugin.title}`) this.$e('a:appstore:install', { app: this.installPlugin.title })
}, },
async installApp(app) { async installApp(app) {
this.pluginInstallOverlay = true this.pluginInstallOverlay = true
this.installPlugin = app this.installPlugin = app
this.$tele.emit(`appstore:install:trigger:${app.title}`) this.$e('c:appstore:install', { app: app.title })
}, },
async resetApp(app) { async resetApp(app) {
this.pluginUninstallModal = true this.pluginUninstallModal = true
@ -254,18 +279,15 @@ export default {
p.parsedInput = p.input && JSON.parse(p.input) p.parsedInput = p.input && JSON.parse(p.input)
return p return p
}) })
} catch (e) { } catch (e) {}
}
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.app-item-card { .app-item-card {
transition: .4s background-color; transition: 0.4s background-color;
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
@ -274,7 +296,7 @@ export default {
opacity: 0; opacity: 0;
right: -100%; right: -100%;
top: 10px; top: 10px;
transition: .4s opacity, .4s right; transition: 0.4s opacity, 0.4s right;
} }
&:hover .install-btn { &:hover .install-btn {
@ -284,7 +306,7 @@ export default {
} }
.app-item-card { .app-item-card {
transition: .4s background-color, .4s transform; transition: 0.4s background-color, 0.4s transform;
&:hover { &:hover {
background: rgba(123, 126, 136, 0.1) !important; background: rgba(123, 126, 136, 0.1) !important;
@ -303,14 +325,15 @@ export default {
} }
.v-input__control .v-input__slot .v-input--selection-controls__input { .v-input__control .v-input__slot .v-input--selection-controls__input {
transform: scale(.75); transform: scale(0.75);
} }
.v-input--selection-controls .v-input__slot > .v-label { .v-input--selection-controls .v-input__slot > .v-label {
font-size: .8rem; font-size: 0.8rem;
} }
.search-field.v-text-field > .v-input__control, .search-field.v-text-field > .v-input__control > .v-input__slot { .search-field.v-text-field > .v-input__control,
.search-field.v-text-field > .v-input__control > .v-input__slot {
min-height: auto; min-height: auto;
} }
} }
@ -319,7 +342,6 @@ export default {
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
} }
</style> </style>
<!-- <!--
/** /**

130
packages/nc-gui/components/project/projectMetadata/sync/metaDiffSync.vue

@ -16,7 +16,7 @@
class="my-2 mx-auto caption" class="my-2 mx-auto caption"
:placeholder="$t('placeholder.searchModels')" :placeholder="$t('placeholder.searchModels')"
prepend-inner-icon="search" prepend-inner-icon="search"
style="max-width:500px" style="max-width: 500px"
outlined outlined
/> />
@ -31,7 +31,7 @@
@click="clickReload" @click="clickReload"
> >
<!-- Reload --> <!-- Reload -->
{{ $t('general.reload') }} {{ $t("general.reload") }}
</x-btn> </x-btn>
<!-- <x-btn <!-- <x-btn
outlined outlined
@ -63,12 +63,12 @@
<tr> <tr>
<th class="grey--text"> <th class="grey--text">
<!--Models--> <!--Models-->
{{ $t('labels.models') }} {{ $t("labels.models") }}
</th> </th>
<!-- <th>APIs</th>--> <!-- <th>APIs</th>-->
<th class="grey--text"> <th class="grey--text">
<!--Sync state--> <!--Sync state-->
{{ $t('labels.syncState') }} {{ $t("labels.syncState") }}
</th> </th>
<th /> <th />
</tr> </tr>
@ -76,7 +76,12 @@
<tbody> <tbody>
<tr <tr
v-for="model in diff" v-for="model in diff"
v-show="!filter.trim() || (model.table_name || model.title || '').toLowerCase().includes(filter.toLowerCase())" v-show="
!filter.trim() ||
(model.table_name || model.title || '')
.toLowerCase()
.includes(filter.toLowerCase())
"
:key="model.table_name" :key="model.table_name"
:class="`nc-metasync-row nc-metasync-row-${model.table_name}`" :class="`nc-metasync-row nc-metasync-row-${model.table_name}`"
> >
@ -86,8 +91,11 @@
{{ viewIcons[model.type === 'table' ? 'grid' : 'view'].icon }} {{ viewIcons[model.type === 'table' ? 'grid' : 'view'].icon }}
</v-icon>--> </v-icon>-->
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{on}"> <template #activator="{ on }">
<span v-on="on">{{ model.table_name && model.table_name.slice(prefix.length) }}</span> <span v-on="on">{{
model.table_name &&
model.table_name.slice(prefix.length)
}}</span>
</template> </template>
<span class="caption">{{ model.title }}</span> <span class="caption">{{ model.title }}</span>
</v-tooltip> </v-tooltip>
@ -112,18 +120,19 @@
<td> <td>
<span <span
v-if="model.detectedChanges && model.detectedChanges.length" v-if="
model.detectedChanges && model.detectedChanges.length
"
class="caption error--text" class="caption error--text"
>{{ model.detectedChanges.map(m => m.msg).join(', ') }}</span> >{{
<span model.detectedChanges.map((m) => m.msg).join(", ")
v-else }}</span
class="caption grey--text"
> >
<span v-else class="caption grey--text">
<!--{{ 'No change identified' }}--> <!--{{ 'No change identified' }}-->
{{ $t('msg.info.metaNoChange') }} {{ $t("msg.info.metaNoChange") }}
</span> </span>
<!-- <span v-else class="caption grey&#45;&#45;text">Recreate metadata.</span>--> <!-- <span v-else class="caption grey&#45;&#45;text">Recreate metadata.</span>-->
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -200,7 +209,7 @@
</div> </div>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="4" style="padding-top:100px"> <v-col cols="4" style="padding-top: 100px">
<div class="d-flex"> <div class="d-flex">
<v-spacer /> <v-spacer />
@ -244,7 +253,7 @@
<div class="d-flex justify-center"> <div class="d-flex justify-center">
<v-btn <v-btn
v-if="isChanged" v-if="isChanged"
v-t="['proj-meta:metadata:metasync']" v-t="['a:proj-meta:meta-data:sync']"
x-large x-large
class="mx-auto primary nc-btn-metasync-sync-now" class="mx-auto primary nc-btn-metasync-sync-now"
@click="syncMetaDiff" @click="syncMetaDiff"
@ -255,12 +264,7 @@
Sync Now Sync Now
</v-btn> </v-btn>
<v-alert <v-alert v-else dense outlined type="success">
v-else
dense
outlined
type="success"
>
Tables metadata is in sync Tables metadata is in sync
</v-alert> </v-alert>
</div> </div>
@ -271,29 +275,32 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
import viewIcons from '~/helpers/viewIcons' import viewIcons from "~/helpers/viewIcons";
export default { export default {
name: 'DisableOrEnableTables', name: "DisableOrEnableTables",
props: ['nodes', 'db'], props: ["nodes", "db"],
data: () => ({ data: () => ({
viewIcons, viewIcons,
edited: false, edited: false,
models: null, models: null,
updating: false, updating: false,
dbsTab: 0, dbsTab: 0,
filter: '', filter: "",
tables: null, tables: null,
diff: null diff: null,
}), }),
async mounted() { async mounted() {
await this.loadXcDiff() await this.loadXcDiff();
// await this.loadMode// await this.loadTableList() // await this.loadMode// await this.loadTableList()
}, },
methods: { methods: {
async loadXcDiff() { async loadXcDiff() {
this.diff = (await this.$api.project.metaDiffGet(this.$store.state.project.projectId, this.db.id)) this.diff = await this.$api.project.metaDiffGet(
this.$store.state.project.projectId,
this.db.id
);
// this.diff = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ // this.diff = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias, // dbAlias: this.db.meta.dbAlias,
@ -301,8 +308,8 @@ export default {
// }, 'xcMetaDiff']) // }, 'xcMetaDiff'])
}, },
clickReload() { clickReload() {
this.loadXcDiff() this.loadXcDiff();
this.$tele.emit('proj-meta:metadata:reload') this.$e("a:proj-meta:meta-data:reload");
}, },
/* async addTableMeta(tables) { /* async addTableMeta(tables) {
try { try {
@ -349,30 +356,37 @@ export default {
*/ */
async syncMetaDiff() { async syncMetaDiff() {
try { try {
await this.$api.project.metaDiffSync(this.$store.state.project.projectId, this.db.id) await this.$api.project.metaDiffSync(
this.$store.state.project.projectId,
this.db.id
);
// await this.$store.dispatch('sqlMgr/ActSqlOp', [{ // await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias, // dbAlias: this.db.meta.dbAlias,
// env: this.$store.getters['project/GtrEnv'] // env: this.$store.getters['project/GtrEnv']
// }, 'xcMetaDiffSync', {}]) // }, 'xcMetaDiffSync', {}])
this.$toast.success('Table metadata recreated successfully').goAway(3000) this.$toast
await this.loadXcDiff() .success("Table metadata recreated successfully")
.goAway(3000);
await this.loadXcDiff();
this.$store.commit('tabs/removeTableOrViewTabs') this.$store.commit("tabs/removeTableOrViewTabs");
await this.$nextTick() await this.$nextTick();
await this.$store.dispatch('project/_loadTables', { await this.$store.dispatch("project/_loadTables", {
dbKey: '0.projectJson.envs._noco.db.0', dbKey: "0.projectJson.envs._noco.db.0",
key: '0.projectJson.envs._noco.db.0.tables', key: "0.projectJson.envs._noco.db.0.tables",
_nodes: { _nodes: {
dbAlias: 'db', dbAlias: "db",
env: '_noco', env: "_noco",
type: 'tableDir' type: "tableDir",
} },
}) });
await this.$store.commit('meta/MutClear') await this.$store.commit("meta/MutClear");
} catch (e) { } catch (e) {
this.$toast[e.response?.status === 402 ? 'info' : 'error'](e.message).goAway(3000) this.$toast[e.response?.status === 402 ? "info" : "error"](
e.message
).goAway(3000);
} }
} },
/* async recreateTableMeta(table) { /* async recreateTableMeta(table) {
try { try {
@ -424,14 +438,19 @@ export default {
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
dbAliasList: 'project/GtrDbAliasList' dbAliasList: "project/GtrDbAliasList",
}), }),
isChanged() { isChanged() {
return this.diff && this.diff.some(d => d && d.detectedChanges && d.detectedChanges.length) return (
this.diff &&
this.diff.some(
(d) => d && d.detectedChanges && d.detectedChanges.length
)
);
}, },
prefix() { prefix() {
return this.$store.getters['project/GtrProjectPrefix'] || '' return this.$store.getters["project/GtrProjectPrefix"] || "";
} },
/* enableCountText() { /* enableCountText() {
return this.models return this.models
? `${this.models.filter(m => m.enabled).length}/${this.models.length} enabled` ? `${this.models.filter(m => m.enabled).length}/${this.models.length} enabled`
@ -470,8 +489,8 @@ export default {
res.sort((a, b) => getPriority(b) - getPriority(a)) res.sort((a, b) => getPriority(b) - getPriority(a))
return res return res
} */ } */
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -484,7 +503,6 @@ export default {
border-right: 1px solid #7f828b33; border-right: 1px solid #7f828b33;
} }
} }
</style> </style>
<!-- <!--
/** /**

142
packages/nc-gui/components/project/projectMetadata/uiAcl/toggleTableUIAcl.vue

@ -10,13 +10,11 @@
hide-details hide-details
class="my-2 mx-auto search-field" class="my-2 mx-auto search-field"
placeholder="Search models" placeholder="Search models"
style="max-width:300px" style="max-width: 300px"
outlined outlined
> >
<template #prepend-inner> <template #prepend-inner>
<v-icon small> <v-icon small> search </v-icon>
search
</v-icon>
</template> </template>
</v-text-field> </v-text-field>
<v-spacer /> <v-spacer />
@ -43,7 +41,7 @@
@click="save()" @click="save()"
> >
<!-- Save --> <!-- Save -->
{{ $t('general.save') }} {{ $t("general.save") }}
</x-btn> </x-btn>
</v-toolbar> </v-toolbar>
@ -53,67 +51,92 @@
<tr> <tr>
<th class="caption" width="100px"> <th class="caption" width="100px">
<!--TableName--> <!--TableName-->
{{ $t('labels.tableName') }} {{ $t("labels.tableName") }}
</th> </th>
<th class="caption" width="150px"> <th class="caption" width="150px">
<!--ViewName--> <!--ViewName-->
{{ $t('labels.viewName') }} {{ $t("labels.viewName") }}
</th> </th>
<th v-for="role in roles" :key="role" class="caption" width="100px"> <th
v-for="role in roles"
:key="role"
class="caption"
width="100px"
>
{{ role.charAt(0).toUpperCase() + role.slice(1) }} {{ role.charAt(0).toUpperCase() + role.slice(1) }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template <template v-for="table in tables">
v-for="table in tables"
>
<tr <tr
v-if="table.title.toLowerCase().indexOf(filter.toLowerCase()) > -1" v-if="
table.title.toLowerCase().indexOf(filter.toLowerCase()) > -1
"
:key="table.table_name" :key="table.table_name"
:class="`nc-acl-table-row nc-acl-table-row-${table.title}`" :class="`nc-acl-table-row nc-acl-table-row-${table.title}`"
> >
<td> <td>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{on}"> <template #activator="{ on }">
<span <span class="caption ml-2" v-on="on">{{
class="caption ml-2" table.ptype === "table"
v-on="on" ? table._ptn
>{{ table.ptype === 'table' ? table._ptn : table.ptype === 'view' ? table._ptn : table._ptn }}</span> : table.ptype === "view"
? table._ptn
: table._ptn
}}</span>
</template> </template>
<span class="caption">{{ table.ptn || table._ptn }}</span> <span class="caption">{{ table.ptn || table._ptn }}</span>
</v-tooltip> </v-tooltip>
</td> </td>
<td> <td>
<v-icon small :color="viewIcons[table.type].color" v-on="on"> <v-icon
small
:color="viewIcons[table.type].color"
v-on="on"
>
{{ viewIcons[table.type].icon }} {{ viewIcons[table.type].icon }}
</v-icon> </v-icon>
<span v-if="table.ptn" class="caption">{{ table.title }}</span> <span v-if="table.ptn" class="caption">{{
<span v-else class="caption">{{ $t('general.default') }}</span> table.title
}}</span>
<span v-else class="caption">{{
$t("general.default")
}}</span>
<!-- {{ table.show_as || table.type }}--> <!-- {{ table.show_as || table.type }}-->
</td> </td>
<td v-for="role in roles" :key="`${table.table_name}-${role}`"> <td
v-for="role in roles"
:key="`${table.table_name}-${role}`"
>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{on}"> <template #activator="{ on }">
<div <div v-on="on">
v-on="on"
>
<v-checkbox <v-checkbox
v-model="table.disabled[role]" v-model="table.disabled[role]"
:class="`pt-0 mt-0 nc-acl-${table.title.toLowerCase().replace('_','')}-${role}-chkbox`" :class="`pt-0 mt-0 nc-acl-${table.title
.toLowerCase()
.replace('_', '')}-${role}-chkbox`"
dense dense
hide-details hide-details
:true-value="false" :true-value="false"
:false-value="true" :false-value="true"
@change="$set(table,'edited',true)" @change="$set(table, 'edited', true)"
/> />
</div> </div>
</template> </template>
<span v-if="table.disabled[role]">Click to make '{{ table.table_name }}' visible for Role:{{ <span v-if="table.disabled[role]"
role >Click to make '{{ table.table_name }}' visible for
}} in UI dashboard</span> Role:{{ role }} in UI dashboard</span
<span v-else>Click to hide '{{ table.table_name }}' for Role:{{ role }} in UI dashboard</span> >
<span v-else
>Click to hide '{{ table.table_name }}' for Role:{{
role
}}
in UI dashboard</span
>
</v-tooltip> </v-tooltip>
</td> </td>
</tr> </tr>
@ -128,30 +151,32 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex' import { mapGetters } from "vuex";
import viewIcons from '~/helpers/viewIcons' import viewIcons from "~/helpers/viewIcons";
export default { export default {
name: 'ToggleTableUiAcl', name: "ToggleTableUiAcl",
components: {}, components: {},
props: ['nodes', 'db'], props: ["nodes", "db"],
data: () => ({ data: () => ({
viewIcons, viewIcons,
models: null, models: null,
updating: false, updating: false,
dbsTab: 0, dbsTab: 0,
filter: '', filter: "",
tables: null tables: null,
}), }),
async mounted() { async mounted() {
await this.loadTableList() await this.loadTableList();
}, },
methods: { methods: {
async loadTableList() { async loadTableList() {
this.tables = (await this.$api.project.modelVisibilityList( this.tables = await this.$api.project.modelVisibilityList(
this.db.project_id, { this.db.project_id,
includeM2M: this.$store.state.windows.includeM2M || '' {
})) includeM2M: this.$store.state.windows.includeM2M || "",
}
);
// this.tables = (await this.$store.dispatch('sqlMgr/ActSqlOp', [{ // this.tables = (await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// dbAlias: this.db.meta.dbAlias, // dbAlias: this.db.meta.dbAlias,
// env: this.$store.getters['project/GtrEnv'] // env: this.$store.getters['project/GtrEnv']
@ -161,27 +186,34 @@ export default {
}, },
async save() { async save() {
try { try {
await this.$api.project.modelVisibilitySet(this.db.project_id, this.tables.filter(t => t.edited)) await this.$api.project.modelVisibilitySet(
this.$toast.success('Updated UI ACL for tables successfully').goAway(3000) this.db.project_id,
this.tables.filter((t) => t.edited)
);
this.$toast
.success("Updated UI ACL for tables successfully")
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit('proj-meta:ui-acl:update') this.$e("a:proj-meta:ui-acl");
} },
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
dbAliasList: 'project/GtrDbAliasList' dbAliasList: "project/GtrDbAliasList",
}), }),
edited() { edited() {
return this.tables && this.tables.length && this.tables.some(t => t.edited) return (
this.tables && this.tables.length && this.tables.some((t) => t.edited)
);
}, },
roles() { roles() {
return ['editor', 'commenter', 'viewer']// this.tables && this.tables.length ? Object.keys(this.tables[0].disabled) : [] return ["editor", "commenter", "viewer"]; // this.tables && this.tables.length ? Object.keys(this.tables[0].disabled) : []
} },
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -194,11 +226,11 @@ export default {
border-right: 1px solid #7f828b33; border-right: 1px solid #7f828b33;
} }
.search-field.v-text-field > .v-input__control, .search-field.v-text-field > .v-input__control > .v-input__slot { .search-field.v-text-field > .v-input__control,
.search-field.v-text-field > .v-input__control > .v-input__slot {
min-height: auto; min-height: auto;
} }
} }
</style> </style>
<!-- <!--
/** /**

7
packages/nc-gui/components/project/settings/appearance.vue

@ -15,7 +15,7 @@
<v-icon <v-icon
x-large x-large
:color="$vuetify.theme.dark ? 'primary':'primary'" :color="$vuetify.theme.dark ? 'primary':'primary'"
@click="toggleDarkTheme" @click="toggleDarkTheme($vuetify.theme.dark)"
v-on="on" v-on="on"
> >
mdi-bat mdi-bat
@ -67,6 +67,7 @@
<template #activator="{ on }"> <template #activator="{ on }">
<v-checkbox <v-checkbox
v-model="includeM2M" v-model="includeM2M"
v-t="[`c:themes:show-m2m-tables`]"
x-large x-large
color="primary" color="primary"
v-on="on" v-on="on"
@ -276,9 +277,11 @@ export default {
this.item = theme this.item = theme
if (theme === 'Custom') { await this.$store.dispatch('windows/ActSetTheme', { theme: { ...t }, custom: true }) } if (theme === 'Custom') { await this.$store.dispatch('windows/ActSetTheme', { theme: { ...t }, custom: true }) }
await this.$store.dispatch('windows/ActSetTheme', { theme: { ...t }, themeName: theme }) await this.$store.dispatch('windows/ActSetTheme', { theme: { ...t }, themeName: theme })
this.$e('c:themes:change', { mode: theme })
}, },
toggleDarkTheme() { toggleDarkTheme(mode) {
this.$store.commit('windows/MutToggleDarkMode') this.$store.commit('windows/MutToggleDarkMode')
this.$e('c:themes:dark-mode', { dark: mode })
} }
}, },
beforeCreated() { beforeCreated() {

264
packages/nc-gui/components/project/settings/xcMeta.vue

@ -10,11 +10,11 @@
<tr> <tr>
<td> <td>
<!-- Export project meta to zip file and download. --> <!-- Export project meta to zip file and download. -->
{{ $t('msg.info.exportZip') }} {{ $t("msg.info.exportZip") }}
</td> </td>
<td> <td>
<v-btn <v-btn
v-t="['proj-meta:export-zip:trigger']" v-t="['c:proj-meta:export']"
min-width="150" min-width="150"
color="primary" color="primary"
small small
@ -22,22 +22,20 @@
:loading="loading === 'export-zip'" :loading="loading === 'export-zip'"
@click="exportMetaZip()" @click="exportMetaZip()"
> >
<v-icon small> <v-icon small> mdi-export </v-icon>&nbsp;
mdi-export
</v-icon>&nbsp;
<!-- Export zip --> <!-- Export zip -->
{{ $t('activity.exportZip') }} {{ $t("activity.exportZip") }}
</v-btn> </v-btn>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<!-- Import project meta zip file and restart. --> <!-- Import project meta zip file and restart. -->
{{ $t('msg.info.importZip') }} {{ $t("msg.info.importZip") }}
</td> </td>
<td> <td>
<v-btn <v-btn
v-t="['proj-meta:import-zip']" v-t="['a:proj-meta:import']"
min-width="150" min-width="150"
:loading="loading === 'import-zip'" :loading="loading === 'import-zip'"
color="info" color="info"
@ -45,12 +43,10 @@
outlined outlined
@click="$refs.importFile.click()" @click="$refs.importFile.click()"
> >
<v-icon small> <v-icon small> mdi-import </v-icon>&nbsp;
mdi-import
</v-icon>&nbsp;
<!-- Import Zip --> <!-- Import Zip -->
{{ $t('activity.importZip') }} {{ $t("activity.importZip") }}
</v-btn> </v-btn>
<input <input
@ -59,31 +55,31 @@
type="file" type="file"
accept=".zip" accept=".zip"
@change="importMetaZip" @change="importMetaZip"
> />
</td> </td>
</tr> </tr>
<!-- <tr>--> <!-- <tr>-->
<!-- <td>--> <!-- <td>-->
<!-- &lt;!&ndash; Clear all metadata from meta tables. &ndash;&gt;--> <!-- &lt;!&ndash; Clear all metadata from meta tables. &ndash;&gt;-->
<!-- {{ $t('tooltip.clearMetadata') }}--> <!-- {{ $t('tooltip.clearMetadata') }}-->
<!-- </td>--> <!-- </td>-->
<!-- <td>--> <!-- <td>-->
<!-- <v-btn--> <!-- <v-btn-->
<!-- :loading="loading === 'reset-metadata'"--> <!-- :loading="loading === 'reset-metadata'"-->
<!-- min-width="150"--> <!-- min-width="150"-->
<!-- color="error"--> <!-- color="error"-->
<!-- small--> <!-- small-->
<!-- outlined--> <!-- outlined-->
<!-- @click="resetMeta"--> <!-- @click="resetMeta"-->
<!-- >--> <!-- >-->
<!-- <v-icon small>--> <!-- <v-icon small>-->
<!-- mdi-delete-variant--> <!-- mdi-delete-variant-->
<!-- </v-icon>&nbsp;--> <!-- </v-icon>&nbsp;-->
<!-- &lt;!&ndash; Reset &ndash;&gt;--> <!-- &lt;!&ndash; Reset &ndash;&gt;-->
<!-- {{ $t('general.reset') }}--> <!-- {{ $t('general.reset') }}-->
<!-- </v-btn>--> <!-- </v-btn>-->
<!-- </td>--> <!-- </td>-->
<!-- </tr>--> <!-- </tr>-->
</tbody> </tbody>
</v-simple-table> </v-simple-table>
@ -102,172 +98,188 @@
</template> </template>
<script> <script>
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel' import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
// import ImportTemplate from '~/components/project/settings/importTemplate' // import ImportTemplate from '~/components/project/settings/importTemplate'
export default { export default {
name: 'XcMeta', name: "XcMeta",
components: { components: {
// ImportTemplate, // ImportTemplate,
DlgLabelSubmitCancel DlgLabelSubmitCancel,
}, },
data: () => ({ data: () => ({
loading: null, loading: null,
dialogShow: false, dialogShow: false,
confirmAction: null, confirmAction: null,
confirmMessage: '' confirmMessage: "",
}), }),
methods: { methods: {
async exportMeta() { async exportMeta() {
this.dialogShow = true this.dialogShow = true;
// this.confirmMessage = 'Do you want to export metadata from meta tables?' // this.confirmMessage = 'Do you want to export metadata from meta tables?'
this.confirmMessage = `${this.$t('msg.info.exportMetadata')}` this.confirmMessage = `${this.$t("msg.info.exportMetadata")}`;
this.confirmAction = async(act) => { this.confirmAction = async (act) => {
if (act === 'hideDialog') { if (act === "hideDialog") {
this.dialogShow = false this.dialogShow = false;
} else { } else {
this.loading = 'export-file' this.loading = "export-file";
try { try {
// todo: set env based on `nodes` prop // todo: set env based on `nodes` prop
await this.$store.dispatch('sqlMgr/ActSqlOp', [ await this.$store.dispatch("sqlMgr/ActSqlOp", [
{ {
// dbAlias: 'db', // dbAlias: 'db',
env: '_noco' env: "_noco",
}, },
'xcMetaTablesExportDbToLocalFs' "xcMetaTablesExportDbToLocalFs",
]) ]);
// this.$toast.success('Successfully exported metadata').goAway(3000) // this.$toast.success('Successfully exported metadata').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.exportMetadata')}`).goAway(3000) this.$toast
.success(`${this.$t("msg.toast.exportMetadata")}`)
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.loading = null this.loading = null;
} }
} };
}, },
async exportMetaZip() { async exportMetaZip() {
this.dialogShow = true this.dialogShow = true;
// this.confirmMessage = 'Do you want to export metadata from meta tables?' // this.confirmMessage = 'Do you want to export metadata from meta tables?'
this.confirmMessage = `${this.$t('msg.info.exportMetadata')}` this.confirmMessage = `${this.$t("msg.info.exportMetadata")}`;
this.confirmAction = async(act) => { this.confirmAction = async (act) => {
if (act === 'hideDialog') { if (act === "hideDialog") {
this.dialogShow = false this.dialogShow = false;
} else { } else {
this.loading = 'export-zip' this.loading = "export-zip";
let data let data;
try { try {
data = await this.$store.dispatch('sqlMgr/ActSqlOp', [ data = await this.$store.dispatch("sqlMgr/ActSqlOp", [
{ {
// dbAlias: 'db', // dbAlias: 'db',
env: '_noco' env: "_noco",
}, },
'xcMetaTablesExportDbToZip', "xcMetaTablesExportDbToZip",
null, null,
null, null,
{ {
responseType: 'blob' responseType: "blob",
} },
]) ]);
const url = window.URL.createObjectURL(new Blob([data], { type: 'application/zip' })) const url = window.URL.createObjectURL(
const link = document.createElement('a') new Blob([data], { type: "application/zip" })
link.href = url );
link.setAttribute('download', 'meta.zip') // or any other extension const link = document.createElement("a");
document.body.appendChild(link) link.href = url;
link.click() link.setAttribute("download", "meta.zip"); // or any other extension
document.body.appendChild(link);
link.click();
// this.$toast.success('Successfully exported metadata').goAway(3000) // this.$toast.success('Successfully exported metadata').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.exportMetadata')}`).goAway(3000) this.$toast
.success(`${this.$t("msg.toast.exportMetadata")}`)
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.loading = null this.loading = null;
this.$tele.emit('proj-meta:export-zip:submit') this.$e("a:proj-meta:export");
} }
} };
}, },
async resetMeta() { async resetMeta() {
this.dialogShow = true this.dialogShow = true;
// this.confirmMessage = 'Do you want to clear metadata from meta tables?' // this.confirmMessage = 'Do you want to clear metadata from meta tables?'
this.confirmMessage = `${this.$t('msg.info.clearMetadata')}` this.confirmMessage = `${this.$t("msg.info.clearMetadata")}`;
this.confirmAction = async(act) => { this.confirmAction = async (act) => {
if (act === 'hideDialog') { if (act === "hideDialog") {
this.dialogShow = false this.dialogShow = false;
} else { } else {
this.loading = 'reset-metadata' this.loading = "reset-metadata";
try { try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [ await this.$store.dispatch("sqlMgr/ActSqlOp", [
{ {
// dbAlias: 'db', // dbAlias: 'db',
env: '_noco' env: "_noco",
}, },
'xcMetaTablesReset' "xcMetaTablesReset",
]) ]);
// this.$toast.success('Metadata cleared successfully').goAway(3000) // this.$toast.success('Metadata cleared successfully').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.clearMetadata')}`).goAway(3000) this.$toast
.success(`${this.$t("msg.toast.clearMetadata")}`)
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.loading = null this.loading = null;
} }
} };
}, },
async importMeta() { async importMeta() {
this.dialogShow = true this.dialogShow = true;
// this.confirmMessage = 'Do you want to import metadata from meta directory?' // this.confirmMessage = 'Do you want to import metadata from meta directory?'
this.confirmMessage = `${this.$t('msg.info.importMetadata')}` this.confirmMessage = `${this.$t("msg.info.importMetadata")}`;
this.confirmAction = async(act) => { this.confirmAction = async (act) => {
if (act === 'hideDialog') { if (act === "hideDialog") {
this.dialogShow = false this.dialogShow = false;
} else { } else {
this.loading = 'import-file' this.loading = "import-file";
try { try {
await this.$store.dispatch('sqlMgr/ActSqlOp', [ await this.$store.dispatch("sqlMgr/ActSqlOp", [
{ {
env: '_noco' env: "_noco",
}, },
'xcMetaTablesImportLocalFsToDb' "xcMetaTablesImportLocalFsToDb",
]) ]);
// this.$toast.success('Metadata imported successfully').goAway(3000) // this.$toast.success('Metadata imported successfully').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.importMetadata')}`).goAway(3000) this.$toast
.success(`${this.$t("msg.toast.importMetadata")}`)
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.loading = null this.loading = null;
} }
} };
}, },
async importMetaZip() { async importMetaZip() {
if (this.$refs.importFile && this.$refs.importFile.files && this.$refs.importFile.files[0]) { if (
const zipFile = this.$refs.importFile.files[0] this.$refs.importFile &&
this.loading = 'import-zip' this.$refs.importFile.files &&
this.$refs.importFile.files[0]
) {
const zipFile = this.$refs.importFile.files[0];
this.loading = "import-zip";
try { try {
this.$refs.importFile.value = '' this.$refs.importFile.value = "";
await this.$store.dispatch('sqlMgr/ActUploadOld', [ await this.$store.dispatch("sqlMgr/ActUploadOld", [
{ {
env: '_noco' env: "_noco",
}, },
'xcMetaTablesImportZipToLocalFsAndDb', "xcMetaTablesImportZipToLocalFsAndDb",
{ {
importsToCurrentProject: true importsToCurrentProject: true,
}, },
zipFile zipFile,
]) ]);
// this.$toast.success('Successfully imported metadata').goAway(3000) // this.$toast.success('Successfully imported metadata').goAway(3000)
this.$toast.success(`${this.$t('msg.toast.importMetadata')}`).goAway(3000) this.$toast
.success(`${this.$t("msg.toast.importMetadata")}`)
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.loading = null this.loading = null;
} }
} },
} },
} };
</script> </script>
<style scoped> <style scoped>

357
packages/nc-gui/components/project/spreadsheet/components/columnFilter.vue

@ -1,27 +1,32 @@
<template> <template>
<div <div
class="backgroundColor pa-2" class="backgroundColor pa-2"
:style="{width:nested ? '100%' : '530px'}" :style="{ width: nested ? '100%' : '530px' }"
> >
<div class="grid" @click.stop> <div class="grid" @click.stop>
<template v-for="(filter,i) in filters" dense> <template v-for="(filter, i) in filters" dense>
<template v-if="filter.status !== 'delete'"> <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
<div class="d-flex" style="gap:6px; padding: 0 6px"> 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">
<v-icon <v-icon
v-if="!filter.readOnly" v-if="!filter.readOnly"
:key="i + '_3'" :key="i + '_3'"
small small
class="nc-filter-item-remove-btn" class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter,i)" @click.stop="deleteFilter(filter, i)"
> >
mdi-close-box mdi-close-box
</v-icon> </v-icon>
<span v-else :key="i + '_1'" /> <span v-else :key="i + '_1'" />
<v-select <v-select
v-model="filter.logical_op" v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption " class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and' ,'or']" :items="['and', 'or']"
solo solo
flat flat
dense dense
@ -30,7 +35,7 @@
@click.stop @click.stop
@change="saveOrUpdate(filter, i)" @change="saveOrUpdate(filter, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span class="caption font-weight-regular">{{ item }}</span> <span class="caption font-weight-regular">{{ item }}</span>
</template> </template>
</v-select> </v-select>
@ -56,7 +61,7 @@
:key="i + '_3'" :key="i + '_3'"
small small
class="nc-filter-item-remove-btn" class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter,i)" @click.stop="deleteFilter(filter, i)"
> >
mdi-close-box mdi-close-box
</v-icon> </v-icon>
@ -65,14 +70,15 @@
v-if="!i" v-if="!i"
:key="i + '_2'" :key="i + '_2'"
class="caption d-flex align-center" class="caption d-flex align-center"
>{{ $t('labels.where') }}</span> >{{ $t("labels.where") }}</span
>
<v-select <v-select
v-else v-else
:key="i + '_4'" :key="i + '_4'"
v-model="filter.logical_op" v-model="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption " class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and' ,'or']" :items="['and', 'or']"
solo solo
flat flat
dense dense
@ -81,7 +87,7 @@
@click.stop @click.stop
@change="filterUpdateCondition(filter, i)" @change="filterUpdateCondition(filter, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span class="caption font-weight-regular">{{ item }}</span> <span class="caption font-weight-regular">{{ item }}</span>
</template> </template>
</v-select> </v-select>
@ -102,7 +108,7 @@
@click.stop @click.stop
@change="saveOrUpdate(filter, i)" @change="saveOrUpdate(filter, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span <span
:class="`caption font-weight-regular nc-filter-fld-${item.title}`" :class="`caption font-weight-regular nc-filter-fld-${item.title}`"
> >
@ -113,12 +119,12 @@
<v-select <v-select
:key="'k' + i" :key="'k' + i"
v-model="filter.comparison_op" v-model="filter.comparison_op"
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select" class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select"
:items="filterComparisonOp(filter)" :items="filterComparisonOp(filter)"
:placeholder="$t('labels.operation')" :placeholder="$t('labels.operation')"
solo solo
flat flat
style="max-width:120px" style="max-width: 120px"
dense dense
:disabled="filter.readOnly" :disabled="filter.readOnly"
hide-details hide-details
@ -126,11 +132,18 @@
@click.stop @click.stop
@change="filterUpdateCondition(filter, i)" @change="filterUpdateCondition(filter, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span class="caption font-weight-regular">{{ item.text }}</span> <span class="caption font-weight-regular">{{ item.text }}</span>
</template> </template>
</v-select> </v-select>
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="'span' + i" /> <span
v-if="
['null', 'notnull', 'empty', 'notempty'].includes(
filter.comparison_op
)
"
:key="'span' + i"
/>
<v-checkbox <v-checkbox
v-else-if="types[filter.field] === 'boolean'" v-else-if="types[filter.field] === 'boolean'"
:key="i + '_7'" :key="i + '_7'"
@ -157,26 +170,20 @@
</template> </template>
</div> </div>
<v-btn <v-btn small class="elevation-0 grey--text my-3" @click.stop="addFilter">
small <v-icon small color="grey"> mdi-plus </v-icon>
class="elevation-0 grey--text my-3"
@click.stop="addFilter"
>
<v-icon small color="grey">
mdi-plus
</v-icon>
<!-- Add Filter --> <!-- Add Filter -->
{{ $t('activity.addFilter') }} {{ $t("activity.addFilter") }}
</v-btn> </v-btn>
<slot /> <slot />
</div> </div>
</template> </template>
<script> <script>
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' import { UITypes } from "~/components/project/spreadsheet/helpers/uiTypes";
export default { export default {
name: 'ColumnFilter', name: "ColumnFilter",
props: { props: {
fieldList: [Array], fieldList: [Array],
meta: Object, meta: Object,
@ -185,269 +192,299 @@ export default {
viewId: String, viewId: String,
shared: Boolean, shared: Boolean,
webHook: Boolean, webHook: Boolean,
hookId: String hookId: String,
}, },
data: () => ({ data: () => ({
filters: [], filters: [],
opList: [ opList: [
'is equal', 'is not equal', 'is like', 'is not like', "is equal",
"is not equal",
"is like",
"is not like",
// 'is empty', 'is not empty', // 'is empty', 'is not empty',
'is null', 'is not null', "is null",
'>', "is not null",
'<', ">",
'>=', "<",
'<=' ">=",
"<=",
], ],
comparisonOp: [ comparisonOp: [
{ {
text: 'is equal', text: "is equal",
value: 'eq' value: "eq",
}, },
{ {
text: 'is not equal', text: "is not equal",
value: 'neq' value: "neq",
}, },
{ {
text: 'is like', text: "is like",
value: 'like' value: "like",
}, },
{ {
text: 'is not like', text: "is not like",
value: 'nlike' value: "nlike",
}, },
{ {
text: 'is empty', text: "is empty",
value: 'empty', value: "empty",
ignoreVal: true ignoreVal: true,
}, },
{ {
text: 'is not empty', text: "is not empty",
value: 'notempty', value: "notempty",
ignoreVal: true ignoreVal: true,
}, },
{ {
text: 'is null', text: "is null",
value: 'null', value: "null",
ignoreVal: true ignoreVal: true,
}, },
{ {
text: 'is not null', text: "is not null",
value: 'notnull', value: "notnull",
ignoreVal: true ignoreVal: true,
}, },
{ {
text: '>', text: ">",
value: 'gt' value: "gt",
}, },
{ {
text: '<', text: "<",
value: 'lt' value: "lt",
}, },
{ {
text: '>=', text: ">=",
value: 'gte' value: "gte",
}, },
{ {
text: '<=', text: "<=",
value: 'lte' value: "lte",
} },
] ],
}), }),
computed: { computed: {
columnsById() { columnsById() {
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {}) return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {});
}, },
autoApply() { autoApply() {
return this.$store.state.windows.autoApplyFilter && !this.webHook return this.$store.state.windows.autoApplyFilter && !this.webHook;
}, },
columns() { columns() {
return (this.meta && this.meta.columns.filter(c => c && (!c.colOptions || !c.system))) return (
this.meta &&
this.meta.columns.filter((c) => c && (!c.colOptions || !c.system))
);
}, },
types() { types() {
if (!this.meta || !this.meta.columns || !this.meta.columns.length) { if (!this.meta || !this.meta.columns || !this.meta.columns.length) {
return {} return {};
} }
return this.meta.columns.reduce((obj, col) => { return this.meta.columns.reduce((obj, col) => {
switch (col.uidt) { switch (col.uidt) {
case UITypes.Number: case UITypes.Number:
case UITypes.Decimal: case UITypes.Decimal:
obj[col.title] = obj[col.column_name] = 'number' obj[col.title] = obj[col.column_name] = "number";
break break;
case UITypes.Checkbox: case UITypes.Checkbox:
obj[col.title] = obj[col.column_name] = 'boolean' obj[col.title] = obj[col.column_name] = "boolean";
break break;
default: default:
break break;
} }
return obj return obj;
}, {}) }, {});
} },
}, },
watch: { watch: {
async viewId(v) { async viewId(v) {
if (v) { if (v) {
await this.loadFilter() await this.loadFilter();
} }
}, },
filters: { filters: {
handler(v) { handler(v) {
this.$emit('input', v && v.filter(f => (f.fk_column_id && f.comparison_op) || f.is_group)) this.$emit(
"input",
v &&
v.filter((f) => (f.fk_column_id && f.comparison_op) || f.is_group)
);
}, },
deep: true deep: true,
} },
}, },
created() { created() {
this.loadFilter() this.loadFilter();
}, },
methods: { methods: {
filterComparisonOp(f) { filterComparisonOp(f) {
return this.comparisonOp.filter((op) => { return this.comparisonOp.filter((op) => {
if (f && f.fk_column_id && this.columnsById[f.fk_column_id] && if (
this.columnsById[f.fk_column_id].uidt === UITypes.LinkToAnotherRecord && f &&
f.fk_column_id &&
this.columnsById[f.fk_column_id] &&
this.columnsById[f.fk_column_id].uidt ===
UITypes.LinkToAnotherRecord &&
this.columnsById[f.fk_column_id].uidt === UITypes.Lookup this.columnsById[f.fk_column_id].uidt === UITypes.Lookup
) { ) {
return ![ return !["notempty", "empty", "notnull", "null"].includes(op.value);
'notempty',
'empty',
'notnull',
'null'
].includes(op.value)
} }
return true return true;
}) });
}, },
async applyChanges(nested = false, { hookId } = {}) { async applyChanges(nested = false, { hookId } = {}) {
for (const [i, filter] of Object.entries(this.filters)) { for (const [i, filter] of Object.entries(this.filters)) {
if (filter.status === 'delete') { if (filter.status === "delete") {
if (this.hookId || hookId) { if (this.hookId || hookId) {
await this.$api.dbTableFilter.delete(filter.id) await this.$api.dbTableFilter.delete(filter.id);
} else { } else {
await this.$api.dbTableFilter.delete(filter.id) await this.$api.dbTableFilter.delete(filter.id);
} }
} else if (filter.status === 'update') { } else if (filter.status === "update") {
if (filter.id) { if (filter.id) {
if (this.hookId || hookId) { if (this.hookId || hookId) {
await this.$api.dbTableFilter.update(filter.id, { await this.$api.dbTableFilter.update(filter.id, {
...filter, ...filter,
fk_parent_id: this.parentId fk_parent_id: this.parentId,
}) });
} else { } else {
await this.$api.dbTableFilter.update(filter.id, { await this.$api.dbTableFilter.update(filter.id, {
...filter, ...filter,
fk_parent_id: this.parentId fk_parent_id: this.parentId,
}) });
} }
} else if (this.hookId || hookId) { } else if (this.hookId || hookId) {
this.$set(this.filters, i, (await this.$api.dbTableWebhookFilter.create(this.hookId || hookId, { this.$set(
...filter, this.filters,
fk_parent_id: this.parentId i,
}))) await this.$api.dbTableWebhookFilter.create(
this.hookId || hookId,
{
...filter,
fk_parent_id: this.parentId,
}
)
);
} else { } else {
this.$set(this.filters, i, (await this.$api.dbTableFilter.create(this.viewId, { this.$set(
...filter, this.filters,
fk_parent_id: this.parentId i,
}))) await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId,
})
);
} }
} }
} }
if (this.$refs.nestedFilter) { if (this.$refs.nestedFilter) {
for (const nestedFilter of this.$refs.nestedFilter) { for (const nestedFilter of this.$refs.nestedFilter) {
await nestedFilter.applyChanges(true) await nestedFilter.applyChanges(true);
} }
} }
this.loadFilter() this.loadFilter();
if (!nested) { this.$emit('updated') } if (!nested) {
this.$emit("updated");
}
}, },
async loadFilter() { async loadFilter() {
let filters = [] let filters = [];
if (this.viewId && this._isUIAllowed('filterSync')) { if (this.viewId && this._isUIAllowed("filterSync")) {
filters = this.parentId filters = this.parentId
? (await this.$api.dbTableFilter.childrenRead(this.parentId)) ? await this.$api.dbTableFilter.childrenRead(this.parentId)
: (await this.$api.dbTableFilter.read(this.viewId)) : await this.$api.dbTableFilter.read(this.viewId);
} }
if (this.hookId && this._isUIAllowed('filterSync')) { if (this.hookId && this._isUIAllowed("filterSync")) {
filters = this.parentId filters = this.parentId
? (await this.$api.dbTableFilter.childrenRead(this.parentId)) ? await this.$api.dbTableFilter.childrenRead(this.parentId)
: (await this.$api.dbTableWebhookFilter.read(this.hookId)) : await this.$api.dbTableWebhookFilter.read(this.hookId);
} }
this.filters = filters this.filters = filters;
}, },
addFilter() { addFilter() {
this.filters.push({ this.filters.push({
fk_column_id: null, fk_column_id: null,
comparison_op: 'eq', comparison_op: "eq",
value: '', value: "",
status: 'update', status: "update",
logical_op: 'and' logical_op: "and",
}) });
this.filters = this.filters.slice() this.filters = this.filters.slice();
this.$tele.emit(`filter:add:trigger:${this.filters.length}`) this.$e("a:filter:add", { length: this.filters.length });
}, },
addFilterGroup() { addFilterGroup() {
this.filters.push({ this.filters.push({
parentId: this.parentId, parentId: this.parentId,
is_group: true, is_group: true,
status: 'update' status: "update",
}) });
this.filters = this.filters.slice() this.filters = this.filters.slice();
const index = this.filters.length - 1 const index = this.filters.length - 1;
this.saveOrUpdate(this.filters[index], index) this.saveOrUpdate(this.filters[index], index);
}, },
filterUpdateCondition(filter, i) { filterUpdateCondition(filter, i) {
this.saveOrUpdate(filter, i) this.saveOrUpdate(filter, i);
this.$tele.emit(`filter:condition:${filter.logical_op}:${filter.comparison_op}`) this.$e("a:filter:update", {
logical: filter.logical_op,
comparison: filter.comparison_op,
});
}, },
async saveOrUpdate(filter, i) { async saveOrUpdate(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) { if (this.shared || !this._isUIAllowed("filterSync")) {
// this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op)) // this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op))
this.$emit('updated') this.$emit("updated");
} else if (!this.autoApply) { } else if (!this.autoApply) {
filter.status = 'update' filter.status = "update";
} else if (filter.id) { } else if (filter.id) {
await this.$api.dbTableFilter.update(filter.id, { await this.$api.dbTableFilter.update(filter.id, {
...filter, ...filter,
fk_parent_id: this.parentId fk_parent_id: this.parentId,
}) });
this.$emit('updated') this.$emit("updated");
} else { } else {
this.$set(this.filters, i, (await this.$api.dbTableFilter.create(this.viewId, { this.$set(
...filter, this.filters,
fk_parent_id: this.parentId i,
}))) await this.$api.dbTableFilter.create(this.viewId, {
...filter,
fk_parent_id: this.parentId,
})
);
this.$emit('updated') this.$emit("updated");
} }
}, },
async deleteFilter(filter, i) { async deleteFilter(filter, i) {
if (this.shared || !this._isUIAllowed('filterSync')) { if (this.shared || !this._isUIAllowed("filterSync")) {
this.filters.splice(i, 1) this.filters.splice(i, 1);
this.$emit('updated') this.$emit("updated");
} else if (filter.id) { } else if (filter.id) {
if (!this.autoApply) { if (!this.autoApply) {
this.$set(filter, 'status', 'delete') this.$set(filter, "status", "delete");
} else { } else {
await this.$api.dbTableFilter.delete(filter.id) await this.$api.dbTableFilter.delete(filter.id);
await this.loadFilter() await this.loadFilter();
this.$emit('updated') this.$emit("updated");
} }
} else { } else {
this.filters.splice(i, 1) this.filters.splice(i, 1);
this.$emit('updated') this.$emit("updated");
} }
this.$tele.emit('filter:delete') this.$e("a:filter:delete");
} },
} },
} };
</script> </script>
<style scoped> <style scoped>
.grid { .grid {
display: grid; display: grid;
grid-template-columns:22px 80px auto auto auto; grid-template-columns: 22px 80px auto auto auto;
column-gap: 6px; column-gap: 6px;
row-gap: 6px row-gap: 6px;
} }
</style> </style>

84
packages/nc-gui/components/project/spreadsheet/components/columnFilterMenu.vue

@ -1,30 +1,25 @@
<template> <template>
<v-menu offset-y eager> <v-menu offset-y eager>
<template #activator="{ on, }"> <template #activator="{ on }">
<v-badge <v-badge :value="filters.length" color="primary" dot overlap>
:value="filters.length"
color="primary"
dot
overlap
>
<v-btn <v-btn
v-t="['filter:trigger']" v-t="['c:filter']"
class="nc-filter-menu-btn px-2 nc-remove-border" class="nc-filter-menu-btn px-2 nc-remove-border"
:disabled="isLocked" :disabled="isLocked"
outlined outlined
small small
text text
:class=" { 'primary lighten-5 grey--text text--darken-3' : filters.length}" :class="{
'primary lighten-5 grey--text text--darken-3': filters.length,
}"
v-on="on" v-on="on"
> >
<v-icon small class="mr-1" color="grey darken-3"> <v-icon small class="mr-1" color="grey darken-3">
mdi-filter-outline mdi-filter-outline
</v-icon> </v-icon>
<!-- Filter --> <!-- Filter -->
{{ $t('activity.filter') }} {{ $t("activity.filter") }}
<v-icon small color="#777"> <v-icon small color="#777"> mdi-menu-down </v-icon>
mdi-menu-down
</v-icon>
</v-btn> </v-btn>
</v-badge> </v-badge>
</template> </template>
@ -49,16 +44,21 @@
> >
<template #label> <template #label>
<span class="grey--text caption"> <span class="grey--text caption">
{{ $t('msg.info.filterAutoApply') }} {{ $t("msg.info.filterAutoApply") }}
<!-- Auto apply --> <!-- Auto apply -->
</span> </span>
</template> </template>
</v-checkbox> </v-checkbox>
<v-spacer /> <v-spacer />
<v-btn v-show="!autosave" color="primary" small class="caption ml-2" @click="applyChanges"> <v-btn
Apply v-show="!autosave"
changes color="primary"
small
class="caption ml-2"
@click="applyChanges"
>
Apply changes
</v-btn> </v-btn>
</div> </div>
</column-filter> </column-filter>
@ -66,59 +66,65 @@
</template> </template>
<script> <script>
import ColumnFilter from '@/components/project/spreadsheet/components/columnFilter' import ColumnFilter from "@/components/project/spreadsheet/components/columnFilter";
export default { export default {
name: 'ColumnFilterMenu', name: "ColumnFilterMenu",
components: { ColumnFilter }, components: { ColumnFilter },
props: ['fieldList', 'isLocked', 'value', 'meta', 'viewId', 'shared'], props: ["fieldList", "isLocked", "value", "meta", "viewId", "shared"],
data: () => ({ data: () => ({
filters: [] filters: [],
}), }),
computed: { computed: {
autosave: { autosave: {
set(v) { set(v) {
this.$store.commit('windows/MutAutoApplyFilter', v) this.$store.commit("windows/MutAutoApplyFilter", v);
this.$tele.emit(`filter:auto-apply:${v}`) this.$e("a:filter:auto-apply", { flag: v });
}, },
get() { get() {
return this.$store.state.windows.autoApplyFilter return this.$store.state.windows.autoApplyFilter;
} },
} },
}, },
watch: { watch: {
filters: { filters: {
handler(v) { handler(v) {
if (this.autosave) { if (this.autosave) {
this.$emit('input', v) this.$emit("input", v);
} }
}, },
deep: true deep: true,
}, },
autosave(v) { autosave(v) {
if (!v) { if (!v) {
this.filters = JSON.parse(JSON.stringify(this.value || [])) this.filters = JSON.parse(JSON.stringify(this.value || []));
} }
}, },
value(v) { value(v) {
this.filters = this.autosave ? v || [] : JSON.parse(JSON.stringify(v || [])) this.filters = this.autosave
} ? v || []
: JSON.parse(JSON.stringify(v || []));
},
}, },
created() { created() {
this.filters = this.autosave ? this.value || [] : JSON.parse(JSON.stringify(this.value || [])) this.filters = this.autosave
? this.value || []
: JSON.parse(JSON.stringify(this.value || []));
}, },
methods: { methods: {
applyChanges() { applyChanges() {
this.$emit('input', this.filters) this.$emit("input", this.filters);
if (this.$refs.filter) { this.$refs.filter.applyChanges() } if (this.$refs.filter) {
this.$tele.emit('filter:apply-explicit') this.$refs.filter.applyChanges();
} }
} this.$e("a:filter:apply");
} },
},
};
</script> </script>
<style scoped> <style scoped>
/deep/ .col-filter-checkbox .v-input--selection-controls__input { /deep/ .col-filter-checkbox .v-input--selection-controls__input {
transform: scale(.7); transform: scale(0.7);
} }
</style> </style>

555
packages/nc-gui/components/project/spreadsheet/components/editColumn.vue

@ -4,7 +4,7 @@
max-width="400px" max-width="400px"
max-height="95vh" max-height="95vh"
style="overflow: auto" style="overflow: auto"
class=" card nc-col-create-or-edit-card " class="card nc-col-create-or-edit-card"
> >
<v-form ref="form" v-model="valid"> <v-form ref="form" v-model="valid">
<v-container fluid @click.stop.prevent> <v-container fluid @click.stop.prevent>
@ -16,10 +16,22 @@
hide-details="auto" hide-details="auto"
color="primary" color="primary"
:rules="[ :rules="[
v => !!v || 'Required', (v) => !!v || 'Required',
v => !meta || !meta.columns || meta.columns.every(c => column && (c.column_name || '').toLowerCase() === (column.column_name || '').toLowerCase() ||( (v) =>
(v||'').toLowerCase() !== (c.column_name||'').toLowerCase() && (v||'').toLowerCase() !== (c.title||'').toLowerCase())) || 'Duplicate column name' ,// && meta.v.every(c => v !== c.title ) || 'Duplicate column name', !meta ||
validateColumnName !meta.columns ||
meta.columns.every(
(c) =>
(column &&
(c.column_name || '').toLowerCase() ===
(column.column_name || '').toLowerCase()) ||
((v || '').toLowerCase() !==
(c.column_name || '').toLowerCase() &&
(v || '').toLowerCase() !==
(c.title || '').toLowerCase())
) ||
'Duplicate column name', // && meta.v.every(c => v !== c.title ) || 'Duplicate column name',
validateColumnName,
]" ]"
class="caption nc-column-name-input" class="caption nc-column-name-input"
:label="$t('labels.columnName')" :label="$t('labels.columnName')"
@ -32,22 +44,20 @@
<v-container <v-container
fluid fluid
:class="{ :class="{
editDisabled :isEditDisabled editDisabled: isEditDisabled,
}" }"
> >
<v-row> <v-row>
<v-col v-if="relation" cols="12"> <v-col v-if="relation" cols="12">
<div class="caption"> <div class="caption">
<p class="mb-1"> <p class="mb-1">Foreign Key</p>
Foreign Key
</p>
<v-icon small class="mt-n1"> <v-icon small class="mt-n1"> mdi-table </v-icon>
mdi-table <span class="text-capitalize font-weight-bold body-1">
</v-icon> {{ relation._rtn }}</span
<span class="text-capitalize font-weight-bold body-1"> {{ relation._rtn }}</span> >
<v-icon <v-icon
v-ge="['columns','fk-delete']" v-ge="['columns', 'fk-delete']"
small small
class="ml-3 mt-n1" class="ml-3 mt-n1"
color="error" color="error"
@ -55,7 +65,9 @@
> >
mdi-delete-forever mdi-delete-forever
</v-icon> </v-icon>
<span v-if="relation.type=== 'virtual'" class="caption">(v)</span> <span v-if="relation.type === 'virtual'" class="caption"
>(v)</span
>
</div> </div>
</v-col> </v-col>
<template v-else> <template v-else>
@ -67,23 +79,25 @@
item-value="name" item-value="name"
item-text="name" item-text="name"
class="caption ui-type nc-ui-dt-dropdown" class="caption ui-type nc-ui-dt-dropdown"
:class="{'primary lighten-5' : newColumn.uidt }" :class="{ 'primary lighten-5': newColumn.uidt }"
:label="$t('labels.columnType')" :label="$t('labels.columnType')"
dense dense
outlined outlined
:items="uiTypes" :items="uiTypes"
@change="onUiTypeChange" @change="onUiTypeChange"
> >
<template #selection="{item}"> <template #selection="{ item }">
<div> <div>
<v-icon color="grey darken-4" small class="mr-1"> <v-icon color="grey darken-4" small class="mr-1">
{{ item.icon }} {{ item.icon }}
</v-icon> </v-icon>
<span class="caption grey--text text--darken-4"> {{ item.name }}</span> <span class="caption grey--text text--darken-4">
{{ item.name }}</span
>
</div> </div>
</template> </template>
<template #item="{item}"> <template #item="{ item }">
<div class="caption"> <div class="caption">
<v-icon small class="mr-1"> <v-icon small class="mr-1">
{{ item.icon }} {{ item.icon }}
@ -94,7 +108,11 @@
</v-autocomplete> </v-autocomplete>
<v-alert <v-alert
v-if="column && newColumn.uidt === 'SingleSelect' && column.uidt === 'MultiSelect'" v-if="
column &&
newColumn.uidt === 'SingleSelect' &&
column.uidt === 'MultiSelect'
"
dense dense
type="warning" type="warning"
class="caption warning--text mt-2 mb-n4 pa-1" class="caption warning--text mt-2 mb-n4 pa-1"
@ -105,8 +123,8 @@
mdi-alert-outline mdi-alert-outline
</v-icon> </v-icon>
</template> </template>
Changing MultiSelect to SingleSelect can lead to errors when there are multiple values associated Changing MultiSelect to SingleSelect can lead to errors when
with a cell there are multiple values associated with a cell
</v-alert> </v-alert>
</v-col> </v-col>
@ -116,14 +134,23 @@
@input="newColumn.altered = newColumn.altered || 2" @input="newColumn.altered = newColumn.altered || 2"
/> />
</v-col> </v-col>
<v-col v-if="accordion" cols="12" class="pt-0" :class="{'pb-0': advanceOptions}"> <v-col
v-if="accordion"
cols="12"
class="pt-0"
:class="{ 'pb-0': advanceOptions }"
>
<div <div
class="pointer grey--text text-right caption nc-more-options" class="pointer grey--text text-right caption nc-more-options"
@click="advanceOptions = !advanceOptions" @click="advanceOptions = !advanceOptions"
> >
{{ advanceOptions ? $t('general.hideAll') : $t('general.showMore') }} {{
advanceOptions
? $t("general.hideAll")
: $t("general.showMore")
}}
<v-icon x-small color="grey"> <v-icon x-small color="grey">
mdi-{{ advanceOptions ? 'minus' : 'plus' }}-circle-outline mdi-{{ advanceOptions ? "minus" : "plus" }}-circle-outline
</v-icon> </v-icon>
</div> </div>
</v-col> </v-col>
@ -131,10 +158,7 @@
<v-col v-show="advanceOptions || !accordion" cols="12"> <v-col v-show="advanceOptions || !accordion" cols="12">
<v-row> <v-row>
<template v-if="newColumn.uidt !== 'Formula'"> <template v-if="newColumn.uidt !== 'Formula'">
<v-col <v-col v-if="isLookup" cols="12">
v-if="isLookup"
cols="12"
>
<lookup-options <lookup-options
ref="lookup" ref="lookup"
:column="newColumn" :column="newColumn"
@ -146,10 +170,7 @@
v-on="$listeners" v-on="$listeners"
/> />
</v-col> </v-col>
<v-col <v-col v-if="isRollup" cols="12">
v-if="isRollup"
cols="12"
>
<rollup-options <rollup-options
ref="rollup" ref="rollup"
:column="newColumn" :column="newColumn"
@ -161,10 +182,7 @@
v-on="$listeners" v-on="$listeners"
/> />
</v-col> </v-col>
<v-col <v-col v-if="isLinkToAnotherRecord" cols="12">
v-if="isLinkToAnotherRecord"
cols="12"
>
<linked-to-another-options <linked-to-another-options
ref="relation" ref="relation"
:column="newColumn" :column="newColumn"
@ -176,10 +194,7 @@
@onColumnSelect="onRelColumnSelect" @onColumnSelect="onRelColumnSelect"
/> />
</v-col> </v-col>
<v-col <v-col v-if="isRelation" cols="12">
v-if="isRelation"
cols="12"
>
<relation-options <relation-options
ref="relation" ref="relation"
:alias="alias" :alias="alias"
@ -191,26 +206,41 @@
/> />
</v-col> </v-col>
<template v-if="newColumn.column_name && newColumn.uidt && !isVirtual"> <template
v-if="
newColumn.column_name && newColumn.uidt && !isVirtual
"
>
<v-col cols="12"> <v-col cols="12">
<v-container fluid class="wrapper"> <v-container fluid class="wrapper">
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<div class="d-flex justify-space-between caption"> <div
class="d-flex justify-space-between caption"
>
<v-tooltip bottom z-index="99999"> <v-tooltip bottom z-index="99999">
<template #activator="{on}"> <template #activator="{ on }">
<div v-on="on"> <div v-on="on">
<v-checkbox <v-checkbox
v-model="newColumn.rqd" v-model="newColumn.rqd"
:disabled="newColumn.pk || !sqlUi.columnEditable(newColumn)" :disabled="
newColumn.pk ||
!sqlUi.columnEditable(newColumn)
"
class="mr-2 mt-0" class="mr-2 mt-0"
dense dense
hide-details hide-details
label="NN" label="NN"
@change="newColumn.altered = newColumn.altered || 2" @change="
newColumn.altered =
newColumn.altered || 2
"
> >
<template #label> <template #label>
<span class="caption font-weight-bold">NN</span> <span
class="caption font-weight-bold"
>NN</span
>
</template> </template>
</v-checkbox> </v-checkbox>
</div> </div>
@ -218,20 +248,27 @@
<span>Not Null</span> <span>Not Null</span>
</v-tooltip> </v-tooltip>
<v-tooltip bottom z-index="99999"> <v-tooltip bottom z-index="99999">
<template #activator="{on}"> <template #activator="{ on }">
<div v-on="on"> <div v-on="on">
<v-checkbox <v-checkbox
v-model="newColumn.pk" v-model="newColumn.pk"
:disabled="!sqlUi.columnEditable(newColumn)" :disabled="
!sqlUi.columnEditable(newColumn)
"
class="mr-2 mt-0" class="mr-2 mt-0"
dense dense
hide-details hide-details
label="PK" label="PK"
@change="newColumn.altered = newColumn.altered || 2" @change="
newColumn.altered =
newColumn.altered || 2
"
> >
<template #label> <template #label>
<span class="caption font-weight-bold">PK</span> <span
class="caption font-weight-bold"
>PK</span
>
</template> </template>
</v-checkbox> </v-checkbox>
</div> </div>
@ -240,19 +277,30 @@
</v-tooltip> </v-tooltip>
<v-tooltip bottom z-index="99999"> <v-tooltip bottom z-index="99999">
<template #activator="{on}"> <template #activator="{ on }">
<div v-on="on"> <div v-on="on">
<v-checkbox <v-checkbox
v-model="newColumn.ai" v-model="newColumn.ai"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)" :disabled="
sqlUi.colPropUNDisabled(
newColumn
) ||
!sqlUi.columnEditable(newColumn)
"
class="mr-2 mt-0" class="mr-2 mt-0"
dense dense
hide-details hide-details
label="AI" label="AI"
@change="newColumn.altered = newColumn.altered || 2" @change="
newColumn.altered =
newColumn.altered || 2
"
> >
<template #label> <template #label>
<span class="caption font-weight-bold">AI</span> <span
class="caption font-weight-bold"
>AI</span
>
</template> </template>
</v-checkbox> </v-checkbox>
</div> </div>
@ -261,7 +309,7 @@
</v-tooltip> </v-tooltip>
<v-tooltip bottom z-index="99999"> <v-tooltip bottom z-index="99999">
<template #activator="{on}"> <template #activator="{ on }">
<div v-on="on"> <div v-on="on">
<v-checkbox <v-checkbox
v-model="newColumn.un" v-model="newColumn.un"
@ -269,11 +317,22 @@
dense dense
hide-details hide-details
label="UN" label="UN"
:disabled="sqlUi.colPropUNDisabled(newColumn) || !sqlUi.columnEditable(newColumn)" :disabled="
@change="newColumn.altered = newColumn.altered || 2" sqlUi.colPropUNDisabled(
newColumn
) ||
!sqlUi.columnEditable(newColumn)
"
@change="
newColumn.altered =
newColumn.altered || 2
"
> >
<template #label> <template #label>
<span class="caption font-weight-bold">UN</span> <span
class="caption font-weight-bold"
>UN</span
>
</template> </template>
</v-checkbox> </v-checkbox>
</div> </div>
@ -282,7 +341,7 @@
</v-tooltip> </v-tooltip>
<v-tooltip bottom z-index="99999"> <v-tooltip bottom z-index="99999">
<template #activator="{on}"> <template #activator="{ on }">
<div v-on="on"> <div v-on="on">
<v-checkbox <v-checkbox
v-model="newColumn.au" v-model="newColumn.au"
@ -290,11 +349,22 @@
dense dense
hide-details hide-details
label="UN" label="UN"
:disabled=" sqlUi.colPropAuDisabled(newColumn) || !sqlUi.columnEditable(newColumn)" :disabled="
@change="newColumn.altered = newColumn.altered || 2" sqlUi.colPropAuDisabled(
newColumn
) ||
!sqlUi.columnEditable(newColumn)
"
@change="
newColumn.altered =
newColumn.altered || 2
"
> >
<template #label> <template #label>
<span class="caption font-weight-bold">AU</span> <span
class="caption font-weight-bold"
>AU</span
>
</template> </template>
</v-checkbox> </v-checkbox>
</div> </div>
@ -317,30 +387,47 @@
/> />
</v-col> </v-col>
<v-col :cols="sqlUi.showScale(newColumn) && !isSelect ? 6 : 12"> <v-col
:cols="
sqlUi.showScale(newColumn) && !isSelect
? 6
: 12
"
>
<!--label="Length / Values"--> <!--label="Length / Values"-->
<v-text-field <v-text-field
v-if="!isSelect" v-if="!isSelect"
v-model="newColumn.dtxp" v-model="newColumn.dtxp"
dense dense
:disabled="sqlUi.getDefaultLengthIsDisabled(newColumn.dt) || !sqlUi.columnEditable(newColumn)" :disabled="
sqlUi.getDefaultLengthIsDisabled(
newColumn.dt
) || !sqlUi.columnEditable(newColumn)
"
class="caption" class="caption"
:label="$t('labels.lengthValue')" :label="$t('labels.lengthValue')"
outlined outlined
hide-details hide-details
@input="newColumn.altered = newColumn.altered || 2" @input="
newColumn.altered = newColumn.altered || 2
"
/> />
</v-col> </v-col>
<v-col v-if="sqlUi.showScale(newColumn)" :cols="isSelect ?12 : 6"> <v-col
v-if="sqlUi.showScale(newColumn)"
:cols="isSelect ? 12 : 6"
>
<v-text-field <v-text-field
v-model="newColumn.dtxs" v-model="newColumn.dtxs"
dense dense
:disabled=" !sqlUi.columnEditable(newColumn)" :disabled="!sqlUi.columnEditable(newColumn)"
class="caption" class="caption"
label="Scale" label="Scale"
outlined outlined
hide-details hide-details
@input="newColumn.altered = newColumn.altered || 2" @input="
newColumn.altered = newColumn.altered || 2
"
/> />
</v-col> </v-col>
@ -348,13 +435,20 @@
<v-textarea <v-textarea
v-model="newColumn.cdf" v-model="newColumn.cdf"
:label="$t('placeholder.defaultValue')" :label="$t('placeholder.defaultValue')"
:hint="sqlUi.getDefaultValueForDatatype(newColumn.dt)" :hint="
sqlUi.getDefaultValueForDatatype(
newColumn.dt
)
"
persistent-hint persistent-hint
rows="3" rows="3"
outlined outlined
dense dense
class="caption" class="caption"
@input="(newColumn.altered = newColumn.altered || 2); (newColumn.cdf = newColumn.cdf || null);" @input="
newColumn.altered = newColumn.altered || 2;
newColumn.cdf = newColumn.cdf || null;
"
/> />
</v-col> </v-col>
</v-row> </v-row>
@ -381,35 +475,34 @@
</v-col> </v-col>
</template> </template>
<div class="disabled-info" :class="{'d-none':!isEditDisabled}"> <div class="disabled-info" :class="{ 'd-none': !isEditDisabled }">
<v-alert dense type="warning" icon="info" class="caption mx-2" outlined> <v-alert
This spreadsheet is connected to an SQLite DB.<br> dense
For production please see <a type="warning"
icon="info"
class="caption mx-2"
outlined
>
This spreadsheet is connected to an SQLite DB.<br />
For production please see
<a
href="https://github.com/nocodb/nocodb#production-setup" href="https://github.com/nocodb/nocodb#production-setup"
target="_blank" target="_blank"
>here</a>. >here</a
>.
</v-alert> </v-alert>
</div> </div>
</v-row> </v-row>
</v-container> </v-container>
<v-col cols="12" class="d-flex pt-0"> <v-col cols="12" class="d-flex pt-0">
<v-spacer /> <v-spacer />
<v-btn <v-btn small outlined @click="close">
small
outlined
@click="close"
>
<!-- Cancel --> <!-- Cancel -->
{{ $t('general.cancel') }} {{ $t("general.cancel") }}
</v-btn> </v-btn>
<v-btn <v-btn small color="primary" :disabled="!valid" @click="save">
small
color="primary"
:disabled="!valid"
@click="save"
>
<!-- Save --> <!-- Save -->
{{ $t('general.save') }} {{ $t("general.save") }}
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
@ -426,19 +519,19 @@
</template> </template>
<script> <script>
import { MssqlUi, SqliteUi } from 'nocodb-sdk' import { MssqlUi, SqliteUi } from "nocodb-sdk";
import { UITypes, uiTypes } from '../helpers/uiTypes' import { UITypes, uiTypes } from "../helpers/uiTypes";
import RollupOptions from './editColumn/rollupOptions' import RollupOptions from "./editColumn/rollupOptions";
import FormulaOptions from '@/components/project/spreadsheet/components/editColumn/formulaOptions' import FormulaOptions from "@/components/project/spreadsheet/components/editColumn/formulaOptions";
import LookupOptions from '@/components/project/spreadsheet/components/editColumn/lookupOptions' import LookupOptions from "@/components/project/spreadsheet/components/editColumn/lookupOptions";
import CustomSelectOptions from '@/components/project/spreadsheet/components/editColumn/customSelectOptions' import CustomSelectOptions from "@/components/project/spreadsheet/components/editColumn/customSelectOptions";
import RelationOptions from '@/components/project/spreadsheet/components/editColumn/relationOptions' import RelationOptions from "@/components/project/spreadsheet/components/editColumn/relationOptions";
import DlgLabelSubmitCancel from '@/components/utils/dlgLabelSubmitCancel' import DlgLabelSubmitCancel from "@/components/utils/dlgLabelSubmitCancel";
import LinkedToAnotherOptions from '@/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions' import LinkedToAnotherOptions from "@/components/project/spreadsheet/components/editColumn/linkedToAnotherOptions";
import { validateColumnName } from '~/helpers' import { validateColumnName } from "~/helpers";
export default { export default {
name: 'EditColumn', name: "EditColumn",
components: { components: {
RollupOptions, RollupOptions,
FormulaOptions, FormulaOptions,
@ -446,7 +539,7 @@ export default {
LinkedToAnotherOptions, LinkedToAnotherOptions,
DlgLabelSubmitCancel, DlgLabelSubmitCancel,
RelationOptions, RelationOptions,
CustomSelectOptions CustomSelectOptions,
}, },
props: { props: {
nodes: Object, nodes: Object,
@ -455,154 +548,189 @@ export default {
editColumn: Boolean, editColumn: Boolean,
column: Object, column: Object,
columnIndex: Number, columnIndex: Number,
value: Boolean value: Boolean,
}, },
data: () => ({ data: () => ({
valid: false, valid: false,
relationDeleteDlg: false, relationDeleteDlg: false,
newColumn: {}, newColumn: {},
advanceOptions: false advanceOptions: false,
}), }),
computed: { computed: {
accordion() { accordion() {
return ![UITypes.LinkToAnotherRecord, UITypes.Lookup, UITypes.Rollup, UITypes.SpecificDBType, UITypes.Formula].includes(this.newColumn && this.newColumn.uidt) return ![
UITypes.LinkToAnotherRecord,
UITypes.Lookup,
UITypes.Rollup,
UITypes.SpecificDBType,
UITypes.Formula,
].includes(this.newColumn && this.newColumn.uidt);
}, },
uiTypes() { uiTypes() {
return uiTypes.filter(t => !this.editColumn || !t.virtual) return uiTypes.filter((t) => !this.editColumn || !t.virtual);
}, },
isEditDisabled() { isEditDisabled() {
return this.editColumn && this.sqlUi === SqliteUi return this.editColumn && this.sqlUi === SqliteUi;
}, },
isSQLite() { isSQLite() {
return this.sqlUi === SqliteUi return this.sqlUi === SqliteUi;
}, },
isMSSQL() { isMSSQL() {
return this.sqlUi === MssqlUi return this.sqlUi === MssqlUi;
}, },
dataTypes() { dataTypes() {
return this.sqlUi.getDataTypeListForUiType(this.newColumn) return this.sqlUi.getDataTypeListForUiType(this.newColumn);
}, },
isSelect() { isSelect() {
return this.newColumn && (this.newColumn.uidt === 'MultiSelect' || return (
this.newColumn.uidt === 'SingleSelect') this.newColumn &&
(this.newColumn.uidt === "MultiSelect" ||
this.newColumn.uidt === "SingleSelect")
);
}, },
isRelation() { isRelation() {
return this.newColumn && this.newColumn.uidt === 'ForeignKey' return this.newColumn && this.newColumn.uidt === "ForeignKey";
}, },
isLinkToAnotherRecord() { isLinkToAnotherRecord() {
return this.newColumn && this.newColumn.uidt === 'LinkToAnotherRecord' return this.newColumn && this.newColumn.uidt === "LinkToAnotherRecord";
}, },
isLookup() { isLookup() {
return this.newColumn && this.newColumn.uidt === 'Lookup' return this.newColumn && this.newColumn.uidt === "Lookup";
}, },
isRollup() { isRollup() {
return this.newColumn && this.newColumn.uidt === 'Rollup' return this.newColumn && this.newColumn.uidt === "Rollup";
}, },
relation() { relation() {
return this.meta && this.column && this.meta.belongsTo && this.meta.belongsTo.find(bt => bt.column_name === this.column.column_name) return (
this.meta &&
this.column &&
this.meta.belongsTo &&
this.meta.belongsTo.find(
(bt) => bt.column_name === this.column.column_name
)
);
}, },
isVirtual() { isVirtual() {
return this.isLinkToAnotherRecord || this.isLookup || this.isRollup return this.isLinkToAnotherRecord || this.isLookup || this.isRollup;
} },
}, },
watch: { watch: {
column() { column() {
this.genColumnData() this.genColumnData();
} },
}, },
async created() { async created() {
this.genColumnData() this.genColumnData();
}, },
mounted() { mounted() {
this.focusInput() this.focusInput();
}, },
methods: { methods: {
validateColumnName(v) { validateColumnName(v) {
return validateColumnName(v, this.$store.getters['project/GtrProjectIsGraphql']) return validateColumnName(
v,
this.$store.getters["project/GtrProjectIsGraphql"]
);
}, },
onRelColumnSelect(colMeta) { onRelColumnSelect(colMeta) {
Object.assign(this.newColumn, { Object.assign(this.newColumn, {
dt: colMeta.dt, dt: colMeta.dt,
dtxp: colMeta.dtxp, dtxp: colMeta.dtxp,
dtxs: colMeta.dtxs, dtxs: colMeta.dtxs,
un: colMeta.un un: colMeta.un,
}) });
}, },
genColumnData() { genColumnData() {
this.newColumn = this.column ? { ...this.column } : this.sqlUi.getNewColumn([...this.meta.columns, ...(this.meta.v || [])].length + 1) this.newColumn = this.column
this.newColumn.cno = this.newColumn.column_name ? { ...this.column }
: this.sqlUi.getNewColumn(
[...this.meta.columns, ...(this.meta.v || [])].length + 1
);
this.newColumn.cno = this.newColumn.column_name;
}, },
close() { close() {
this.$emit('close') this.$emit("close");
this.newColumn = {} this.newColumn = {};
}, },
async save() { async save() {
if (!this.$refs.form.validate()) { if (!this.$refs.form.validate()) {
return return;
} }
try { try {
if (this.newColumn.uidt === 'Formula') { if (this.newColumn.uidt === "Formula") {
await this.$refs.formula.save() await this.$refs.formula.save();
return this.$emit('saved') return this.$emit("saved");
// return this.$toast.info('Coming Soon...').goAway(3000) // return this.$toast.info('Coming Soon...').goAway(3000)
} }
if (this.isLinkToAnotherRecord && this.$refs.relation) { if (this.isLinkToAnotherRecord && this.$refs.relation) {
await this.$refs.relation.saveRelation() await this.$refs.relation.saveRelation();
return this.$emit('saved') return this.$emit("saved");
} }
if (this.isLookup && this.$refs.lookup) { if (this.isLookup && this.$refs.lookup) {
return await this.$refs.lookup.save() return await this.$refs.lookup.save();
} }
if (this.isRollup && this.$refs.rollup) { if (this.isRollup && this.$refs.rollup) {
return await this.$refs.rollup.save() return await this.$refs.rollup.save();
} }
if (this.newColumn.uidt === 'Formula' && this.$refs.formula) { if (this.newColumn.uidt === "Formula" && this.$refs.formula) {
return await this.$refs.formula.save() return await this.$refs.formula.save();
} }
this.newColumn.table_name = this.nodes.table_name this.newColumn.table_name = this.nodes.table_name;
this.newColumn.title = this.newColumn.column_name this.newColumn.title = this.newColumn.column_name;
if (this.editColumn) { if (this.editColumn) {
await this.$api.dbTableColumn.update(this.column.id, this.newColumn) await this.$api.dbTableColumn.update(this.column.id, this.newColumn);
} else { } else {
await this.$api.dbTableColumn.create(this.meta.id, this.newColumn) await this.$api.dbTableColumn.create(this.meta.id, this.newColumn);
} }
this.$emit('saved', this.newColumn.title, this.editColumn ? this.meta.columns[this.columnIndex].title : null) this.$emit(
"saved",
this.newColumn.title,
this.editColumn ? this.meta.columns[this.columnIndex].title : null
);
} catch (e) { } catch (e) {
console.log(e) console.log(e);
} }
this.$emit('close') this.$emit("close");
this.$tele.emit(`column:edit:save:${this.newColumn.uidt}`) this.$e("a:column:add", { datatype: this.newColumn.uidt });
}, },
onDataTypeChange() { onDataTypeChange() {
this.newColumn.rqd = false this.newColumn.rqd = false;
if (this.newColumn.uidt !== UITypes.ID) { if (this.newColumn.uidt !== UITypes.ID) {
this.newColumn.primaryKey = false this.newColumn.primaryKey = false;
} }
this.newColumn.ai = false this.newColumn.ai = false;
this.newColumn.cdf = null this.newColumn.cdf = null;
this.newColumn.un = false this.newColumn.un = false;
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt) this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(this.newColumn.dt) this.newColumn.dt
);
this.newColumn.dtx = 'specificType' this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(
this.newColumn.dt
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect] );
if (this.column && selectTypes.includes(this.newColumn.uidt) && selectTypes.includes(this.column.uidt)) {
this.newColumn.dtxp = this.column.dtxp this.newColumn.dtx = "specificType";
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect];
if (
this.column &&
selectTypes.includes(this.newColumn.uidt) &&
selectTypes.includes(this.column.uidt)
) {
this.newColumn.dtxp = this.column.dtxp;
} }
// this.$set(this.newColumn, 'uidt', this.sqlUi.getUIType(this.newColumn)); // this.$set(this.newColumn, 'uidt', this.sqlUi.getUIType(this.newColumn));
this.newColumn.altered = this.newColumn.altered || 2 this.newColumn.altered = this.newColumn.altered || 2;
}, },
onUiTypeChange() { onUiTypeChange() {
const colProp = this.sqlUi.getDataTypeForUiType(this.newColumn) const colProp = this.sqlUi.getDataTypeForUiType(this.newColumn);
this.newColumn = { this.newColumn = {
...this.newColumn, ...this.newColumn,
rqd: false, rqd: false,
@ -610,69 +738,77 @@ export default {
ai: false, ai: false,
cdf: null, cdf: null,
un: false, un: false,
dtx: 'specificType', dtx: "specificType",
...colProp ...colProp,
};
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(
this.newColumn.dt
);
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(
this.newColumn.dt
);
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect];
if (
this.column &&
selectTypes.includes(this.newColumn.uidt) &&
selectTypes.includes(this.column.uidt)
) {
this.newColumn.dtxp = this.column.dtxp;
} }
this.newColumn.dtxp = this.sqlUi.getDefaultLengthForDatatype(this.newColumn.dt) this.newColumn.altered = this.newColumn.altered || 2;
this.newColumn.dtxs = this.sqlUi.getDefaultScaleForDatatype(this.newColumn.dt)
const selectTypes = [UITypes.MultiSelect, UITypes.SingleSelect]
if (this.column && selectTypes.includes(this.newColumn.uidt) && selectTypes.includes(this.column.uidt)) {
this.newColumn.dtxp = this.column.dtxp
}
this.newColumn.altered = this.newColumn.altered || 2
}, },
focusInput() { focusInput() {
setTimeout(() => { setTimeout(() => {
if (this.$refs.column && this.$refs.column.$el) { if (this.$refs.column && this.$refs.column.$el) {
const el = this.$refs.column.$el.querySelector('input') const el = this.$refs.column.$el.querySelector("input");
el.focus() el.focus();
el.select() el.select();
} }
}, 100) }, 100);
}, },
async deleteRelation(action = '', column) { async deleteRelation(action = "", column) {
try { try {
if (action === 'showDialog') { if (action === "showDialog") {
this.relationDeleteDlg = true this.relationDeleteDlg = true;
} else if (action === 'hideDialog') { } else if (action === "hideDialog") {
this.relationDeleteDlg = false this.relationDeleteDlg = false;
} else { } else {
const result = await this.$store.dispatch('sqlMgr/ActSqlOpPlus', [ const result = await this.$store.dispatch("sqlMgr/ActSqlOpPlus", [
{ {
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias dbAlias: this.nodes.dbAlias,
}, },
this.relation.type === 'virtual' ? 'xcVirtualRelationDelete' : 'relationDelete', this.relation.type === "virtual"
? "xcVirtualRelationDelete"
: "relationDelete",
{ {
childColumn: this.relation.column_name, childColumn: this.relation.column_name,
childTable: this.nodes.table_name, childTable: this.nodes.table_name,
parentTable: this.relation parentTable: this.relation.rtn,
.rtn, parentColumn: this.relation.rcn,
parentColumn: this.relation },
.rcn ]);
} this.relationDeleteDlg = false;
]) this.relation = null;
this.relationDeleteDlg = false this.$toast.success("Foreign Key deleted successfully").goAway(3000);
this.relation = null this.$emit("onRelationDelete");
this.$toast.success('Foreign Key deleted successfully').goAway(3000)
this.$emit('onRelationDelete')
} }
} catch (e) { } catch (e) {
console.log(e) console.log(e);
this.$toast.error('Foreign key relation delete failed' + e).goAway(3000) this.$toast
throw e .error("Foreign key relation delete failed" + e)
.goAway(3000);
throw e;
} }
} },
} },
};
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
::v-deep { ::v-deep {
.wrapper { .wrapper {
border: solid 2px #7f828b33; border: solid 2px #7f828b33;
@ -687,7 +823,9 @@ export default {
border-color: #7f828b33 !important; border-color: #7f828b33 !important;
} }
.data-type, .ui-type, .formula-type { .data-type,
.ui-type,
.formula-type {
.v-input__append-inner { .v-input__append-inner {
margin-top: 4px !important; margin-top: 4px !important;
} }
@ -698,11 +836,11 @@ export default {
} }
.v-input--selection-controls__input > i { .v-input--selection-controls__input > i {
transform: scale(.83); transform: scale(0.83);
} }
label { label {
font-size: 0.75rem !important font-size: 0.75rem !important;
} }
.v-text-field--outlined.v-input--dense .v-label:not(.v-label--active) { .v-text-field--outlined.v-input--dense .v-label:not(.v-label--active) {
@ -732,12 +870,11 @@ export default {
top: 0; top: 0;
bottom: 0; bottom: 0;
background: var(--v-backgroundColor-base); background: var(--v-backgroundColor-base);
opacity: .9; opacity: 0.9;
& > * { & > * {
opacity: 1; opacity: 1;
} }
} }
} }
</style> </style>

2
packages/nc-gui/components/project/spreadsheet/components/editVirtualColumn.vue

@ -49,7 +49,7 @@
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
v-t="['virtual:column:edit']" v-t="['c:column:edit']"
x-small x-small
color="primary" color="primary"
:disabled="!valid" :disabled="!valid"

476
packages/nc-gui/components/project/spreadsheet/components/expandedForm.vue

@ -1,11 +1,9 @@
<template> <template>
<v-card width="1000" max-width="100%"> <v-card width="1000" max-width="100%">
<v-toolbar height="55" class="elevation-1"> <v-toolbar height="55" class="elevation-1">
<div class="d-100 d-flex "> <div class="d-100 d-flex">
<h5 class="title text-center"> <h5 class="title text-center">
<v-icon :color="iconColor"> <v-icon :color="iconColor"> mdi-table-arrow-right </v-icon>
mdi-table-arrow-right
</v-icon>
<template v-if="meta"> <template v-if="meta">
{{ meta.title }} {{ meta.title }}
@ -17,9 +15,7 @@
</h5> </h5>
<v-spacer /> <v-spacer />
<v-btn small text @click="reload"> <v-btn small text @click="reload">
<v-icon small> <v-icon small> mdi-reload </v-icon>
mdi-reload
</v-icon>
</v-btn> </v-btn>
<x-icon <x-icon
@ -30,59 +26,80 @@
text text
@click="toggleDrawer = !toggleDrawer" @click="toggleDrawer = !toggleDrawer"
> >
{{ toggleDrawer ? 'mdi-door-open' : 'mdi-door-closed' }} {{ toggleDrawer ? "mdi-door-open" : "mdi-door-closed" }}
</x-icon> </x-icon>
<v-btn small @click="$emit('cancel')"> <v-btn small @click="$emit('cancel')">
<!-- Cancel --> <!-- Cancel -->
{{ $t('general.cancel') }} {{ $t("general.cancel") }}
</v-btn> </v-btn>
<v-btn :disabled="!_isUIAllowed('tableRowUpdate')" small color="primary" @click="save"> <v-btn
:disabled="!_isUIAllowed('tableRowUpdate')"
small
color="primary"
@click="save"
>
<!--Save Row--> <!--Save Row-->
{{ $t('activity.saveRow') }} {{ $t("activity.saveRow") }}
</v-btn> </v-btn>
</div> </div>
</v-toolbar> </v-toolbar>
<div class="form-container "> <div class="form-container">
<v-card-text <v-card-text
class=" py-0 px-0 " class="py-0 px-0"
:class="{ :class="{
'px-10' : isNew || !toggleDrawer, 'px-10': isNew || !toggleDrawer,
}" }"
> >
<v-breadcrumbs <v-breadcrumbs
v-if="localBreadcrumbs && localBreadcrumbs.length" v-if="localBreadcrumbs && localBreadcrumbs.length"
class="caption pt-0 pb-2 justify-center d-100" class="caption pt-0 pb-2 justify-center d-100"
:items="localBreadcrumbs.map(text => ({text}))" :items="localBreadcrumbs.map((text) => ({ text }))"
/> />
<v-container fluid style="height:70vh" class="py-0"> <v-container fluid style="height: 70vh" class="py-0">
<v-row class="h-100"> <v-row class="h-100">
<v-col class="h-100 px-10" style="overflow-y: auto" cols="8" :offset="isNew || !toggleDrawer ? 2 : 0"> <v-col
class="h-100 px-10"
style="overflow-y: auto"
cols="8"
:offset="isNew || !toggleDrawer ? 2 : 0"
>
<div v-if="showNextPrev" class="d-flex my-4"> <div v-if="showNextPrev" class="d-flex my-4">
<x-icon tooltip="Previous record" small outlined @click="$emit('prev', localState)"> <x-icon
tooltip="Previous record"
small
outlined
@click="$emit('prev', localState)"
>
mdi-arrow-left-bold-outline mdi-arrow-left-bold-outline
</x-icon> </x-icon>
<span class="flex-grow-1" /> <span class="flex-grow-1" />
<x-icon tooltip="Next record" small outlined @click="$emit('next', localState)"> <x-icon
tooltip="Next record"
small
outlined
@click="$emit('next', localState)"
>
mdi-arrow-right-bold-outline mdi-arrow-right-bold-outline
</x-icon> </x-icon>
</div> </div>
<template <template v-for="(col, i) in fields">
v-for="(col,i) in fields"
>
<div <div
v-if="!col.lk && (!showFields || showFields[col.title])" v-if="!col.lk && (!showFields || showFields[col.title])"
:key="i" :key="i"
:class="{ :class="{
'active-row' : active === col.title, 'active-row': active === col.title,
required: isValid(col, localState) required: isValid(col, localState),
}" }"
class="row-col my-4" class="row-col my-4"
> >
<div> <div>
<label :for="`data-table-form-${col.title}`" class="body-2 text-capitalize"> <label
:for="`data-table-form-${col.title}`"
class="body-2 text-capitalize"
>
<virtual-header-cell <virtual-header-cell
v-if="col.colOptions" v-if="col.colOptions"
:column="col" :column="col"
@ -98,7 +115,6 @@
:column="col" :column="col"
:sql-ui="sqlUi" :sql-ui="sqlUi"
/> />
</label> </label>
<virtual-cell <virtual-cell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
@ -114,21 +130,30 @@
:is-form="true" :is-form="true"
:breadcrumbs="localBreadcrumbs" :breadcrumbs="localBreadcrumbs"
@updateCol="updateCol" @updateCol="updateCol"
@newRecordsSaved="$listeners.loadTableData|| reload" @newRecordsSaved="$listeners.loadTableData || reload"
/> />
<div <div
v-else-if="col.ai || (col.pk && !isNew) || disabledColumns[col.title]" v-else-if="
style="height:100%; width:100%" col.ai ||
(col.pk && !isNew) ||
disabledColumns[col.title]
"
style="height: 100%; width: 100%"
class="caption xc-input" class="caption xc-input"
@click="col.ai && $toast.info('Auto Increment field is not editable').goAway(3000)" @click="
col.ai &&
$toast
.info('Auto Increment field is not editable')
.goAway(3000)
"
> >
<input <input
style="height:100%; width: 100%" style="height: 100%; width: 100%"
readonly readonly
disabled disabled
:value="localState[col.title]" :value="localState[col.title]"
> />
</div> </div>
<editable-cell <editable-cell
@ -144,7 +169,7 @@
:is-locked="isLocked" :is-locked="isLocked"
@focus="active = col.title" @focus="active = col.title"
@blur="active = ''" @blur="active = ''"
@input="$set(changedColumns,col.title, true)" @input="$set(changedColumns, col.title, true)"
/> />
</div> </div>
</div> </div>
@ -153,45 +178,70 @@
<v-col <v-col
v-if="!isNew && toggleDrawer" v-if="!isNew && toggleDrawer"
cols="4" cols="4"
class="d-flex flex-column h-100 flex-grow-1 blue-grey " class="d-flex flex-column h-100 flex-grow-1 blue-grey"
:class="{ :class="{
'lighten-5':!$vuetify.theme.dark, 'lighten-5': !$vuetify.theme.dark,
'darken-4':$vuetify.theme.dark 'darken-4': $vuetify.theme.dark,
}" }"
> >
<v-skeleton-loader v-if="loadingLogs && !logs" type="list-item-avatar-two-line@8" /> <v-skeleton-loader
v-if="loadingLogs && !logs"
type="list-item-avatar-two-line@8"
/>
<v-list <v-list
v-else v-else
ref="commentsList" ref="commentsList"
width="100%" width="100%"
style="overflow-y: auto; overflow-x: auto" style="overflow-y: auto; overflow-x: auto"
class="blue-grey " class="blue-grey"
:class="{ :class="{
'lighten-5':!$vuetify.theme.dark, 'lighten-5': !$vuetify.theme.dark,
'darken-4':$vuetify.theme.dark 'darken-4': $vuetify.theme.dark,
}" }"
> >
<div> <div>
<v-list-item v-for="log in logs" :key="log.id" class="d-flex"> <v-list-item v-for="log in logs" :key="log.id" class="d-flex">
<v-list-item-icon class="ma-0 mr-2"> <v-list-item-icon class="ma-0 mr-2">
<v-icon :color="isYou(log.user) ? 'pink lighten-2' : 'blue lighten-2'"> <v-icon
:color="
isYou(log.user) ? 'pink lighten-2' : 'blue lighten-2'
"
>
mdi-account-circle mdi-account-circle
</v-icon> </v-icon>
</v-list-item-icon> </v-list-item-icon>
<div class="flex-grow-1" style="min-width: 0"> <div class="flex-grow-1" style="min-width: 0">
<p class="mb-1 caption edited-text"> <p class="mb-1 caption edited-text">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }} {{ {{
log.op_type === 'COMMENT' ? 'commented' : ( isYou(log.user)
log.op_sub_type === 'INSERT' ? 'created' : 'edited' ? "You"
) : log.user == null
? "Shared base"
: log.user
}}
{{
log.op_type === "COMMENT"
? "commented"
: log.op_sub_type === "INSERT"
? "created"
: "edited"
}} }}
</p> </p>
<p v-if="log.op_type === 'COMMENT'" class="caption mb-0 nc-chip" :style="{background :colors[2]}"> <p
v-if="log.op_type === 'COMMENT'"
class="caption mb-0 nc-chip"
:style="{ background: colors[2] }"
>
{{ log.description }} {{ log.description }}
</p> </p>
<p v-else class="caption mb-0" style="word-break: break-all;" v-html="log.details" /> <p
v-else
class="caption mb-0"
style="word-break: break-all"
v-html="log.details"
/>
<p class="time text-right mb-0"> <p class="time text-right mb-0">
{{ calculateDiff(log.created_at) }} {{ calculateDiff(log.created_at) }}
@ -206,7 +256,7 @@
<div class="d-flex align-center justify-center"> <div class="d-flex align-center justify-center">
<v-switch <v-switch
v-model="commentsOnly" v-model="commentsOnly"
v-t="['record:comment:comments-only']" v-t="['c:row-expand:comment-only']"
class="mt-1" class="mt-1"
dense dense
hide-details hide-details
@ -229,9 +279,9 @@
solo solo
hide-details hide-details
class="caption comment-box" class="caption comment-box"
:class="{ focus : showborder }" :class="{ focus: showborder }"
@focusin=" showborder = true" @focusin="showborder = true"
@focusout=" showborder = false" @focusout="showborder = false"
@keyup.enter.prevent="saveComment" @keyup.enter.prevent="saveComment"
> >
<template v-if="comment" #append> <template v-if="comment" #append>
@ -250,7 +300,7 @@
<v-btn <v-btn
v-if="_isUIAllowed('rowComments')" v-if="_isUIAllowed('rowComments')"
v-show="!toggleDrawer" v-show="!toggleDrawer"
v-t="['record:comment-toggle']" v-t="['c:row-expand:comment-toggle']"
class="comment-icon" class="comment-icon"
color="primary" color="primary"
fab fab
@ -262,40 +312,44 @@
</template> </template>
<script> <script>
import dayjs from "dayjs";
import dayjs from 'dayjs' import {
import { AuditOperationSubTypes, AuditOperationTypes, isVirtualCol, UITypes } from 'nocodb-sdk' AuditOperationSubTypes,
import form from '../mixins/form' AuditOperationTypes,
import HeaderCell from '@/components/project/spreadsheet/components/headerCell' isVirtualCol,
import EditableCell from '@/components/project/spreadsheet/components/editableCell' UITypes,
import colors from '@/mixins/colors' } from "nocodb-sdk";
import VirtualCell from '@/components/project/spreadsheet/components/virtualCell' import form from "../mixins/form";
import VirtualHeaderCell from '@/components/project/spreadsheet/components/virtualHeaderCell' import HeaderCell from "@/components/project/spreadsheet/components/headerCell";
import EditableCell from "@/components/project/spreadsheet/components/editableCell";
const relativeTime = require('dayjs/plugin/relativeTime') import colors from "@/mixins/colors";
const utc = require('dayjs/plugin/utc') import VirtualCell from "@/components/project/spreadsheet/components/virtualCell";
dayjs.extend(utc) import VirtualHeaderCell from "@/components/project/spreadsheet/components/virtualHeaderCell";
dayjs.extend(relativeTime)
const relativeTime = require("dayjs/plugin/relativeTime");
const utc = require("dayjs/plugin/utc");
dayjs.extend(utc);
dayjs.extend(relativeTime);
export default { export default {
name: 'ExpandedForm', name: "ExpandedForm",
components: { components: {
VirtualHeaderCell, VirtualHeaderCell,
VirtualCell, VirtualCell,
EditableCell, EditableCell,
HeaderCell HeaderCell,
}, },
mixins: [colors, form], mixins: [colors, form],
props: { props: {
showFields: Object, showFields: Object,
showNextPrev: { showNextPrev: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
breadcrumbs: { breadcrumbs: {
type: Array, type: Array,
default() { default() {
return [] return [];
} },
}, },
dbAlias: String, dbAlias: String,
value: Object, value: Object,
@ -307,13 +361,13 @@ export default {
oldRow: Object, oldRow: Object,
iconColor: { iconColor: {
type: String, type: String,
default: 'primary' default: "primary",
}, },
availableColumns: [Object, Array], availableColumns: [Object, Array],
queryParams: Object, queryParams: Object,
meta: Object, meta: Object,
presetValues: Object, presetValues: Object,
isLocked: Boolean isLocked: Boolean,
}, },
data: () => ({ data: () => ({
isVirtualCol, isVirtualCol,
@ -322,228 +376,286 @@ export default {
loadingLogs: true, loadingLogs: true,
toggleDrawer: false, toggleDrawer: false,
logs: null, logs: null,
active: '', active: "",
localState: {}, localState: {},
changedColumns: {}, changedColumns: {},
comment: null, comment: null,
showSystemFields: false, showSystemFields: false,
commentsOnly: false commentsOnly: false,
}), }),
computed: { computed: {
primaryKey() { primaryKey() {
return this.isNew ? '' : this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___') return this.isNew
? ""
: this.meta.columns
.filter((c) => c.pk)
.map((c) => this.localState[c.title])
.join("___");
}, },
edited() { edited() {
return !!Object.keys(this.changedColumns).length return !!Object.keys(this.changedColumns).length;
}, },
fields() { fields() {
if (this.availableColumns) { if (this.availableColumns) {
return this.availableColumns return this.availableColumns;
} }
const hideCols = ['created_at', 'updated_at'] const hideCols = ["created_at", "updated_at"];
if (this.showSystemFields) { if (this.showSystemFields) {
return this.meta.columns || [] return this.meta.columns || [];
} else { } else {
return this.meta.columns.filter(c => !(c.pk && c.ai) && !hideCols.includes(c.column_name) && return (
!((this.meta.v || []).some(v => v.bt && v.bt.column_name === c.column_name)) this.meta.columns.filter(
) || [] (c) =>
!(c.pk && c.ai) &&
!hideCols.includes(c.column_name) &&
!(this.meta.v || []).some(
(v) => v.bt && v.bt.column_name === c.column_name
)
) || []
);
} }
}, },
isChanged() { isChanged() {
return Object.values(this.changedColumns).some(Boolean) return Object.values(this.changedColumns).some(Boolean);
}, },
localBreadcrumbs() { localBreadcrumbs() {
return [...this.breadcrumbs, `${this.meta ? this.meta.title : this.table} ${this.primaryValue() ? `(${this.primaryValue()})` : ''}`] return [
} ...this.breadcrumbs,
`${this.meta ? this.meta.title : this.table} ${
this.primaryValue() ? `(${this.primaryValue()})` : ""
}`,
];
},
}, },
watch: { watch: {
value(obj) { value(obj) {
this.localState = { ...obj } this.localState = { ...obj };
if (!this.isNew && this.toggleDrawer) { if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments() this.getAuditsAndComments();
} }
}, },
isNew(n) { isNew(n) {
if (!n && this.toggleDrawer) { if (!n && this.toggleDrawer) {
this.getAuditsAndComments() this.getAuditsAndComments();
} }
}, },
meta() { meta() {
if (!this.isNew && this.toggleDrawer) { if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments() this.getAuditsAndComments();
} }
}, },
toggleDrawer(td) { toggleDrawer(td) {
if (td) { if (td) {
this.getAuditsAndComments() this.getAuditsAndComments();
} }
} },
}, },
created() { created() {
this.localState = { ...this.value } this.localState = { ...this.value };
if (!this.isNew && this.toggleDrawer) { if (!this.isNew && this.toggleDrawer) {
this.getAuditsAndComments() this.getAuditsAndComments();
} }
}, },
methods: { methods: {
updateCol(_row, _cn, pid) { updateCol(_row, _cn, pid) {
this.$set(this.localState, _cn, pid) this.$set(this.localState, _cn, pid);
this.$set(this.changedColumns, _cn, true) this.$set(this.changedColumns, _cn, true);
}, },
isYou(email) { isYou(email) {
return this.$store.state.users.user && this.$store.state.users.user.email === email return (
this.$store.state.users.user &&
this.$store.state.users.user.email === email
);
}, },
async getAuditsAndComments() { async getAuditsAndComments() {
this.loadingLogs = true this.loadingLogs = true;
const data = (await this.$api.utils.commentList({ const data = await this.$api.utils.commentList({
row_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___'), row_id: this.meta.columns
.filter((c) => c.pk)
.map((c) => this.localState[c.title])
.join("___"),
fk_model_id: this.meta.id, fk_model_id: this.meta.id,
comments_only: this.commentsOnly comments_only: this.commentsOnly,
})) });
this.logs = data.reverse() this.logs = data.reverse();
this.loadingLogs = false this.loadingLogs = false;
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.commentsList && this.$refs.commentsList.$el && this.$refs.commentsList.$el.firstElementChild) { if (
this.$refs.commentsList.$el.scrollTop = this.$refs.commentsList.$el.firstElementChild.offsetHeight this.$refs.commentsList &&
this.$refs.commentsList.$el &&
this.$refs.commentsList.$el.firstElementChild
) {
this.$refs.commentsList.$el.scrollTop =
this.$refs.commentsList.$el.firstElementChild.offsetHeight;
} }
}) });
}, },
async save() { async save() {
try { try {
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___') const id = this.meta.columns
.filter((c) => c.pk)
.map((c) => this.localState[c.title])
.join("___");
if (this.presetValues) { if (this.presetValues) {
// cater presetValues // cater presetValues
for (const k in this.presetValues) { for (const k in this.presetValues) {
this.$set(this.changedColumns, k, true) this.$set(this.changedColumns, k, true);
} }
} }
const updatedObj = Object.keys(this.changedColumns).reduce((obj, col) => { const updatedObj = Object.keys(this.changedColumns).reduce(
obj[col] = this.localState[col] (obj, col) => {
return obj obj[col] = this.localState[col];
}, {}) return obj;
},
{}
);
if (this.isNew) { if (this.isNew) {
const data = (await this.$api.dbTableRow.create( const data = await this.$api.dbTableRow.create(
'noco', "noco",
this.projectName, this.projectName,
this.meta.title, updatedObj)) this.meta.title,
this.localState = { ...this.localState, ...data } updatedObj
);
this.localState = { ...this.localState, ...data };
// save hasmany and manytomany relations from local state // save hasmany and manytomany relations from local state
if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) { if (this.$refs.virtual && Array.isArray(this.$refs.virtual)) {
for (const vcell of this.$refs.virtual) { for (const vcell of this.$refs.virtual) {
if (vcell.save) { if (vcell.save) {
await vcell.save(this.localState) await vcell.save(this.localState);
} }
} }
} }
await this.reload() await this.reload();
} else if (Object.keys(updatedObj).length) { } else if (Object.keys(updatedObj).length) {
if (!id) { if (!id) {
return this.$toast.info('Update not allowed for table which doesn\'t have primary Key').goAway(3000) return this.$toast
.info(
"Update not allowed for table which doesn't have primary Key"
)
.goAway(3000);
} }
await this.$api.dbTableRow.update( await this.$api.dbTableRow.update(
'noco', "noco",
this.projectName, this.projectName,
this.meta.title, this.meta.title,
id, id,
updatedObj updatedObj
) );
for (const key of Object.keys(updatedObj)) { for (const key of Object.keys(updatedObj)) {
// audit // audit
this.$api.utils.auditRowUpdate(id, { this.$api.utils
fk_model_id: this.meta.id, .auditRowUpdate(id, {
column_name: key, fk_model_id: this.meta.id,
row_id: id, column_name: key,
value: updatedObj[key], row_id: id,
prev_value: this.oldRow[key] value: updatedObj[key],
}).then(() => { prev_value: this.oldRow[key],
}) })
.then(() => {});
} }
} else { } else {
return this.$toast.info('No columns to update').goAway(3000) return this.$toast.info("No columns to update").goAway(3000);
} }
this.$emit('update:oldRow', { ...this.localState }) this.$emit("update:oldRow", { ...this.localState });
this.changedColumns = {} this.changedColumns = {};
this.$emit('input', this.localState) this.$emit("input", this.localState);
this.$emit('update:isNew', false) this.$emit("update:isNew", false);
this.$toast.success(`${this.primaryValue() || 'Row'} updated successfully.`, { this.$toast
position: 'bottom-right' .success(`${this.primaryValue() || "Row"} updated successfully.`, {
}).goAway(3000) position: "bottom-right",
})
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(`Failed to update row : ${e.message}`).goAway(3000) this.$toast.error(`Failed to update row : ${e.message}`).goAway(3000);
} }
this.$tele.emit('record:add:submit') this.$e("a:row-expand:add");
}, },
async reload() { async reload() {
const id = this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___') const id = this.meta.columns
this.$set(this, 'changedColumns', {}) .filter((c) => c.pk)
this.localState = (await this.$api.dbTableRow.read( .map((c) => this.localState[c.title])
'noco', .join("___");
this.$set(this, "changedColumns", {});
this.localState = await this.$api.dbTableRow.read(
"noco",
this.projectName, this.projectName,
this.meta.title, id, { query: this.queryParams || {} })) this.meta.title,
id,
{ query: this.queryParams || {} }
);
}, },
calculateDiff(date) { calculateDiff(date) {
return dayjs.utc(date).fromNow() return dayjs.utc(date).fromNow();
}, },
async saveComment() { async saveComment() {
try { try {
await this.$api.utils.commentRow({ await this.$api.utils.commentRow({
fk_model_id: this.meta.id, fk_model_id: this.meta.id,
row_id: this.meta.columns.filter(c => c.pk).map(c => this.localState[c.title]).join('___'), row_id: this.meta.columns
description: this.comment .filter((c) => c.pk)
}) .map((c) => this.localState[c.title])
.join("___"),
this.comment = '' description: this.comment,
this.$toast.success('Comment added successfully').goAway(3000) });
this.$emit('commented')
await this.getAuditsAndComments() this.comment = "";
this.$toast.success("Comment added successfully").goAway(3000);
this.$emit("commented");
await this.getAuditsAndComments();
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit('record:comment:insert') this.$e("a:row-expand:comment");
}, },
primaryValue() { primaryValue() {
if (this.localState) { if (this.localState) {
const value = this.localState[this.primaryValueColumn] const value = this.localState[this.primaryValueColumn];
const col = this.meta.columns.find(c => c.title == this.primaryValueColumn) const col = this.meta.columns.find(
(c) => c.title == this.primaryValueColumn
);
if (!col) { if (!col) {
return return;
} }
const uidt = col.uidt const uidt = col.uidt;
if (uidt == UITypes.Date) { if (uidt == UITypes.Date) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD') return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format(
"YYYY-MM-DD"
);
} else if (uidt == UITypes.DateTime) { } else if (uidt == UITypes.DateTime) {
return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format('YYYY-MM-DD HH:mm') return (/^\d+$/.test(value) ? dayjs(+value) : dayjs(value)).format(
"YYYY-MM-DD HH:mm"
);
} else if (uidt == UITypes.Time) { } else if (uidt == UITypes.Time) {
let dateTime = dayjs(value) let dateTime = dayjs(value);
if (!dateTime.isValid()) { if (!dateTime.isValid()) {
dateTime = dayjs(value, 'HH:mm:ss') dateTime = dayjs(value, "HH:mm:ss");
} }
if (!dateTime.isValid()) { if (!dateTime.isValid()) {
dateTime = dayjs(`1999-01-01 ${value}`) dateTime = dayjs(`1999-01-01 ${value}`);
} }
if (!dateTime.isValid()) { if (!dateTime.isValid()) {
return value return value;
} }
return dateTime.format('HH:mm:ss') return dateTime.format("HH:mm:ss");
} }
return value return value;
} }
} },
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -552,7 +664,8 @@ export default {
font-weight: 700; font-weight: 700;
} }
.row-col:focus > label, .active-row > label { .row-col:focus > label,
.active-row > label {
color: var(--v-primary-base); color: var(--v-primary-base);
} }
@ -563,15 +676,14 @@ export default {
} }
::v-deep { ::v-deep {
.v-breadcrumbs__item:nth-child(odd) { .v-breadcrumbs__item:nth-child(odd) {
font-size: .72rem; font-size: 0.72rem;
color: grey; color: grey;
} }
.v-breadcrumbs li:nth-child(even) { .v-breadcrumbs li:nth-child(even) {
padding: 0 6px; padding: 0 6px;
font-size: .72rem; font-size: 0.72rem;
color: var(--v-textColor-base); color: var(--v-textColor-base);
} }
@ -594,7 +706,7 @@ export default {
& > div textarea:not(.inputarea) { & > div textarea:not(.inputarea) {
border: 1px solid #7f828b33; border: 1px solid #7f828b33;
padding: 1px 5px; padding: 1px 5px;
font-size: .8rem; font-size: 0.8rem;
border-radius: 4px; border-radius: 4px;
min-height: 44px; min-height: 44px;
@ -605,13 +717,10 @@ export default {
&:hover:not(:focus) { &:hover:not(:focus) {
box-shadow: 0 0 2px dimgrey; box-shadow: 0 0 2px dimgrey;
} }
} }
} }
&.v-card { &.v-card {
&.theme--dark .v-card__text { &.theme--dark .v-card__text {
background: #363636; background: #363636;
@ -643,9 +752,7 @@ export default {
} }
} }
} }
} }
} }
h5 { h5 {
@ -658,8 +765,9 @@ h5 {
overflow: auto; overflow: auto;
} }
.time, .edited-text { .time,
font-size: .65rem; .edited-text {
font-size: 0.65rem;
color: grey; color: grey;
} }

10
packages/nc-gui/components/project/spreadsheet/components/extras.vue

@ -32,19 +32,19 @@
> >
<v-list-item> <v-list-item>
<div class="d-flex justify-space-between d-100 pr-2"> <div class="d-flex justify-space-between d-100 pr-2">
<v-icon v-t="['community:discord']" class="mr-1" size="22" :color="textColors[0]" @click="open('https://discord.gg/5RgZmkW')"> <v-icon v-t="['e:community:discord']" class="mr-1" size="22" :color="textColors[0]" @click="open('https://discord.gg/5RgZmkW')">
mdi-discord mdi-discord
</v-icon> </v-icon>
<v-icon v-t="['community:discourse']" class="mr-1 discourse" size="22" :color="textColors[0]" @click="open('https://community.nocodb.com/')"> <v-icon v-t="['e:community:discourse']" class="mr-1 discourse" size="22" :color="textColors[0]" @click="open('https://community.nocodb.com/')">
mdi-discourse mdi-discourse
</v-icon> </v-icon>
<v-icon v-t="['community:discord']" class="mr-1" size="22" color="#ff4600" @click="open('https://www.reddit.com/r/NocoDB/')"> <v-icon v-t="['e:community:reddit']" class="mr-1" size="22" color="#ff4600" @click="open('https://www.reddit.com/r/NocoDB/')">
mdi-reddit mdi-reddit
</v-icon> </v-icon>
<v-icon v-t="['community:twitter']" class="mr-1" size="22" :color="textColors[1]" @click="open('https://twitter.com/NocoDB')"> <v-icon v-t="['e:community:twitter']" class="mr-1" size="22" :color="textColors[1]" @click="open('https://twitter.com/NocoDB')">
mdi-twitter mdi-twitter
</v-icon> </v-icon>
<v-icon v-t="['community:book-demo']" class="mr-1" size="22" :color="textColors[3]" @click="open('https://calendly.com/nocodb-meeting')"> <v-icon v-t="['e:community:book-demo']" class="mr-1" size="22" :color="textColors[3]" @click="open('https://calendly.com/nocodb-meeting')">
mdi-calendar-month mdi-calendar-month
</v-icon> </v-icon>
</div> </div>

344
packages/nc-gui/components/project/spreadsheet/components/fieldsMenu.vue

@ -1,30 +1,23 @@
<template> <template>
<v-menu offset-y> <v-menu offset-y>
<template #activator="{ on }"> <template #activator="{ on }">
<v-badge <v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
:value="isAnyFieldHidden"
color="primary"
dot
overlap
>
<v-btn <v-btn
v-t="['fields:trigger']" v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border" class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked" :disabled="isLocked"
outlined outlined
small small
text text
:class=" { 'primary lighten-5 grey--text text--darken-3' : isAnyFieldHidden}" :class="{
'primary lighten-5 grey--text text--darken-3': isAnyFieldHidden,
}"
v-on="on" v-on="on"
> >
<v-icon small class="mr-1" color="#777"> <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon>
mdi-eye-off-outline
</v-icon>
<!-- Fields --> <!-- Fields -->
{{ $t('objects.fields') }} {{ $t("objects.fields") }}
<v-icon small color="#777"> <v-icon small color="#777"> mdi-menu-down </v-icon>
mdi-menu-down
</v-icon>
</v-btn> </v-btn>
</v-badge> </v-badge>
</template> </template>
@ -45,9 +38,7 @@
@click.stop @click.stop
> >
<template #prepend-inner> <template #prepend-inner>
<v-icon small class="field-icon"> <v-icon small class="field-icon"> mdi-image </v-icon>
mdi-image
</v-icon>
</template> </template>
</v-select> </v-select>
</div> </div>
@ -69,19 +60,14 @@
@click.stop @click.stop
> >
<template #prepend-inner> <template #prepend-inner>
<v-icon small class="field-icon"> <v-icon small class="field-icon"> mdi-select-group </v-icon>
mdi-select-group
</v-icon>
</template> </template>
</v-select> </v-select>
</div> </div>
<v-divider /> <v-divider />
</template> </template>
<v-list-item <v-list-item dense class="">
dense
class=""
>
<v-text-field <v-text-field
v-model="fieldFilter" v-model="fieldFilter"
dense dense
@ -101,16 +87,21 @@
</v-list-item> </v-list-item>
<draggable <draggable
v-model="fields" v-model="fields"
@start="drag=true" @start="drag = true"
@end="drag=false" @end="drag = false"
@change="onMove($event)" @change="onMove($event)"
> >
<template <template v-for="(field, i) in fields">
v-for="(field,i) in fields"
>
<v-list-item <v-list-item
v-show="(!fieldFilter || (field.title||'').toLowerCase().includes(fieldFilter.toLowerCase())) v-show="
&& !(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id)) (!fieldFilter ||
(field.title || '')
.toLowerCase()
.includes(fieldFilter.toLowerCase())) &&
!(
!showSystemFieldsLoc &&
systemColumnsIds.includes(field.fk_column_id)
)
" "
:key="field.id" :key="field.id"
dense dense
@ -138,9 +129,7 @@
</v-list-item> </v-list-item>
</template> </template>
</draggable> </draggable>
<v-divider <v-divider class="my-2" />
class="my-2"
/>
<v-list-item v-if="!isPublic" dense> <v-list-item v-if="!isPublic" dense>
<v-checkbox <v-checkbox
@ -153,7 +142,7 @@
<template #label> <template #label>
<span class="caption"> <span class="caption">
<!-- Show System Fields --> <!-- Show System Fields -->
{{ $t('activity.showSystemFields') }} {{ $t("activity.showSystemFields") }}
</span> </span>
</template> </template>
</v-checkbox> </v-checkbox>
@ -161,11 +150,11 @@
<v-list-item dense class="mt-2 list-btn mb-3"> <v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey--text" @click.stop="showAll"> <v-btn small class="elevation-0 grey--text" @click.stop="showAll">
<!-- Show All --> <!-- Show All -->
{{ $t('general.showAll') }} {{ $t("general.showAll") }}
</v-btn> </v-btn>
<v-btn small class="elevation-0 grey--text" @click.stop="hideAll"> <v-btn small class="elevation-0 grey--text" @click.stop="hideAll">
<!-- Hide All --> <!-- Hide All -->
{{ $t('general.hideAll') }} {{ $t("general.hideAll") }}
</v-btn> </v-btn>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -173,13 +162,13 @@
</template> </template>
<script> <script>
import draggable from 'vuedraggable' import draggable from "vuedraggable";
import { getSystemColumnsIds } from 'nocodb-sdk' import { getSystemColumnsIds } from "nocodb-sdk";
export default { export default {
name: 'FieldsMenu', name: "FieldsMenu",
components: { components: {
draggable draggable,
}, },
props: { props: {
coverImageField: String, coverImageField: String,
@ -193,208 +182,278 @@ export default {
fieldList: [Array, Object], fieldList: [Array, Object],
showSystemFields: { showSystemFields: {
type: [Boolean, Number], type: [Boolean, Number],
default: false default: false,
}, },
isLocked: Boolean, isLocked: Boolean,
isPublic: Boolean, isPublic: Boolean,
viewId: String viewId: String,
}, },
data: () => ({ data: () => ({
fields: [], fields: [],
fieldFilter: '', fieldFilter: "",
showFields: {}, showFields: {},
fieldsOrderLoc: [] fieldsOrderLoc: [],
}), }),
computed: { computed: {
systemColumnsIds() { systemColumnsIds() {
return getSystemColumnsIds(this.meta && this.meta.columns) return getSystemColumnsIds(this.meta && this.meta.columns);
}, },
attachmentFields() { attachmentFields() {
return [...(this.meta && this.meta.columns ? this.meta.columns.filter(f => f.uidt === 'Attachment') : []), { return [
alias: 'None', ...(this.meta && this.meta.columns
id: null ? this.meta.columns.filter((f) => f.uidt === "Attachment")
}] : []),
{
alias: "None",
id: null,
},
];
}, },
singleSelectFields() { singleSelectFields() {
return [...(this.meta && this.meta.columns ? this.meta.columns.filter(f => f.uidt === 'SingleSelect') : []), { return [
alias: 'None', ...(this.meta && this.meta.columns
id: null ? this.meta.columns.filter((f) => f.uidt === "SingleSelect")
}] : []),
{
alias: "None",
id: null,
},
];
}, },
coverImageFieldLoc: { coverImageFieldLoc: {
get() { get() {
return this.coverImageField return this.coverImageField;
}, },
set(val) { set(val) {
this.$emit('update:coverImageField', val) this.$emit("update:coverImageField", val);
} },
}, },
groupingFieldLoc: { groupingFieldLoc: {
get() { get() {
return this.groupingField return this.groupingField;
}, },
set(val) { set(val) {
this.$emit('update:groupingField', val) this.$emit("update:groupingField", val);
} },
}, },
columnMeta() { columnMeta() {
return this.meta && this.meta.columns return this.meta && this.meta.columns
? this.meta.columns.reduce((o, c) => ({ ? this.meta.columns.reduce(
...o, (o, c) => ({
[c.title]: c ...o,
}), {}) [c.title]: c,
: {} }),
{}
)
: {};
}, },
isAnyFieldHidden() { isAnyFieldHidden() {
return this.fields.some(f => !(!this.showSystemFieldsLoc && this.systemColumnsIds.includes(f.fk_column_id)) && return this.fields.some(
!f.show (f) =>
)// Object.values(this.showFields).some(v => !v) !(
!this.showSystemFieldsLoc &&
this.systemColumnsIds.includes(f.fk_column_id)
) && !f.show
); // Object.values(this.showFields).some(v => !v)
}, },
showSystemFieldsLoc: { showSystemFieldsLoc: {
get() { get() {
return this.showSystemFields return this.showSystemFields;
}, },
set(v) { set(v) {
this.$emit('update:showSystemFields', v) this.$emit("update:showSystemFields", v);
this.showFields = this.fields.reduce((o, c) => ({ [c.title]: c.show, ...o }), {}) this.showFields = this.fields.reduce(
this.$emit('update:fieldsOrder', this.fields.map(c => c.title)) (o, c) => ({ [c.title]: c.show, ...o }),
{}
);
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
this.$tele.emit('fields:system-field-checkbox') this.$e("a:fields:system-fields");
} },
} },
}, },
watch: { watch: {
async viewId(v) { async viewId(v) {
if (v) { if (v) {
await this.loadFields() await this.loadFields();
} }
}, },
fieldList(f) { fieldList(f) {
this.fieldsOrderLoc = [...f] this.fieldsOrderLoc = [...f];
}, },
showFields: { showFields: {
handler(v) { handler(v) {
this.$nextTick(() => { this.$nextTick(() => {
this.$emit('input', v) this.$emit("input", v);
}) });
}, },
deep: true deep: true,
}, },
value(v) { value(v) {
this.showFields = v || [] this.showFields = v || [];
}, },
fieldsOrder(n, o) { fieldsOrder(n, o) {
if ((n && n.join()) !== (o && o.join())) { if ((n && n.join()) !== (o && o.join())) {
this.fieldsOrderLoc = n this.fieldsOrderLoc = n;
} }
this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList] this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList];
}, },
fieldsOrderLoc: { fieldsOrderLoc: {
handler(n, o) { handler(n, o) {
if ((n && n.join()) !== (o && o.join())) { if ((n && n.join()) !== (o && o.join())) {
this.$emit('update:fieldsOrder', n) this.$emit("update:fieldsOrder", n);
} }
}, },
deep: true deep: true,
} },
}, },
created() { created() {
this.loadFields() this.loadFields();
this.showFields = this.value this.showFields = this.value;
this.fieldsOrderLoc = this.fieldsOrder && this.fieldsOrder.length ? this.fieldsOrder : [...this.fieldList] this.fieldsOrderLoc =
this.fieldsOrder && this.fieldsOrder.length
? this.fieldsOrder
: [...this.fieldList];
}, },
methods: { methods: {
async loadFields() { async loadFields() {
let fields = [] let fields = [];
let order = 1 let order = 1;
if (this.viewId) { if (this.viewId) {
const data = await this.$api.dbViewColumn.list(this.viewId) const data = await this.$api.dbViewColumn.list(this.viewId);
const fieldById = data.reduce((o, f) => ({ const fieldById = data.reduce(
...o, (o, f) => ({
[f.fk_column_id]: f ...o,
}), {}) [f.fk_column_id]: f,
fields = this.meta.columns.map(c => ({ }),
title: c.title, {}
fk_column_id: c.id, );
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++
})
).sort((a, b) => a.order - b.order)
} else if (this.isPublic) {
fields = this.meta.columns fields = this.meta.columns
.map((c) => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
}))
.sort((a, b) => a.order - b.order);
} else if (this.isPublic) {
fields = this.meta.columns;
} }
this.fields = fields this.fields = fields;
this.$emit('input', this.fields.reduce((o, c) => ({ this.$emit(
...o, "input",
[c.title]: c.show this.fields.reduce(
}), {})) (o, c) => ({
this.$emit('update:fieldsOrder', this.fields.map(c => c.title)) ...o,
[c.title]: c.show,
}),
{}
)
);
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
}, },
async saveOrUpdate(field, i) { async saveOrUpdate(field, i) {
if (!this.isPublic && this._isUIAllowed('fieldsSync')) { if (!this.isPublic && this._isUIAllowed("fieldsSync")) {
if (field.id) { if (field.id) {
await this.$api.dbViewColumn.update(this.viewId, field.id, field) await this.$api.dbViewColumn.update(this.viewId, field.id, field);
} else { } else {
this.fields[i] = (await this.$api.dbViewColumn.create(this.viewId, field)) this.fields[i] = await this.$api.dbViewColumn.create(
this.viewId,
field
);
} }
} }
this.$emit('updated') this.$emit("updated");
this.$emit('input', this.fields.reduce((o, c) => ({ this.$emit(
...o, "input",
[c.title]: c.show this.fields.reduce(
}), {})) (o, c) => ({
this.$emit('update:fieldsOrder', this.fields.map(c => c.title)) ...o,
[c.title]: c.show,
}),
{}
)
);
this.$emit(
"update:fieldsOrder",
this.fields.map((c) => c.title)
);
this.$tele.emit('fields:show-hide-checkbox') this.$e("a:fields:show-hide");
}, },
async showAll() { async showAll() {
if (!this.isPublic) { if (!this.isPublic) {
await this.$api.dbView.showAllColumn(this.viewId) await this.$api.dbView.showAllColumn(this.viewId);
} }
for (const f of this.fields) { for (const f of this.fields) {
f.show = true f.show = true;
} }
this.$emit('updated') this.$emit("updated");
// eslint-disable-next-line no-return-assign,no-sequences // eslint-disable-next-line no-return-assign,no-sequences
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => (o[k] = true, o), {}) this.showFields = (
this.fieldsOrderLoc || Object.keys(this.showFields)
).reduce((o, k) => ((o[k] = true), o), {});
this.$tele.emit('fields:show-all') this.$e("a:fields:show-all");
}, },
async hideAll() { async hideAll() {
if (!this.isPublic) { if (!this.isPublic) {
await this.$api.dbView.hideAllColumn(this.viewId) await this.$api.dbView.hideAllColumn(this.viewId);
} }
for (const f of this.fields) { for (const f of this.fields) {
f.show = false f.show = false;
} }
this.$emit('updated') this.$emit("updated");
this.$nextTick(() => { this.$nextTick(() => {
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => (o[k] = false, o), {}) this.showFields = (
}) this.fieldsOrderLoc || Object.keys(this.showFields)
).reduce((o, k) => ((o[k] = false), o), {});
});
this.$tele.emit('fields:hide-all') this.$e("a:fields:hide-all");
}, },
onMove(event) { onMove(event) {
if (this.fields.length - 1 === event.moved.newIndex) { if (this.fields.length - 1 === event.moved.newIndex) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[event.moved.newIndex - 1].order + 1) this.$set(
this.fields[event.moved.newIndex],
"order",
this.fields[event.moved.newIndex - 1].order + 1
);
} else if (event.moved.newIndex === 0) { } else if (event.moved.newIndex === 0) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[1].order / 2) this.$set(
this.fields[event.moved.newIndex],
"order",
this.fields[1].order / 2
);
} else { } else {
this.$set(this.fields[event.moved.newIndex], 'order', ( this.$set(
this.fields[event.moved.newIndex - 1].order + this.fields[event.moved.newIndex + 1].order) / 2 this.fields[event.moved.newIndex],
) "order",
(this.fields[event.moved.newIndex - 1].order +
this.fields[event.moved.newIndex + 1].order) /
2
);
} }
this.saveOrUpdate(this.fields[event.moved.newIndex], event.moved.newIndex) this.saveOrUpdate(
this.$tele.emit('fields:drag') this.fields[event.moved.newIndex],
} event.moved.newIndex
} );
} this.$e("a:fields:reorder");
},
},
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -429,5 +488,4 @@ export default {
.drag-icon { .drag-icon {
cursor: all-scroll; /*cursor: grab;*/ cursor: all-scroll; /*cursor: grab;*/
} }
</style> </style>

5
packages/nc-gui/components/project/spreadsheet/components/headerCell.vue

@ -72,7 +72,7 @@
</span> </span>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-t="['column:set-as-primary']" v-t="['a:column:set-primary']"
dense dense
@click="setAsPrimaryValue" @click="setAsPrimaryValue"
> >
@ -143,7 +143,6 @@
<v-card-actions class="d-flex pa-4"> <v-card-actions class="d-flex pa-4">
<v-spacer /> <v-spacer />
<v-btn <v-btn
v-t="['column:delete:cancel']"
small small
@click="columnDeleteDialog = false" @click="columnDeleteDialog = false"
> >
@ -151,7 +150,7 @@
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
v-t="['column:delete']" v-t="['a:column:delete']"
small small
color="error" color="error"
@click="deleteColumn" @click="deleteColumn"

99
packages/nc-gui/components/project/spreadsheet/components/lockMenu.vue

@ -1,10 +1,20 @@
<template> <template>
<v-menu offset-y max-width="350"> <v-menu offset-y max-width="350">
<template #activator="{on}"> <template #activator="{ on }">
<v-icon v-if="value === 'locked'" small class="mx-1 nc-view-lock-menu" v-on="on"> <v-icon
v-if="value === 'locked'"
small
class="mx-1 nc-view-lock-menu"
v-on="on"
>
mdi-lock-outline mdi-lock-outline
</v-icon> </v-icon>
<v-icon v-else-if="value === 'personal'" small class="mx-1 nc-view-lock-menu" v-on="on"> <v-icon
v-else-if="value === 'personal'"
small
class="mx-1 nc-view-lock-menu"
v-on="on"
>
mdi-account mdi-account
</v-icon> </v-icon>
<v-icon v-else small class="mx-1 nc-view-lock-menu" v-on="on"> <v-icon v-else small class="mx-1 nc-view-lock-menu" v-on="on">
@ -12,7 +22,11 @@
</v-icon> </v-icon>
</template> </template>
<v-list maxc-width="350"> <v-list maxc-width="350">
<v-list-item two-line class="pb-4" @click="changeLockType('collaborative')"> <v-list-item
two-line
class="pb-4"
@click="changeLockType('collaborative')"
>
<v-list-item-icon class="mr-1 align-self-center"> <v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="!value || value === 'collaborative'" small> <v-icon v-if="!value || value === 'collaborative'" small>
mdi-check-bold mdi-check-bold
@ -26,51 +40,62 @@
Collaborative view Collaborative view
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal"> <v-list-item-subtitle
Collaborators with edit permissions or higher can change the view configuration. class="pt-2 pl- font-weight-light"
style="white-space: normal"
>
Collaborators with edit permissions or higher can change the view
configuration.
</v-list-item-subtitle> </v-list-item-subtitle>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item two-line class="pb-4" @click="changeLockType('locked')"> <v-list-item two-line class="pb-4" @click="changeLockType('locked')">
<v-list-item-icon class="mr-1 align-self-center"> <v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === 'locked'" small> <v-icon v-if="value === 'locked'" small> mdi-check-bold </v-icon>
mdi-check-bold
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content class="pb-1"> <v-list-item-content class="pb-1">
<v-list-item-title> <v-list-item-title>
<v-icon small class="mt-n1" color="primary"> <v-icon small class="mt-n1" color="primary"> mdi-lock </v-icon>
mdi-lock
</v-icon>
Locked View Locked View
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal"> <v-list-item-subtitle
class="pt-2 pl- font-weight-light"
style="white-space: normal"
>
No one can edit the view configuration until it is unlocked. No one can edit the view configuration until it is unlocked.
</v-list-item-subtitle> </v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Locked view.</span> <span class="caption mt-3"
><v-icon class="mr-1 mt-n1" x-small color="#fcb401">
mdi-star</v-icon
>Locked view.</span
>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
<v-list-item three-line @click="changeLockType('personal')"> <v-list-item three-line @click="changeLockType('personal')">
<v-list-item-icon class="mr-1 align-self-center"> <v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === 'personal'" small> <v-icon v-if="value === 'personal'" small> mdi-check-bold </v-icon>
mdi-check-bold
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-content> <v-list-item-content>
<v-list-item-title> <v-list-item-title>
<v-icon small class="mt-n1" color="primary"> <v-icon small class="mt-n1" color="primary"> mdi-account </v-icon>
mdi-account
</v-icon>
Personal view Personal view
</v-list-item-title> </v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal"> <v-list-item-subtitle
Only you can edit the view configuration. Other collaborators personal views are hidden by default. class="pt-2 pl- font-weight-light"
style="white-space: normal"
>
Only you can edit the view configuration. Other collaborators
personal views are hidden by default.
</v-list-item-subtitle> </v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Coming soon.</span> <span class="caption mt-3"
><v-icon class="mr-1 mt-n1" x-small color="#fcb401">
mdi-star</v-icon
>Coming soon.</span
>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -79,24 +104,20 @@
<script> <script>
export default { export default {
name: 'LockMenu', name: "LockMenu",
props: ['value'], props: ["value"],
data: () => ({ data: () => ({}),
}),
methods: { methods: {
changeLockType(type) { changeLockType(type) {
this.$tele.emit(`lockmenu:${type}`) this.$e("a:grid:lockmenu", { lockType: type });
if (type === 'personal') { if (type === "personal") {
return this.$toast.info('Coming soon').goAway(3000) return this.$toast.info("Coming soon").goAway(3000);
} }
this.$emit('input', type) this.$emit("input", type);
this.$toast.success(`Successfully Switched to ${type} view`).goAway(3000) this.$toast.success(`Successfully Switched to ${type} view`).goAway(3000);
} },
} },
} };
</script> </script>
<style scoped> <style scoped></style>
</style>

18
packages/nc-gui/components/project/spreadsheet/components/moreActions.vue

@ -7,7 +7,7 @@
> >
<template #activator="{on}"> <template #activator="{on}">
<v-btn <v-btn
v-t="['actions:trigger']" v-t="['c:actions']"
outlined outlined
class="nc-actions-menu-btn caption px-2 nc-remove-border font-weight-medium" class="nc-actions-menu-btn caption px-2 nc-remove-border font-weight-medium"
small small
@ -28,7 +28,7 @@
<v-list dense> <v-list dense>
<v-list-item <v-list-item
v-t="['actions:download-csv']" v-t="['a:actions:download-csv']"
dense dense
@click="exportCsv" @click="exportCsv"
> >
@ -44,7 +44,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="_isUIAllowed('csvImport') && !isView" v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:upload-csv']" v-t="['a:actions:upload-csv']"
dense dense
@click="importModal = true" @click="importModal = true"
> >
@ -64,7 +64,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="_isUIAllowed('csvImport') && !isView" v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:shared-view-list']" v-t="['a:actions:shared-view-list']"
dense dense
@click="$emit('showAdditionalFeatOverlay', 'shared-views')" @click="$emit('showAdditionalFeatOverlay', 'shared-views')"
> >
@ -80,7 +80,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="_isUIAllowed('csvImport') && !isView" v-if="_isUIAllowed('csvImport') && !isView"
v-t="['actions:webhook:trigger']" v-t="['c:actions:webhook']"
dense dense
@click="$emit('webhook')" @click="$emit('webhook')"
> >
@ -309,10 +309,10 @@ export default {
return res return res
}, {})) }, {}))
await this.$api.dbTableRow.bulkCreate( await this.$api.dbTableRow.bulkCreate(
'noco', 'noco',
this.projectName, this.projectName,
this.meta.title, this.meta.title,
batchData batchData
) )
progress += batchData.length progress += batchData.length
this.$store.commit('loader/MutMessage', `Importing data : ${progress}/${data.length}`) this.$store.commit('loader/MutMessage', `Importing data : ${progress}/${data.length}`)

2
packages/nc-gui/components/project/spreadsheet/components/shareViewMenu.vue

@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<v-btn <v-btn
v-t="['share-view:trigger']"
v-if="_isUIAllowed('add-user')" v-if="_isUIAllowed('add-user')"
v-t="['c:view:share']"
outlined outlined
class="nc-btn-share-view caption px-2 nc-remove-border font-weight-medium" class="nc-btn-share-view caption px-2 nc-remove-border font-weight-medium"
small small

139
packages/nc-gui/components/project/spreadsheet/components/sortListMenu.vue

@ -1,37 +1,36 @@
<template> <template>
<v-menu offset-y> <v-menu offset-y>
<template #activator="{ on }"> <template #activator="{ on }">
<v-badge <v-badge :value="sortList && sortList.length" color="primary" dot overlap>
:value="sortList && sortList.length"
color="primary"
dot
overlap
>
<v-btn <v-btn
v-t="['sort:trigger']" v-t="['c:sort']"
class="nc-sort-menu-btn px-2 nc-remove-border" class="nc-sort-menu-btn px-2 nc-remove-border"
:disabled="isLocked" :disabled="isLocked"
small small
text text
outlined outlined
:class=" { 'primary lighten-5 grey--text text--darken-3' : sortList && sortList.length}" :class="{
'primary lighten-5 grey--text text--darken-3':
sortList && sortList.length,
}"
v-on="on" v-on="on"
> >
<v-icon small class="mr-1" color="#777"> <v-icon small class="mr-1" color="#777"> mdi-sort </v-icon>
mdi-sort
</v-icon>
<!-- Sort --> <!-- Sort -->
{{ $t('activity.sort') }} {{ $t("activity.sort") }}
<v-icon small color="#777"> <v-icon small color="#777"> mdi-menu-down </v-icon>
mdi-menu-down
</v-icon>
</v-btn> </v-btn>
</v-badge> </v-badge>
</template> </template>
<div class="backgroundColor pa-2" style="min-width: 330px"> <div class="backgroundColor pa-2" style="min-width: 330px">
<div class="sort-grid" @click.stop> <div class="sort-grid" @click.stop>
<template v-for="(sort,i) in sortList||[]" dense> <template v-for="(sort, i) in sortList || []" dense>
<v-icon :key="i + 'icon'" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> <v-icon
:key="i + 'icon'"
class="nc-sort-item-remove-btn"
small
@click.stop="deleteSort(sort)"
>
mdi-close-box mdi-close-box
</v-icon> </v-icon>
@ -50,7 +49,7 @@
@click.stop @click.stop
@change="saveOrUpdate(sort, i)" @change="saveOrUpdate(sort, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span <span
:class="`caption font-weight-regular nc-sort-fld-${item.title}`" :class="`caption font-weight-regular nc-sort-fld-${item.title}`"
> >
@ -62,7 +61,10 @@
:key="i + 'sel2'" :key="i + 'sel2'"
v-model="sort.direction" v-model="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select" class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:items="[{text : 'A -> Z', value: 'asc'},{text : 'Z -> A', value: 'desc'}]" :items="[
{ text: 'A -> Z', value: 'asc' },
{ text: 'Z -> A', value: 'desc' },
]"
:label="$t('labels.operation')" :label="$t('labels.operation')"
solo solo
flat flat
@ -71,116 +73,125 @@
@click.stop @click.stop
@change="saveOrUpdate(sort, i)" @change="saveOrUpdate(sort, i)"
> >
<template #item="{item}"> <template #item="{ item }">
<span class="caption font-weight-regular">{{ item.text }}</span> <span class="caption font-weight-regular">{{ item.text }}</span>
</template> </template>
</v-select> </v-select>
</template> </template>
</div> </div>
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addSort"> <v-btn small class="elevation-0 grey--text my-3" @click.stop="addSort">
<v-icon small color="grey"> <v-icon small color="grey"> mdi-plus </v-icon>
mdi-plus
</v-icon>
<!-- Add Sort Option --> <!-- Add Sort Option -->
{{ $t('activity.addSort') }} {{ $t("activity.addSort") }}
</v-btn> </v-btn>
</div> </div>
</v-menu> </v-menu>
</template> </template>
<script> <script>
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from "nocodb-sdk";
export default { export default {
name: 'SortListMenu', name: "SortListMenu",
props: { props: {
fieldList: Array, fieldList: Array,
value: [Array, Object], value: [Array, Object],
isLocked: Boolean, isLocked: Boolean,
meta: [Object], meta: [Object],
viewId: String, viewId: String,
shared: Boolean shared: Boolean,
}, },
data: () => ({ data: () => ({
sortList: [] sortList: [],
}), }),
computed: { computed: {
columns() { columns() {
if (!this.meta || !this.meta.columns) { return [] } if (!this.meta || !this.meta.columns) {
return this.meta.columns.filter(c => !(c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== RelationTypes.BELONGS_TO)) return [];
} }
return this.meta.columns.filter(
(c) =>
!(
c.uidt === UITypes.LinkToAnotherRecord &&
c.colOptions.type !== RelationTypes.BELONGS_TO
)
);
},
}, },
watch: { watch: {
value(v) { value(v) {
this.sortList = v || [] this.sortList = v || [];
}, },
async viewId(v) { async viewId(v) {
if (v) { if (v) {
await this.loadSortList() await this.loadSortList();
} }
} },
}, },
async created() { async created() {
this.sortList = this.value || [] this.sortList = this.value || [];
this.loadSortList() this.loadSortList();
}, },
methods: { methods: {
addSort() { addSort() {
this.sortList.push({ this.sortList.push({
fk_column_id: null, fk_column_id: null,
direction: 'asc' direction: "asc",
}) });
this.sortList = this.sortList.slice() this.sortList = this.sortList.slice();
this.$tele.emit(`sort:add:${this.sortList.length}`) this.$e("a:sort:add", { length: this.sortList.length });
}, },
async loadSortList() { async loadSortList() {
if (!this.shared) { // && !this._isUIAllowed('sortSync')) { if (!this.shared) {
let sortList = [] // && !this._isUIAllowed('sortSync')) {
let sortList = [];
if (this.viewId) { if (this.viewId) {
const data = await this.$api.dbTableSort.list(this.viewId) const data = await this.$api.dbTableSort.list(this.viewId);
sortList = data.sorts.list sortList = data.sorts.list;
} }
this.sortList = sortList this.sortList = sortList;
} }
}, },
async saveOrUpdate(sort, i) { async saveOrUpdate(sort, i) {
if (!this.shared && this._isUIAllowed('sortSync')) { if (!this.shared && this._isUIAllowed("sortSync")) {
if (sort.id) { if (sort.id) {
await this.$api.dbTableSort.update(sort.id, sort) await this.$api.dbTableSort.update(sort.id, sort);
} else { } else {
this.$set(this.sortList, i, (await this.$api.dbTableSort.create(this.viewId, sort))) this.$set(
this.sortList,
i,
await this.$api.dbTableSort.create(this.viewId, sort)
);
} }
} else { } else {
this.$emit('input', this.sortList) this.$emit("input", this.sortList);
} }
this.$emit('updated') this.$emit("updated");
this.$tele.emit(`sort:dir:${sort.direction}`) this.$e("a:sort:dir", { direction: sort.direction });
}, },
async deleteSort(sort, i) { async deleteSort(sort, i) {
if (!this.shared && sort.id && this._isUIAllowed('sortSync')) { if (!this.shared && sort.id && this._isUIAllowed("sortSync")) {
await this.$api.dbTableSort.delete(sort.id) await this.$api.dbTableSort.delete(sort.id);
await this.loadSortList() await this.loadSortList();
} else { } else {
this.sortList.splice(i, 1) this.sortList.splice(i, 1);
this.$emit('input', this.sortList) this.$emit("input", this.sortList);
} }
this.$emit('updated') this.$emit("updated");
this.$tele.emit('sort:delete') this.$e("a:sort:delete");
} },
} },
} };
</script> </script>
<style scoped> <style scoped>
.sort-grid { .sort-grid {
display: grid; display: grid;
grid-template-columns:22px auto 100px; grid-template-columns: 22px auto 100px;
column-gap: 6px; column-gap: 6px;
row-gap: 6px; row-gap: 6px;
} }
</style> </style>

459
packages/nc-gui/components/project/spreadsheet/components/spreadsheetNavDrawer.vue

@ -13,9 +13,15 @@
<v-list v-if="views && views.length" dense> <v-list v-if="views && views.length" dense>
<v-list-item dense> <v-list-item dense>
<!-- Views --> <!-- Views -->
<span class="body-2 font-weight-medium">{{ $t('objects.views') }}</span> <span class="body-2 font-weight-medium">{{
$t("objects.views")
}}</span>
</v-list-item> </v-list-item>
<v-list-item-group v-model="selectedViewIdLocal" mandatory color="primary"> <v-list-item-group
v-model="selectedViewIdLocal"
mandatory
color="primary"
>
<draggable <draggable
:is="_isUIAllowed('viewlist-drag-n-drop') ? 'draggable' : 'div'" :is="_isUIAllowed('viewlist-drag-n-drop') ? 'draggable' : 'div'"
v-model="viewsList" v-model="viewsList"
@ -23,15 +29,20 @@
v-bind="dragOptions" v-bind="dragOptions"
@change="onMove($event)" @change="onMove($event)"
> >
<transition-group type="transition" :name="!drag ? 'flip-list' : null"> <transition-group
type="transition"
:name="!drag ? 'flip-list' : null"
>
<v-list-item <v-list-item
v-for="(view, i) in viewsList" v-for="(view, i) in viewsList"
:key="view.id" :key="view.id"
v-t="['view:open']" v-t="['a:view:open', { view: view.type }]"
dense dense
:value="view.id" :value="view.id"
active-class="x-active--text" active-class="x-active--text"
:class="`body-2 view nc-view-item nc-draggable-child nc-${viewTypeAlias[view.type]}-view-item`" :class="`body-2 view nc-view-item nc-draggable-child nc-${
viewTypeAlias[view.type]
}-view-item`"
@click="$emit('rerender')" @click="$emit('rerender')"
> >
<v-icon <v-icon
@ -50,9 +61,7 @@
> >
{{ viewIcons[view.type].icon }} {{ viewIcons[view.type].icon }}
</v-icon> </v-icon>
<v-icon v-else color="primary" small> <v-icon v-else color="primary" small> mdi-table </v-icon>
mdi-table
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title> <v-list-item-title>
<v-tooltip bottom> <v-tooltip bottom>
@ -68,11 +77,11 @@
@click.stop @click.stop
@keydown.enter.stop="updateViewName(view, i)" @keydown.enter.stop="updateViewName(view, i)"
@blur="updateViewName(view, i)" @blur="updateViewName(view, i)"
> />
<template <template v-else>
v-else <span v-on="on">{{
> view.alias || view.title
<span v-on="on">{{ view.alias || view.title }}</span> }}</span>
</template> </template>
</div> </div>
</template> </template>
@ -127,7 +136,9 @@
</draggable> </draggable>
</v-list-item-group> </v-list-item-group>
</v-list> </v-list>
<template v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')"> <template
v-if="hideViews && _isUIAllowed('virtualViewsCreateOrEdit')"
>
<v-divider class="advance-menu-divider" /> <v-divider class="advance-menu-divider" />
<v-list <v-list
@ -138,8 +149,11 @@
> >
<v-list-item dense> <v-list-item dense>
<!-- Create a View --> <!-- Create a View -->
<span class="body-2 font-weight-medium" @dblclick="enableDummyFeat = true"> <span
{{ $t('activity.createView') }} class="body-2 font-weight-medium"
@dblclick="enableDummyFeat = true"
>
{{ $t("activity.createView") }}
</span> </span>
<v-tooltip top> <v-tooltip top>
<template #activator="{ on }"> <template #activator="{ on }">
@ -156,32 +170,33 @@
</template> </template>
<!-- Only visible to Creator --> <!-- Only visible to Creator -->
<span class="caption"> <span class="caption">
{{ $t('msg.info.onlyCreator') }} {{ $t("msg.info.onlyCreator") }}
</span> </span>
</v-tooltip> </v-tooltip>
</v-list-item> </v-list-item>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item dense class="body-2 nc-create-grid-view" v-on="on" @click="openCreateViewDlg(viewTypes.GRID)"> <v-list-item
dense
class="body-2 nc-create-grid-view"
v-on="on"
@click="openCreateViewDlg(viewTypes.GRID)"
>
<v-list-item-icon class="mr-n1"> <v-list-item-icon class="mr-n1">
<v-icon color="blue" x-small> <v-icon color="blue" x-small> mdi-grid-large </v-icon>
mdi-grid-large
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title> <v-list-item-title>
<span class="font-weight-regular"> <span class="font-weight-regular">
<!-- Grid --> <!-- Grid -->
{{ $t('objects.viewType.grid') }} {{ $t("objects.viewType.grid") }}
</span> </span>
</v-list-item-title> </v-list-item-title>
<v-spacer /> <v-spacer />
<v-icon class="mr-1" small> <v-icon class="mr-1" small> mdi-plus </v-icon>
mdi-plus
</v-icon>
</v-list-item> </v-list-item>
</template> </template>
<!-- Add Grid View --> <!-- Add Grid View -->
{{ $t('msg.info.addView.grid') }} {{ $t("msg.info.addView.grid") }}
</v-tooltip> </v-tooltip>
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
@ -192,30 +207,24 @@
@click="openCreateViewDlg(viewTypes.GALLERY)" @click="openCreateViewDlg(viewTypes.GALLERY)"
> >
<v-list-item-icon class="mr-n1"> <v-list-item-icon class="mr-n1">
<v-icon color="orange" x-small> <v-icon color="orange" x-small> mdi-camera-image </v-icon>
mdi-camera-image
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title> <v-list-item-title>
<span class="font-weight-regular"> <span class="font-weight-regular">
<!-- Gallery --> <!-- Gallery -->
{{ $t('objects.viewType.gallery') }} {{ $t("objects.viewType.gallery") }}
</span> </span>
</v-list-item-title> </v-list-item-title>
<v-spacer /> <v-spacer />
<v-icon class="mr-1" small> <v-icon class="mr-1" small> mdi-plus </v-icon>
mdi-plus
</v-icon>
</v-list-item> </v-list-item>
</template> </template>
<!-- Add Gallery View --> <!-- Add Gallery View -->
{{ $t('msg.info.addView.gallery') }} {{ $t("msg.info.addView.gallery") }}
</v-tooltip> </v-tooltip>
<v-tooltip <v-tooltip bottom>
bottom
>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
v-if="!isView" v-if="!isView"
@ -225,7 +234,11 @@
@click="openCreateViewDlg(viewTypes.FORM)" @click="openCreateViewDlg(viewTypes.FORM)"
> >
<v-list-item-icon class="mr-n1"> <v-list-item-icon class="mr-n1">
<v-icon x-small :color="viewIcons[viewTypes.FORM].color" class="mt-n1"> <v-icon
x-small
:color="viewIcons[viewTypes.FORM].color"
class="mt-n1"
>
mdi-form-select mdi-form-select
</v-icon> </v-icon>
</v-list-item-icon> </v-list-item-icon>
@ -233,18 +246,16 @@
<span class="font-weight-regular"> <span class="font-weight-regular">
<!-- Form --> <!-- Form -->
{{ $t('objects.viewType.form') }} {{ $t("objects.viewType.form") }}
</span> </span>
</v-list-item-title> </v-list-item-title>
<v-spacer /> <v-spacer />
<v-icon class="mr-1" small> <v-icon class="mr-1" small> mdi-plus </v-icon>
mdi-plus
</v-icon>
</v-list-item> </v-list-item>
</template> </template>
<!-- Add Form View --> <!-- Add Form View -->
{{ $t('msg.info.addView.form') }} {{ $t("msg.info.addView.form") }}
</v-tooltip> </v-tooltip>
</v-list> </v-list>
</template> </template>
@ -254,11 +265,7 @@
v-if="time - $store.state.windows.miniSponsorCard > 15 * 60 * 1000" v-if="time - $store.state.windows.miniSponsorCard > 15 * 60 * 1000"
class="pa-2 sponsor-wrapper" class="pa-2 sponsor-wrapper"
> >
<v-icon <v-icon small class="close-icon" @click="hideMiniSponsorCard">
small
class="close-icon"
@click="hideMiniSponsorCard"
>
mdi-close-circle-outline mdi-close-circle-outline
</v-icon> </v-icon>
@ -396,45 +403,51 @@
<v-container @click.stop> <v-container @click.stop>
<h3 class="title mb-3"> <h3 class="title mb-3">
<!-- This view is shared via a private link --> <!-- This view is shared via a private link -->
{{ $t('msg.info.privateLink') }} {{ $t("msg.info.privateLink") }}
</h3> </h3>
<p class="grey&#45;&#45;text body-2"> <p class="grey&#45;&#45;text body-2">
<!-- People with private link can only see cells visible in this view --> <!-- People with private link can only see cells visible in this view -->
</p> </p>
<div style="border-radius: 4px" class="share-link-box body-2 pa-2 d-flex align-center"> <div
style="border-radius: 4px"
class="share-link-box body-2 pa-2 d-flex align-center"
>
{{ sharedViewUrl }} {{ sharedViewUrl }}
<v-spacer /> <v-spacer />
<a <a
v-t="['share-view:open-url']" v-t="['c:view:share:open-url']"
:href="`${sharedViewUrl}`" :href="`${sharedViewUrl}`"
style="text-decoration: none" style="text-decoration: none"
target="_blank" target="_blank"
> >
<v-icon small class="mx-2">mdi-open-in-new</v-icon> <v-icon small class="mx-2">mdi-open-in-new</v-icon>
</a> </a>
<v-icon <v-icon small class="pointer" @click="copyShareUrlToClipboard">
small
class="pointer"
@click="copyShareUrlToClipboard"
>
mdi-content-copy mdi-content-copy
</v-icon> </v-icon>
</div> </div>
<v-switch v-model="passwordProtect" dense @change="onPasswordProtectChange"> <v-switch
v-model="passwordProtect"
dense
@change="onPasswordProtectChange"
>
<template #label> <template #label>
<!-- Restrict access with a password --> <!-- Restrict access with a password -->
<span v-show="!passwordProtect" class="caption"> <span v-show="!passwordProtect" class="caption">
{{ $t('msg.info.beforeEnablePwd') }} {{ $t("msg.info.beforeEnablePwd") }}
</span> </span>
<!-- Access is password restricted --> <!-- Access is password restricted -->
<span v-show="passwordProtect" class="caption"> <span v-show="passwordProtect" class="caption">
{{ $t('msg.info.afterEnablePwd') }} {{ $t("msg.info.afterEnablePwd") }}
</span> </span>
</template> </template>
</v-switch> </v-switch>
<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"
autocomplete="new-password" autocomplete="new-password"
@ -449,14 +462,22 @@
flat flat
> >
<template #append> <template #append>
<v-icon small @click="showShareLinkPassword = !showShareLinkPassword"> <v-icon
{{ showShareLinkPassword ? 'visibility_off' : 'visibility' }} small
@click="showShareLinkPassword = !showShareLinkPassword"
>
{{ showShareLinkPassword ? "visibility_off" : "visibility" }}
</v-icon> </v-icon>
</template> </template>
</v-text-field> </v-text-field>
<v-btn color="primary" class="caption" small @click="saveShareLinkPassword"> <v-btn
color="primary"
class="caption"
small
@click="saveShareLinkPassword"
>
<!-- Save password --> <!-- Save password -->
{{ $t('placeholder.password.save') }} {{ $t("placeholder.password.save") }}
</v-btn> </v-btn>
</div> </div>
</v-container> </v-container>
@ -466,16 +487,16 @@
</template> </template>
<script> <script>
import draggable from 'vuedraggable' import draggable from "vuedraggable";
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from "nocodb-sdk";
import CreateViewDialog from '@/components/project/spreadsheet/dialog/createViewDialog' import CreateViewDialog from "@/components/project/spreadsheet/dialog/createViewDialog";
import Extras from '~/components/project/spreadsheet/components/extras' import Extras from "~/components/project/spreadsheet/components/extras";
import viewIcons from '~/helpers/viewIcons' import viewIcons from "~/helpers/viewIcons";
import { copyTextToClipboard } from '~/helpers/xutils' import { copyTextToClipboard } from "~/helpers/xutils";
import SponsorMini from '~/components/sponsorMini' import SponsorMini from "~/components/sponsorMini";
export default { export default {
name: 'SpreadsheetNavDrawer', name: "SpreadsheetNavDrawer",
components: { SponsorMini, Extras, CreateViewDialog, draggable }, components: { SponsorMini, Extras, CreateViewDialog, draggable },
props: { props: {
extraViewParams: Object, extraViewParams: Object,
@ -485,7 +506,7 @@ export default {
primaryValueColumn: [Number, String], primaryValueColumn: [Number, String],
toggleDrawer: { toggleDrawer: {
type: Boolean, type: Boolean,
default: false default: false,
}, },
nodes: Object, nodes: Object,
table: String, table: String,
@ -499,7 +520,7 @@ export default {
sortList: [Object, Array], sortList: [Object, Array],
load: { load: {
default: true, default: true,
type: Boolean type: Boolean,
}, },
currentApiUrl: String, currentApiUrl: String,
fieldsOrder: Array, fieldsOrder: Array,
@ -508,23 +529,23 @@ export default {
// coverImageField: String, // coverImageField: String,
groupingField: String, groupingField: String,
// showSystemFields: Boolean, // showSystemFields: Boolean,
views: Array views: Array,
}, },
data: () => ({ data: () => ({
drag: false, drag: false,
dragOptions: { dragOptions: {
animation: 200, animation: 200,
group: 'description', group: "description",
disabled: false, disabled: false,
ghostClass: 'ghost' ghostClass: "ghost",
}, },
time: Date.now(), time: Date.now(),
sponsorMiniVisible: true, sponsorMiniVisible: true,
enableDummyFeat: false, enableDummyFeat: false,
searchQueryVal: '', searchQueryVal: "",
showShareLinkPassword: false, showShareLinkPassword: false,
passwordProtect: false, passwordProtect: false,
sharedViewPassword: '', sharedViewPassword: "",
overAdvShieldIcon: false, overAdvShieldIcon: false,
overShieldIcon: false, overShieldIcon: false,
viewIcons, viewIcons,
@ -533,103 +554,121 @@ export default {
showShareModel: false, showShareModel: false,
showCreateView: false, showCreateView: false,
loading: false, loading: false,
viewTypeAlias: { [ViewTypes.GRID]: 'grid', [ViewTypes.FORM]: 'form', [ViewTypes.GALLERY]: 'gallery' } viewTypeAlias: {
[ViewTypes.GRID]: "grid",
[ViewTypes.FORM]: "form",
[ViewTypes.GALLERY]: "gallery",
},
}), }),
computed: { computed: {
viewsList: { viewsList: {
set(v) { set(v) {
this.$emit('update:views', v) this.$emit("update:views", v);
}, },
get() { get() {
return this.views return this.views;
} },
}, },
viewTypes() { viewTypes() {
return ViewTypes return ViewTypes;
}, },
newViewParams() { newViewParams() {
if (!this.showFields) { if (!this.showFields) {
return {} return {};
} }
const showFields = { ...this.showFields } const showFields = { ...this.showFields };
Object.keys(showFields).forEach((k) => { Object.keys(showFields).forEach((k) => {
showFields[k] = true showFields[k] = true;
}) });
return { showFields } return { showFields };
}, },
selectedViewIdLocal: { selectedViewIdLocal: {
set(val) { set(val) {
const view = (this.views || []).find(v => v.id === val) const view = (this.views || []).find((v) => v.id === val);
this.$router.push({ this.$router.push({
query: { query: {
...this.$route.query, ...this.$route.query,
view: view && (view.id) view: view && view.id,
} },
}) });
}, },
get() { get() {
let id let id;
if (this.views) { if (this.views) {
const view = this.views.find(v => v.id === this.$route.query.view) const view = this.views.find((v) => v.id === this.$route.query.view);
id = (view && view.id) || ((this.views && this.views[0]) || {}).id id = (view && view.id) || ((this.views && this.views[0]) || {}).id;
} }
return id return id;
} },
}, },
sharedViewUrl() { sharedViewUrl() {
let viewType let viewType;
switch (this.shareLink.type) { switch (this.shareLink.type) {
case this.viewTypes.FORM: case this.viewTypes.FORM:
viewType = 'form' viewType = "form";
break break;
case this.viewTypes.KANBAN: case this.viewTypes.KANBAN:
viewType = 'kanban' viewType = "kanban";
break break;
default: default:
viewType = 'view' viewType = "view";
} }
return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}` return `${this.dashboardUrl}#/nc/${viewType}/${this.shareLink.uuid}`;
} },
}, },
watch: { watch: {
async load(v) { async load(v) {
if (v) { if (v) {
await this.loadViews() await this.loadViews();
this.onViewIdChange(this.selectedViewIdLocal) this.onViewIdChange(this.selectedViewIdLocal);
} }
}, },
selectedViewIdLocal(id) { selectedViewIdLocal(id) {
this.onViewIdChange(id) this.onViewIdChange(id);
} },
}, },
async created() { async created() {
if (this.load) { if (this.load) {
await this.loadViews() await this.loadViews();
} }
this.onViewIdChange(this.selectedViewIdLocal) this.onViewIdChange(this.selectedViewIdLocal);
}, },
methods: { methods: {
async onMove(event) { async onMove(event) {
if (this.viewsList.length - 1 === event.moved.newIndex) { if (this.viewsList.length - 1 === event.moved.newIndex) {
this.$set(this.viewsList[event.moved.newIndex], 'order', this.viewsList[event.moved.newIndex - 1].order + 1) this.$set(
this.viewsList[event.moved.newIndex],
"order",
this.viewsList[event.moved.newIndex - 1].order + 1
);
} else if (event.moved.newIndex === 0) { } else if (event.moved.newIndex === 0) {
this.$set(this.viewsList[event.moved.newIndex], 'order', this.viewsList[1].order / 2) this.$set(
this.viewsList[event.moved.newIndex],
"order",
this.viewsList[1].order / 2
);
} else { } else {
this.$set(this.viewsList[event.moved.newIndex], 'order', (this.viewsList[event.moved.newIndex - 1].order + this.viewsList[event.moved.newIndex + 1].order) / 2) this.$set(
this.viewsList[event.moved.newIndex],
"order",
(this.viewsList[event.moved.newIndex - 1].order +
this.viewsList[event.moved.newIndex + 1].order) /
2
);
} }
await this.$api.dbView.update(this.viewsList[event.moved.newIndex].id, { await this.$api.dbView.update(this.viewsList[event.moved.newIndex].id, {
title: this.viewsList[event.moved.newIndex].title, title: this.viewsList[event.moved.newIndex].title,
order: this.viewsList[event.moved.newIndex].order order: this.viewsList[event.moved.newIndex].order,
}) });
this.$tele.emit('view:drag') this.$e("a:view:reorder");
}, },
onViewIdChange(id) { onViewIdChange(id) {
const selectedView = this.views && this.views.find(v => v.id === id) const selectedView = this.views && this.views.find((v) => v.id === id);
// const queryParams = {} // const queryParams = {}
this.$emit('update:selectedViewId', id) this.$emit("update:selectedViewId", id);
this.$emit('update:selectedView', selectedView) this.$emit("update:selectedView", selectedView);
// if (selectedView.type === 'table') { // if (selectedView.type === 'table') {
// return; // return;
// } // }
@ -652,51 +691,52 @@ export default {
// } else { // } else {
// this.$emit('mapFieldsAndShowFields') // this.$emit('mapFieldsAndShowFields')
// } // }
this.$emit('loadTableData') this.$emit("loadTableData");
}, },
hideMiniSponsorCard() { hideMiniSponsorCard() {
this.$store.commit('windows/MutMiniSponsorCard', Date.now()) this.$store.commit("windows/MutMiniSponsorCard", Date.now());
}, },
openCreateViewDlg(type) { openCreateViewDlg(type) {
const mainView = this.viewsList.find(v => v.type === 'table' || v.type === 'view') const mainView = this.viewsList.find(
(v) => v.type === "table" || v.type === "view"
);
try { try {
this.copyViewRef = this.copyViewRef || { this.copyViewRef = this.copyViewRef || {
query_params: JSON.stringify({ query_params: JSON.stringify({
...this.newViewParams, ...this.newViewParams,
fieldsOrder: JSON.parse(mainView.query_params).fieldsOrder fieldsOrder: JSON.parse(mainView.query_params).fieldsOrder,
}) }),
} };
} catch { } catch {}
} this.createViewType = type;
this.createViewType = type this.showCreateView = true;
this.showCreateView = true this.$e("c:view:create", { view: type });
this.$tele.emit(`view:create:trigger:${type}`)
}, },
isCentrallyAligned(col) { isCentrallyAligned(col) {
return ![ return ![
'SingleLineText', "SingleLineText",
'LongText', "LongText",
'Attachment', "Attachment",
'Date', "Date",
'Time', "Time",
'Email', "Email",
'URL', "URL",
'DateTime', "DateTime",
'CreateTime', "CreateTime",
'LastModifiedTime' "LastModifiedTime",
].includes(col.uidt) ].includes(col.uidt);
}, },
onPasswordProtectChange() { onPasswordProtectChange() {
if (!this.passwordProtect) { if (!this.passwordProtect) {
this.shareLink.password = null this.shareLink.password = null;
this.saveShareLinkPassword() this.saveShareLinkPassword();
} }
}, },
async saveShareLinkPassword() { async saveShareLinkPassword() {
try { try {
await this.$api.dbViewShare.update(this.shareLink.id, { await this.$api.dbViewShare.update(this.shareLink.id, {
password: this.shareLink.password password: this.shareLink.password,
}) });
// await this.$store.dispatch('sqlMgr/ActSqlOp', [ // await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { dbAlias: this.nodes.dbAlias }, // { dbAlias: this.nodes.dbAlias },
@ -706,12 +746,14 @@ export default {
// password: this.shareLink.password // password: this.shareLink.password
// } // }
// ]) // ])
this.$toast.success('Successfully updated').goAway(3000) this.$toast.success("Successfully updated").goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
} }
this.$tele.emit('share-view:enable-pwd') this.$e("a:view:share:enable-pwd");
}, },
async loadViews() { async loadViews() {
// this.viewsList = await this.sqlOp( // this.viewsList = await this.sqlOp(
@ -727,8 +769,8 @@ export default {
// this.viewsList = [] // this.viewsList = []
const views = (await this.$api.dbView.list(this.meta.id)).list const views = (await this.$api.dbView.list(this.meta.id)).list;
this.$emit('update:views', views) this.$emit("update:views", views);
}, },
// async onViewChange() { // async onViewChange() {
// let query_params = {} // let query_params = {}
@ -748,23 +790,27 @@ export default {
// this.$emit('loadTableData'); // this.$emit('loadTableData');
// }, // },
copyapiUrlToClipboard() { copyapiUrlToClipboard() {
copyTextToClipboard(this.currentApiUrl) copyTextToClipboard(this.currentApiUrl);
this.clipboardSuccessHandler() this.clipboardSuccessHandler();
}, },
async updateViewName(view, index) { async updateViewName(view, index) {
if (!view.edit) { if (!view.edit) {
return return;
} }
// const oldTitle = view.title // const oldTitle = view.title
this.$set(view, 'edit', false) this.$set(view, "edit", false);
if (view.title_temp === view.title) { if (view.title_temp === view.title) {
return return;
} }
if (this.viewsList.some((v, i) => i !== index && (v.alias || v.title) === view.title_temp)) { if (
this.$toast.info('View name should be unique').goAway(3000) this.viewsList.some(
return (v, i) => i !== index && (v.alias || v.title) === view.title_temp
)
) {
this.$toast.info("View name should be unique").goAway(3000);
return;
} }
try { try {
// if (this.selectedViewIdLocal === view.id) { // if (this.selectedViewIdLocal === view.id) {
@ -775,35 +821,39 @@ export default {
// } // }
// }) // })
// } // }
this.$set(view, 'title', view.title_temp) this.$set(view, "title", view.title_temp);
await this.$api.dbView.update(view.id, { await this.$api.dbView.update(view.id, {
title: view.title, title: view.title,
order: view.order order: view.order,
}) });
this.$toast.success('View renamed successfully').goAway(3000) this.$toast.success("View renamed successfully").goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
} }
}, },
showRenameTextBox(view, i) { showRenameTextBox(view, i) {
this.$set(view, 'edit', true) this.$set(view, "edit", true);
this.$set(view, 'title_temp', view.title) this.$set(view, "title_temp", view.title);
this.$nextTick(() => { this.$nextTick(() => {
const input = this.$refs[`input${i}`][0] const input = this.$refs[`input${i}`][0];
input.focus() input.focus();
input.setSelectionRange(0, input.value.length) input.setSelectionRange(0, input.value.length);
}) });
this.$tele.emit(`view:rename:trigger:${view.type}`) this.$e("c:view:rename", { view: view.type });
}, },
async deleteView(view) { async deleteView(view) {
try { try {
await this.$api.dbView.delete(view.id) await this.$api.dbView.delete(view.id);
this.$toast.success('View deleted successfully').goAway(3000) this.$toast.success("View deleted successfully").goAway(3000);
await this.loadViews() await this.loadViews();
} catch (e) { } catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) this.$toast
.error(await this._extractSdkResponseErrorMsg(e))
.goAway(3000);
} }
this.$tele.emit(`view:delete:submit:${view.type}`) this.$e("a:view:delete", { view: view.type });
}, },
async genShareLink() { async genShareLink() {
// const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [ // const sharedViewUrl = await this.$store.dispatch('sqlMgr/ActSqlOp', [
@ -830,44 +880,44 @@ export default {
// password: this.sharedViewPassword // password: this.sharedViewPassword
// } // }
// ]) // ])
const shared = (await this.$api.dbViewShare.create(this.selectedViewId)) const shared = await this.$api.dbViewShare.create(this.selectedViewId);
// todo: url // todo: url
this.shareLink = shared this.shareLink = shared;
this.showShareModel = true this.showShareModel = true;
}, },
copyView(view, i) { copyView(view, i) {
this.createViewType = view.type this.createViewType = view.type;
this.showCreateView = true this.showCreateView = true;
this.copyViewRef = view this.copyViewRef = view;
this.$tele.emit(`view:copy:trigger${view.type}`) this.$e("c:view:copy", { view: view.type });
}, },
async onViewCreate(viewMeta) { async onViewCreate(viewMeta) {
this.copyViewRef = null this.copyViewRef = null;
await this.loadViews() await this.loadViews();
this.selectedViewIdLocal = viewMeta.id this.selectedViewIdLocal = viewMeta.id;
// await this.onViewChange(); // await this.onViewChange();
this.$tele.emit(`view:create:submit:${viewMeta.type}`) this.$e("a:view:create", { view: viewMeta.type });
}, },
clipboard(str) { clipboard(str) {
const el = document.createElement('textarea') const el = document.createElement("textarea");
el.addEventListener('focusin', e => e.stopPropagation()) el.addEventListener("focusin", (e) => e.stopPropagation());
el.value = str el.value = str;
document.body.appendChild(el) document.body.appendChild(el);
el.select() el.select();
document.execCommand('copy') document.execCommand("copy");
document.body.removeChild(el) document.body.removeChild(el);
}, },
clipboardSuccessHandler() { clipboardSuccessHandler() {
this.$toast.info('Copied to clipboard').goAway(1000) this.$toast.info("Copied to clipboard").goAway(1000);
}, },
copyShareUrlToClipboard() { copyShareUrlToClipboard() {
this.clipboard(this.sharedViewUrl) this.clipboard(this.sharedViewUrl);
this.clipboardSuccessHandler() this.clipboardSuccessHandler();
this.$tele.emit('share-view:copy-url') this.$e("c:view:share:copy-url");
} },
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -933,7 +983,7 @@ export default {
right: 0; right: 0;
background: var(--v-primary-base); background: var(--v-primary-base);
opacity: 0.2; opacity: 0.2;
content: ''; content: "";
z-index: 1; z-index: 1;
pointer-events: none; pointer-events: none;
} }
@ -961,7 +1011,7 @@ export default {
.nc-draggable-child .nc-child-draggable-icon { .nc-draggable-child .nc-child-draggable-icon {
opacity: 0; opacity: 0;
transition: .3s opacity; transition: 0.3s opacity;
position: absolute; position: absolute;
left: 0; left: 0;
} }
@ -984,5 +1034,4 @@ export default {
opacity: 0.5; opacity: 0.5;
background: grey; background: grey;
} }
</style> </style>

2
packages/nc-gui/components/project/spreadsheet/components/virtualHeaderCell.vue

@ -106,7 +106,7 @@
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</v-btn> </v-btn>
<v-btn <v-btn
v-t="['vitual:column:delete']" v-t="['a:column:delete']"
small small
color="error" color="error"
@click="deleteColumn" @click="deleteColumn"

1203
packages/nc-gui/components/project/spreadsheet/rowsXcDataTable.vue

File diff suppressed because it is too large Load Diff

776
packages/nc-gui/components/project/spreadsheet/views/formView.vue

File diff suppressed because it is too large Load Diff

668
packages/nc-gui/components/project/spreadsheet/views/xcGridView.vue

File diff suppressed because it is too large Load Diff

112
packages/nc-gui/components/project/table.vue

@ -6,23 +6,21 @@
<template v-else> <template v-else>
<v-tabs <v-tabs
v-model="active" v-model="active"
:height="relationTabs && relationTabs.length ?38:0" :height="relationTabs && relationTabs.length ? 38 : 0"
class="table-tabs" class="table-tabs"
:class="{'hidden-tab':!relationTabs || !relationTabs.length}" :class="{ 'hidden-tab': !relationTabs || !relationTabs.length }"
color="pink" color="pink"
@change="onTabChange" @change="onTabChange"
> >
<template v-if="_isUIAllowed('smartSheet')"> <template v-if="_isUIAllowed('smartSheet')">
<v-tab v-show="relationTabs && relationTabs.length" class=""> <v-tab v-show="relationTabs && relationTabs.length" class="">
<v-icon small> <v-icon small> mdi-table-edit </v-icon>&nbsp;<span
mdi-table-edit class="caption text-capitalize font-weight-bold"
</v-icon>&nbsp;<span >
class="caption text-capitalize font-weight-bold" {{ nodes.title }}</span
> {{ nodes.title }}</span> >
</v-tab> </v-tab>
<v-tab-item <v-tab-item style="height: 100%">
style="height:100%"
>
<rows-xc-data-table <rows-xc-data-table
ref="tabs7" ref="tabs7"
:is-view="isView" :is-view="isView"
@ -51,15 +49,15 @@
</template> </template>
<script> <script>
import { mapActions } from 'vuex' import { mapActions } from "vuex";
import dlgLabelSubmitCancel from '../utils/dlgLabelSubmitCancel' import dlgLabelSubmitCancel from "../utils/dlgLabelSubmitCancel";
import { isMetaTable } from '@/helpers/xutils' import { isMetaTable } from "@/helpers/xutils";
import RowsXcDataTable from '@/components/project/spreadsheet/rowsXcDataTable' import RowsXcDataTable from "@/components/project/spreadsheet/rowsXcDataTable";
export default { export default {
components: { components: {
RowsXcDataTable, RowsXcDataTable,
dlgLabelSubmitCancel dlgLabelSubmitCancel,
}, },
data() { data() {
return { return {
@ -74,90 +72,90 @@ export default {
loadRows: false, loadRows: false,
loadColumnsMock: false, loadColumnsMock: false,
relationTabs: [], relationTabs: [],
deleteId: null deleteId: null,
} };
}, },
methods: { methods: {
async handleKeyDown(event) { async handleKeyDown(event) {
const activeTabEleKey = `tabs${this.active}` const activeTabEleKey = `tabs${this.active}`;
if (this.$refs[activeTabEleKey] && if (
this.$refs[activeTabEleKey] &&
this.$refs[activeTabEleKey].handleKeyDown this.$refs[activeTabEleKey].handleKeyDown
) { ) {
await this.$refs[activeTabEleKey].handleKeyDown(event) await this.$refs[activeTabEleKey].handleKeyDown(event);
} }
}, },
...mapActions({ ...mapActions({
removeTableTab: 'tabs/removeTableTab', removeTableTab: "tabs/removeTableTab",
loadTablesFromParentTreeNode: 'project/loadTablesFromParentTreeNode' loadTablesFromParentTreeNode: "project/loadTablesFromParentTreeNode",
}), }),
mtdNewTableUpdate(value) { mtdNewTableUpdate(value) {
this.newTableCopy = value this.newTableCopy = value;
}, },
async deleteTable(action = '', id) { async deleteTable(action = "", id) {
if (id) { if (id) {
this.deleteId = id this.deleteId = id;
} }
if (action === 'showDialog') { if (action === "showDialog") {
this.dialogShow = true this.dialogShow = true;
} else if (action === 'hideDialog') { } else if (action === "hideDialog") {
this.dialogShow = false this.dialogShow = false;
} else { } else {
// todo : check relations and triggers // todo : check relations and triggers
try { try {
await this.$api.dbTable.delete(this.deleteId) await this.$api.dbTable.delete(this.deleteId);
this.removeTableTab({ this.removeTableTab({
env: this.nodes.env, env: this.nodes.env,
dbAlias: this.nodes.dbAlias, dbAlias: this.nodes.dbAlias,
table_name: this.nodes.table_name table_name: this.nodes.table_name,
}) });
await this.loadTablesFromParentTreeNode({ await this.loadTablesFromParentTreeNode({
_nodes: { _nodes: {
...this.nodes ...this.nodes,
} },
}) });
this.$store.commit('meta/MutMeta', { this.$store.commit("meta/MutMeta", {
key: this.nodes.table_name, key: this.nodes.table_name,
value: null value: null,
}) });
this.$store.commit('meta/MutMeta', { this.$store.commit("meta/MutMeta", {
key: this.deleteId, key: this.deleteId,
value: null value: null,
}) });
} catch (e) { } catch (e) {
const msg = await this._extractSdkResponseErrorMsg(e) const msg = await this._extractSdkResponseErrorMsg(e);
this.$toast.error(msg).goAway(3000) this.$toast.error(msg).goAway(3000);
} }
this.dialogShow = false this.dialogShow = false;
this.$tele.emit('table:delete:submit') this.$e("a:table:delete");
} }
}, },
onTabChange() { onTabChange() {
this.$emit('update:hideLogWindows', this.active === 2) this.$emit("update:hideLogWindows", this.active === 2);
} },
}, },
computed: { computed: {
isMetaTable() { isMetaTable() {
return isMetaTable(this.nodes.table_name) return isMetaTable(this.nodes.table_name);
} },
}, },
mounted() { mounted() {
this.onTabChange() this.onTabChange();
}, },
props: { props: {
nodes: Object, nodes: Object,
hideLogWindows: Boolean, hideLogWindows: Boolean,
tabId: String, tabId: String,
isActive: Boolean, isActive: Boolean,
isView: Boolean isView: Boolean,
} },
} };
</script> </script>
<style scoped> <style scoped>
/*/deep/ .table-tabs > .v-tabs-items { /*/deep/ .table-tabs > .v-tabs-items {
border-top: 1px solid #7F828B33; border-top: 1px solid #7F828B33;
}*/ }*/
@ -166,12 +164,13 @@ export default {
margin-top: -2px; margin-top: -2px;
} }
.table-tabs, /deep/ .table-tabs > .v-windows { .table-tabs,
/deep/ .table-tabs > .v-windows {
height: 100%; height: 100%;
} }
/deep/ .v-window-item { /deep/ .v-window-item {
height: 100% height: 100%;
} }
.rel-row-parent { .rel-row-parent {
@ -189,7 +188,6 @@ export default {
overflow: hidden; overflow: hidden;
color: grey; color: grey;
} }
</style> </style>
<!-- <!--
/** /**

591
packages/nc-gui/components/project/tableTabs/webhooks.vue

@ -3,73 +3,64 @@
<v-toolbar flat height="42" class="toolbar-border-bottom"> <v-toolbar flat height="42" class="toolbar-border-bottom">
<v-toolbar-title> <v-toolbar-title>
<v-breadcrumbs <v-breadcrumbs
:items="[{ :items="[
text: nodes.env, {
disabled: true, text: nodes.env,
href: '#' disabled: true,
},{ href: '#',
text: nodes.dbAlias, },
disabled: true, {
href: '#' text: nodes.dbAlias,
}, disabled: true,
{ href: '#',
text: nodes.title + ' (Webhooks)', },
disabled: true, {
href: '#' text: nodes.title + ' (Webhooks)',
}]" disabled: true,
href: '#',
},
]"
divider=">" divider=">"
small small
> >
<template #divider> <template #divider>
<v-icon small color="grey lighten-2"> <v-icon small color="grey lighten-2"> forward </v-icon>
forward
</v-icon>
</template> </template>
</v-breadcrumbs> </v-breadcrumbs>
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
<!--tooltip="Close webhooks modal"--> <!--tooltip="Close webhooks modal"-->
<x-btn <x-btn outlined small @click.prevent="$emit('close')">
outlined <v-icon small left> mdi-close-circle-outline </v-icon>
small
@click.prevent="$emit('close')"
>
<v-icon small left>
mdi-close-circle-outline
</v-icon>
<!-- Close --> <!-- Close -->
{{ $t('general.close') }} {{ $t("general.close") }}
</x-btn> </x-btn>
<!--tooltip="Reload hooks"--> <!--tooltip="Reload hooks"-->
<x-btn <x-btn
v-ge="['hooks','reload']" v-ge="['hooks', 'reload']"
outlined outlined
color="primary" color="primary"
small small
@click.prevent="loadHooksList" @click.prevent="loadHooksList"
> >
<v-icon small left> <v-icon small left> mdi-reload </v-icon>
mdi-reload
</v-icon>
<!-- Reload --> <!-- Reload -->
{{ $t('general.reload') }} {{ $t("general.reload") }}
</x-btn> </x-btn>
<!--:tooltip="$t('tooltip.saveChanges')"--> <!--:tooltip="$t('tooltip.saveChanges')"-->
<x-btn <x-btn
v-ge="['hooks','add new']" v-ge="['hooks', 'add new']"
outlined outlined
color="primary" color="primary"
small small
@click.prevent="addNewHook" @click.prevent="addNewHook"
> >
<v-icon small left> <v-icon small left> mdi-plus </v-icon>
mdi-plus
</v-icon>
<!--Add New--> <!--Add New-->
{{ $t('activity.addWebhook') }} {{ $t("activity.addWebhook") }}
</x-btn> </x-btn>
<!-- <x-btn outlined tooltip="Save Changes" <!-- <x-btn outlined tooltip="Save Changes"
@ -84,12 +75,7 @@
</x-btn>--> </x-btn>-->
</v-toolbar> </v-toolbar>
<v-form <v-form ref="form" v-model="valid" class="mx-auto" lazy-validation>
ref="form"
v-model="valid"
class="mx-auto"
lazy-validation
>
<v-container fluid> <v-container fluid>
<v-row> <v-row>
<v-col cols="7"> <v-col cols="7">
@ -102,30 +88,30 @@
<th /> <th />
<th> <th>
<!--Title--> <!--Title-->
{{ $t('general.title') }} {{ $t("general.title") }}
</th> </th>
<th> <th>
<!--Event--> <!--Event-->
{{ $t('general.event') }} {{ $t("general.event") }}
</th> </th>
<th> <th>
<!--Condition--> <!--Condition-->
{{ $t('general.condition') }} {{ $t("general.condition") }}
</th> </th>
<th> <th>
<!--Notify Via--> <!--Notify Via-->
{{ $t('labels.notifyVia') }} {{ $t("labels.notifyVia") }}
</th> </th>
<th> <th>
<!--Action--> <!--Action-->
{{ $t('labels.action') }} {{ $t("labels.action") }}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-if="hooks && hooks.length"> <template v-if="hooks && hooks.length">
<tr v-for="(item,i) in hooks" :key="i"> <tr v-for="(item, i) in hooks" :key="i">
<td> <td>
<v-radio :value="i" /> <v-radio :value="i" />
</td> </td>
@ -136,12 +122,18 @@
mdi-check-bold mdi-check-bold
</v-icon> </v-icon>
</td> </td>
<td>{{ item.notification && item.notification.type }}</td>
<td> <td>
<x-icon small color="error" @click.stop="deleteHook(item, i)"> {{ item.notification && item.notification.type }}
</td>
<td>
<x-icon
small
color="error"
@click.stop="deleteHook(item, i)"
>
mdi-delete mdi-delete
</x-icon> </x-icon>
<!-- <x-icon small :color="loading || !valid || !hook.event ? 'grey' : 'primary'" <!-- <x-icon small :color="loading || !valid || !hook.event ? 'grey' : 'primary'"
@click.stop="(!loading && valid && hook.event) && saveHooks()">save @click.stop="(!loading && valid && hook.event) && saveHooks()">save
</x-icon>--> </x-icon>-->
</td> </td>
@ -151,17 +143,15 @@
<td colspan="6" class="text-center py-5"> <td colspan="6" class="text-center py-5">
<!--:tooltip="$t('tooltip.saveChanges')"--> <!--:tooltip="$t('tooltip.saveChanges')"-->
<x-btn <x-btn
v-ge="['hooks','add new']" v-ge="['hooks', 'add new']"
outlined outlined
color="primary" color="primary"
small small
@click.prevent="addNewHook" @click.prevent="addNewHook"
> >
<v-icon small left> <v-icon small left> mdi-plus </v-icon>
mdi-plus
</v-icon>
<!--Add New Webhook--> <!--Add New Webhook-->
{{ $t('activity.addWebhook') }} {{ $t("activity.addWebhook") }}
</x-btn> </x-btn>
</td> </td>
</tr> </tr>
@ -177,20 +167,17 @@
Webhook Webhook
<v-spacer /> <v-spacer />
<x-btn <x-btn
v-ge="['hooks','save']" v-ge="['hooks', 'save']"
outlined outlined
tooltip="Save" tooltip="Save"
color="primary" color="primary"
small small
:disabled="loading || !valid || !hook.event" :disabled="loading || !valid || !hook.event"
@click.prevent="saveHooks" @click.prevent="saveHooks"
> >
<v-icon small left> <v-icon small left> save </v-icon>
save
</v-icon>
<!-- Save --> <!-- Save -->
{{ $t('general.save') }} {{ $t("general.save") }}
</x-btn> </x-btn>
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
@ -201,7 +188,7 @@
dense dense
:label="$t('general.title')" :label="$t('general.title')"
required required
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
/> />
<webhook-event <webhook-event
@ -242,12 +229,12 @@
:label="$t('general.notification')" :label="$t('general.notification')"
required required
:items="notificationList" :items="notificationList"
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
class="caption" class="caption"
:prepend-inner-icon="notificationIcon[hook.notification.type]" :prepend-inner-icon="notificationIcon[hook.notification.type]"
@change="onNotTypeChange" @change="onNotTypeChange"
> >
<template #item="{item}"> <template #item="{ item }">
<v-list-item-icon> <v-list-item-icon>
<v-icon small> <v-icon small>
{{ notificationIcon[item] }} {{ notificationIcon[item] }}
@ -267,7 +254,7 @@
<v-combobox <v-combobox
v-if="slackChannels" v-if="slackChannels"
v-model="notification.channels" v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
:items="slackChannels" :items="slackChannels"
item-text="channel" item-text="channel"
label="Select Slack channels" label="Select Slack channels"
@ -281,7 +268,7 @@
<v-combobox <v-combobox
v-if="teamsChannels" v-if="teamsChannels"
v-model="notification.channels" v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
:items="teamsChannels" :items="teamsChannels"
item-text="channel" item-text="channel"
label="Select Teams channels" label="Select Teams channels"
@ -295,7 +282,7 @@
<v-combobox <v-combobox
v-if="discordChannels" v-if="discordChannels"
v-model="notification.channels" v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
:items="discordChannels" :items="discordChannels"
item-text="channel" item-text="channel"
label="Select Discord channels" label="Select Discord channels"
@ -309,7 +296,7 @@
<v-combobox <v-combobox
v-if="mattermostChannels" v-if="mattermostChannels"
v-model="notification.channels" v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]" :rules="[(v) => !!v || `${$t('general.required')}`]"
:items="mattermostChannels" :items="mattermostChannels"
item-text="channel" item-text="channel"
label="Select Mattermost channels" label="Select Mattermost channels"
@ -330,7 +317,10 @@
dense dense
outlined outlined
:label="input.label" :label="input.label"
:rules="[v => !input.required || !!v || `${$t('general.required')}`]" :rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/> />
<v-text-field <v-text-field
v-else v-else
@ -340,7 +330,10 @@
dense dense
outlined outlined
:label="input.label" :label="input.label"
:rules="[v => !input.required || !!v || `${$t('general.required')}`]" :rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/> />
</template> </template>
</template> </template>
@ -348,25 +341,27 @@
<v-card-text> <v-card-text>
<span class="caption grey--text"> <span class="caption grey--text">
<em>Available context variables are <strong>data and user</strong></em> <em
>Available context variables are
<strong>data and user</strong></em
>
<v-tooltip top> <v-tooltip top>
<template #activator="{on}"> <template #activator="{ on }">
<v-icon <v-icon small color="grey" class="ml-2" v-on="on"
small >mdi-information</v-icon
color="grey" >
class="ml-2"
v-on="on"
>mdi-information</v-icon>
</template> </template>
<span class="caption"> <span class="caption">
<strong>data</strong> : Row data <br> <strong>data</strong> : Row data <br />
<strong>user</strong> : User information<br> <strong>user</strong> : User information<br />
</span> </span>
</v-tooltip> </v-tooltip>
<br> <br />
<a href="https://docs.nocodb.com/developer-resources/webhooks/"> <a
href="https://docs.nocodb.com/developer-resources/webhooks/"
>
<!--Document Reference--> <!--Document Reference-->
{{ $t('labels.docReference') }} {{ $t("labels.docReference") }}
</a> </a>
</span> </span>
@ -377,8 +372,8 @@
filters, filters,
notification: { notification: {
...hook.notification, ...hook.notification,
payload: notification payload: notification,
} },
}" }"
/> />
</v-card-text> </v-card-text>
@ -391,22 +386,22 @@
</template> </template>
<script> <script>
import HttpWebhook from './webhook/httpWebhook' import HttpWebhook from "./webhook/httpWebhook";
import ColumnFilter from '~/components/project/spreadsheet/components/columnFilter' import ColumnFilter from "~/components/project/spreadsheet/components/columnFilter";
// import FormInput from '~/components/project/appStore/FormInput' // import FormInput from '~/components/project/appStore/FormInput'
import WebhookEvent from '~/components/project/tableTabs/webhookEvent' import WebhookEvent from "~/components/project/tableTabs/webhookEvent";
import WebhooksTest from '~/components/project/tableTabs/webhooksTest' import WebhooksTest from "~/components/project/tableTabs/webhooksTest";
export default { export default {
name: 'Webhooks', name: "Webhooks",
components: { components: {
WebhooksTest, WebhooksTest,
HttpWebhook, HttpWebhook,
WebhookEvent, WebhookEvent,
// FormInput, // FormInput,
ColumnFilter ColumnFilter,
}, },
props: ['nodes'], props: ["nodes"],
data: () => ({ data: () => ({
key: 0, key: 0,
apps: {}, apps: {},
@ -421,138 +416,148 @@ export default {
meta: null, meta: null,
loading: false, loading: false,
notificationList: [ notificationList: [
'Email', "Email",
'Slack', "Slack",
'Microsoft Teams', "Microsoft Teams",
'Discord', "Discord",
'Mattermost', "Mattermost",
'Twilio', "Twilio",
'Whatsapp Twilio', "Whatsapp Twilio",
'URL' "URL",
], ],
filters: [], filters: [],
hook: null, hook: null,
notification: {}, notification: {},
notificationIcon: { notificationIcon: {
URL: 'mdi-link', URL: "mdi-link",
Email: 'mdi-email', Email: "mdi-email",
Slack: 'mdi-slack', Slack: "mdi-slack",
'Microsoft Teams': 'mdi-microsoft-teams', "Microsoft Teams": "mdi-microsoft-teams",
Discord: 'mdi-discord', Discord: "mdi-discord",
Mattermost: 'mdi-chat', Mattermost: "mdi-chat",
'Whatsapp Twilio': 'mdi-whatsapp', "Whatsapp Twilio": "mdi-whatsapp",
Twilio: 'mdi-cellphone-message' Twilio: "mdi-cellphone-message",
}, },
urlRules: [ urlRules: [
v => !v || !v.trim() || /^https?:\/\/.{1,}/.test(v) || 'Not a valid URL' (v) =>
!v || !v.trim() || /^https?:\/\/.{1,}/.test(v) || "Not a valid URL",
], ],
fieldList: [], fieldList: [],
inputs: { inputs: {
Email: [ Email: [
{ {
key: 'to', key: "to",
label: 'To Address', label: "To Address",
placeholder: 'To Address', placeholder: "To Address",
type: 'SingleLineText', type: "SingleLineText",
required: true required: true,
}, },
{ {
key: 'subject', key: "subject",
label: 'Subject', label: "Subject",
placeholder: 'Subject', placeholder: "Subject",
type: 'SingleLineText', type: "SingleLineText",
required: true required: true,
}, { },
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
} required: true,
},
], ],
Slack: [{ Slack: [
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
} required: true,
},
], ],
'Microsoft Teams': [{ "Microsoft Teams": [
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
} required: true,
},
], ],
Discord: [{ Discord: [
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
} required: true,
},
], ],
Mattermost: [{ Mattermost: [
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
} required: true,
},
], ],
Twilio: [{ Twilio: [
key: 'body', {
label: 'Body', key: "body",
placeholder: 'Body', label: "Body",
type: 'LongText', placeholder: "Body",
required: true type: "LongText",
}, { required: true,
key: 'to', },
label: 'Comma separated Mobile #', {
placeholder: 'Comma separated Mobile #', key: "to",
type: 'LongText', label: "Comma separated Mobile #",
required: true placeholder: "Comma separated Mobile #",
}], type: "LongText",
'Whatsapp Twilio': [{ required: true,
key: 'body', },
label: 'Body', ],
placeholder: 'Body', "Whatsapp Twilio": [
type: 'LongText', {
required: true key: "body",
}, { label: "Body",
key: 'to', placeholder: "Body",
label: 'Comma separated Mobile #', type: "LongText",
placeholder: 'Comma separated Mobile #', required: true,
type: 'LongText', },
required: true {
}] key: "to",
} label: "Comma separated Mobile #",
placeholder: "Comma separated Mobile #",
type: "LongText",
required: true,
},
],
},
}), }),
async created() { async created() {
await this.loadMeta() await this.loadMeta();
await this.loadHooksList() await this.loadHooksList();
// todo: load only necessary plugins // todo: load only necessary plugins
await this.loadPluginList() await this.loadPluginList();
this.selectedHook = 0 this.selectedHook = 0;
this.onEventChange() this.onEventChange();
}, },
methods: { methods: {
async loadPluginList() { async loadPluginList() {
try { try {
// const plugins = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginList']) // const plugins = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginList'])
const plugins = (await this.$api.plugin.list()).list const plugins = (await this.$api.plugin.list()).list;
// plugins.push(...plugins.splice(0, 3)) // plugins.push(...plugins.splice(0, 3))
this.apps = plugins.reduce((o, p) => { this.apps = plugins.reduce((o, p) => {
p.tags = p.tags ? p.tags.split(',') : [] p.tags = p.tags ? p.tags.split(",") : [];
p.parsedInput = p.input && JSON.parse(p.input) p.parsedInput = p.input && JSON.parse(p.input);
o[p.title] = p o[p.title] = p;
return o return o;
}, {}) }, {});
} catch (e) { } catch (e) {}
}
}, },
checkConditionAvail() { checkConditionAvail() {
// if (!process.env.EE) { // if (!process.env.EE) {
@ -562,84 +567,102 @@ export default {
// this.hook.condition = [] // this.hook.condition = []
}, },
async onNotTypeChange() { async onNotTypeChange() {
this.notification = {} this.notification = {};
if (this.hook.notification.type === 'Slack') { if (this.hook.notification.type === "Slack") {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', { // const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Slack' // title: 'Slack'
// }]) // }])
// this.slackChannels = JSON.parse(plugin.input) || [] // this.slackChannels = JSON.parse(plugin.input) || []
this.slackChannels = (this.apps && this.apps.Slack && this.apps.Slack.parsedInput) || [] this.slackChannels =
(this.apps && this.apps.Slack && this.apps.Slack.parsedInput) || [];
} }
if (this.hook.notification.type === 'Microsoft Teams') { if (this.hook.notification.type === "Microsoft Teams") {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', { // const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Microsoft Teams' // title: 'Microsoft Teams'
// }]) // }])
// this.teamsChannels = JSON.parse(plugin.input) || [] // this.teamsChannels = JSON.parse(plugin.input) || []
this.teamsChannels = (this.apps && this.apps['Microsoft Teams'] && this.apps['Microsoft Teams'].parsedInput) || [] this.teamsChannels =
(this.apps &&
this.apps["Microsoft Teams"] &&
this.apps["Microsoft Teams"].parsedInput) ||
[];
} }
if (this.hook.notification.type === 'Discord') { if (this.hook.notification.type === "Discord") {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', { // const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Discord' // title: 'Discord'
// }]) // }])
this.discordChannels = (this.apps && this.apps.Discord && this.apps.Discord.parsedInput) || [] this.discordChannels =
(this.apps && this.apps.Discord && this.apps.Discord.parsedInput) ||
[];
} }
if (this.hook.notification.type === 'Mattermost') { if (this.hook.notification.type === "Mattermost") {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', { // const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Mattermost' // title: 'Mattermost'
// }]) // }])
// this.mattermostChannels = JSON.parse(plugin.input) || [] // this.mattermostChannels = JSON.parse(plugin.input) || []
this.mattermostChannels = (this.apps && this.apps.Mattermost && this.apps.Mattermost.parsedInput) || [] this.mattermostChannels =
(this.apps &&
this.apps.Mattermost &&
this.apps.Mattermost.parsedInput) ||
[];
} }
}, },
async onEventChange() { async onEventChange() {
this.key++ this.key++;
if (!this.hooks || !this.hooks.length) { if (!this.hooks || !this.hooks.length) {
return return;
} }
const { const { notification: { payload, type } = {}, ...hook } =
notification: { this.hooks[this.selectedHook] || {};
payload,
type
} = {},
...hook
} = this.hooks[this.selectedHook] || {}
this.hook = { this.hook = {
...hook, ...hook,
notification: { notification: {
type type,
} },
} };
// this.enableCondition = !!(this.hook && this.hook.condition && Object.keys(this.hook.condition).length) // this.enableCondition = !!(this.hook && this.hook.condition && Object.keys(this.hook.condition).length)
await this.onNotTypeChange() await this.onNotTypeChange();
this.notification = payload this.notification = payload;
if (this.hook.notification.type === 'Slack') { if (this.hook.notification.type === "Slack") {
this.notification.webhook_url = this.notification.webhook_url && this.notification.webhook_url =
this.notification.webhook_url.map(v => this.slackChannels.find(s => v.webhook_url === s.webhook_url)) this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.slackChannels.find((s) => v.webhook_url === s.webhook_url)
);
} }
if (this.hook.notification.type === 'Microsoft Teams') { if (this.hook.notification.type === "Microsoft Teams") {
this.notification.webhook_url = this.notification.webhook_url && this.notification.webhook_url =
this.notification.webhook_url.map(v => this.teamsChannels.find(s => v.webhook_url === s.webhook_url)) this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.teamsChannels.find((s) => v.webhook_url === s.webhook_url)
);
} }
if (this.hook.notification.type === 'Discord') { if (this.hook.notification.type === "Discord") {
this.notification.webhook_url = this.notification.webhook_url && this.notification.webhook_url =
this.notification.webhook_url.map(v => this.discordChannels.find(s => v.webhook_url === s.webhook_url)) this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.discordChannels.find((s) => v.webhook_url === s.webhook_url)
);
} }
if (this.hook.notification.type === 'Mattermost') { if (this.hook.notification.type === "Mattermost") {
this.notification.webhook_url = this.notification.webhook_url && this.notification.webhook_url =
this.notification.webhook_url.map(v => this.mattermostChannels.find(s => v.webhook_url === s.webhook_url)) this.notification.webhook_url &&
this.notification.webhook_url.map((v) =>
this.mattermostChannels.find((s) => v.webhook_url === s.webhook_url)
);
} }
if (this.hook.notification.type === 'URL') { if (this.hook.notification.type === "URL") {
// eslint-disable-next-line no-self-assign // eslint-disable-next-line no-self-assign
this.notification.api = this.notification.api this.notification.api = this.notification.api;
} }
}, },
async saveHooks() { async saveHooks() {
if (!this.$refs.form.validate() || !this.valid || !this.hook.event) { if (!this.$refs.form.validate() || !this.valid || !this.hook.event) {
return return;
} }
this.loading = true this.loading = true;
try { try {
// const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [ // const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { // {
@ -656,58 +679,66 @@ export default {
// } // }
// } // }
// ]) // ])
let res let res;
if (this.hook.id) { if (this.hook.id) {
res = await this.$api.dbTableWebhook.update(this.hook.id, { res = await this.$api.dbTableWebhook.update(this.hook.id, {
...this.hook, ...this.hook,
notification: { notification: {
...this.hook.notification, ...this.hook.notification,
payload: this.notification payload: this.notification,
} },
}) });
} else { } else {
res = await this.$api.dbTableWebhook.create(this.meta.id, { res = await this.$api.dbTableWebhook.create(this.meta.id, {
...this.hook, ...this.hook,
notification: { notification: {
...this.hook.notification, ...this.hook.notification,
payload: this.notification payload: this.notification,
} },
}) });
} }
if (!this.hook.id && res) { if (!this.hook.id && res) {
this.hook.id = res.id this.hook.id = res.id;
} }
if (this.$refs.filter) { if (this.$refs.filter) {
await this.$refs.filter.applyChanges(false, { await this.$refs.filter.applyChanges(false, {
hookId: this.hook.id hookId: this.hook.id,
}) });
} }
this.$toast.success('Webhook details updated successfully').goAway(3000) this.$toast
.success("Webhook details updated successfully")
.goAway(3000);
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.loading = false this.loading = false;
await this.loadHooksList() await this.loadHooksList();
this.$tele.emit(`webhooks:save:${this.hook.operation}:${this.hook.condition}:${this.hook.notification.type}`) this.$e("a:webhook:add", {
operation: this.hook.operation,
condition: this.hook.condition,
notification: this.hook.notification.type,
});
}, },
async loadMeta() { async loadMeta() {
this.loadingMeta = true this.loadingMeta = true;
// const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ // const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env, // env: this.nodes.env,
// dbAlias: this.nodes.dbAlias // dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', { // }, 'tableXcModelGet', {
// tn: this.nodes.table_name // tn: this.nodes.table_name
// }] ) // }] )
this.meta = await this.$store.dispatch('meta/ActLoadMeta', { table_name: this.nodes.table_name })// JSON.parse(tableMeta.meta) this.meta = await this.$store.dispatch("meta/ActLoadMeta", {
this.fieldList = this.meta.columns.map(c => c.column_name) table_name: this.nodes.table_name,
this.loadingMeta = false }); // JSON.parse(tableMeta.meta)
this.fieldList = this.meta.columns.map((c) => c.column_name);
this.loadingMeta = false;
}, },
async loadHooksList() { async loadHooksList() {
this.key++ this.key++;
this.loading = true this.loading = true;
// const hooks = await this.$store.dispatch('sqlMgr/ActSqlOp', [{ // const hooks = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env, // env: this.nodes.env,
// dbAlias: this.nodes.dbAlias // dbAlias: this.nodes.dbAlias
@ -715,49 +746,49 @@ export default {
// tn: this.nodes.table_name // tn: this.nodes.table_name
// }]) // }])
const hooks = await this.$api.dbTableWebhook.list(this.meta.id) const hooks = await this.$api.dbTableWebhook.list(this.meta.id);
this.hooks = hooks.list.map((h) => { this.hooks = hooks.list.map((h) => {
h.notification = h.notification && JSON.parse(h.notification) h.notification = h.notification && JSON.parse(h.notification);
// h.condition = h.condition && JSON.parse(h.condition) // h.condition = h.condition && JSON.parse(h.condition)
return h return h;
}) });
this.loading = false this.loading = false;
}, },
addNewHook() { addNewHook() {
this.key++ this.key++;
this.selectedHook = this.hooks.length this.selectedHook = this.hooks.length;
this.hooks.push({ this.hooks.push({
notification: { notification: {
// type:'Email' // type:'Email'
} },
}) });
this.onEventChange() this.onEventChange();
this.$refs.form.resetValidation() this.$refs.form.resetValidation();
this.$tele.emit(`webhook:add:trigger:${this.hooks.length}`) this.$e("c:webhook:add", { count: this.hooks.length });
}, },
async deleteHook(item, i) { async deleteHook(item, i) {
try { try {
if (item.id) { if (item.id) {
await this.$api.dbTableWebhook.delete(item.id) await this.$api.dbTableWebhook.delete(item.id);
this.hooks.splice(i, 1) this.hooks.splice(i, 1);
} else { } else {
this.hooks.splice(i, 1) this.hooks.splice(i, 1);
} }
this.$toast.success('Hook deleted successfully').goAway(3000) this.$toast.success("Hook deleted successfully").goAway(3000);
if (!this.hooks.length) { if (!this.hooks.length) {
this.hook = null this.hook = null;
} }
} catch (e) { } catch (e) {
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
this.$tele.emit('webhook:delete') this.$e("a:webhook:delete");
} },
} },
} };
</script> </script>
<style scoped> <style scoped>
@ -766,7 +797,7 @@ export default {
/*}*/ /*}*/
/deep/ label { /deep/ label {
font-size: 0.75rem !important font-size: 0.75rem !important;
} }
</style> </style>
<!-- <!--

84
packages/nc-gui/components/projectList/createNewProjectBtn.vue

@ -4,7 +4,7 @@
<slot :on="on"> <slot :on="on">
<div> <div>
<v-btn <v-btn
v-if="_isUIAllowed('projectCreate',true)" v-if="_isUIAllowed('projectCreate', true)"
v-ge="['home', 'project-new']" v-ge="['home', 'project-new']"
:x-large="$vuetify.breakpoint.lgAndUp" :x-large="$vuetify.breakpoint.lgAndUp"
:large="$vuetify.breakpoint.mdAndDown" :large="$vuetify.breakpoint.mdAndDown"
@ -15,15 +15,11 @@
class="nc-new-project-menu elevation-3" class="nc-new-project-menu elevation-3"
v-on="on" v-on="on"
> >
<v-icon class="mr-2"> <v-icon class="mr-2"> mdi-plus </v-icon>
mdi-plus
</v-icon>
<!-- New Project --> <!-- New Project -->
{{ $t('title.newProj') }} {{ $t("title.newProj") }}
<v-icon class="mr-1" small> <v-icon class="mr-1" small> mdi-menu-down </v-icon>
mdi-menu-down
</v-icon>
</v-btn> </v-btn>
</div> </div>
</slot> </slot>
@ -34,32 +30,21 @@
@click="onCreateProject('xcdb')" @click="onCreateProject('xcdb')"
> >
<v-list-item-icon class="mr-2"> <v-list-item-icon class="mr-2">
<v-icon small color="blue"> <v-icon small color="blue"> mdi-plus </v-icon>
mdi-plus
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title> <v-list-item-title>
<!-- Create --> <!-- Create -->
<span>{{ <span>{{ $t("general.create") }}</span>
$t('general.create')
}}</span>
</v-list-item-title> </v-list-item-title>
<v-spacer /> <v-spacer />
<v-tooltip right> <v-tooltip right>
<template #activator="{ on }"> <template #activator="{ on }">
<v-icon <v-icon x-small color="grey" class="ml-4" v-on="on">
x-small
color="grey"
class="ml-4"
v-on="on"
>
mdi-information-outline mdi-information-outline
</v-icon> </v-icon>
</template> </template>
<!-- Create a new project --> <!-- Create a new project -->
<span class="caption">{{ <span class="caption">{{ $t("tooltip.xcDB") }}</span>
$t('tooltip.xcDB')
}}</span>
</v-tooltip> </v-tooltip>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
@ -68,41 +53,30 @@
@click="onCreateProject()" @click="onCreateProject()"
> >
<v-list-item-icon class="mr-2"> <v-list-item-icon class="mr-2">
<v-icon small color="green"> <v-icon small color="green"> mdi-database-outline </v-icon>
mdi-database-outline
</v-icon>
</v-list-item-icon> </v-list-item-icon>
<v-list-item-title> <v-list-item-title>
<!-- Create By Connecting <br>To An External Database --> <!-- Create By Connecting <br>To An External Database -->
<span <span
style="line-height: 1.5em" style="line-height: 1.5em"
v-html=" v-html="$t('activity.createProjectExtended.extDB')"
$t('activity.createProjectExtended.extDB')
"
/> />
</v-list-item-title> </v-list-item-title>
<v-spacer /> <v-spacer />
<v-tooltip right> <v-tooltip right>
<template #activator="{ on }"> <template #activator="{ on }">
<v-icon <v-icon x-small color="grey" class="ml-4" v-on="on">
x-small
color="grey"
class="ml-4"
v-on="on"
>
mdi-information-outline mdi-information-outline
</v-icon> </v-icon>
</template> </template>
<!-- Supports MySQL, PostgreSQL, SQL Server & SQLite --> <!-- Supports MySQL, PostgreSQL, SQL Server & SQLite -->
<span class="caption">{{ <span class="caption">{{ $t("tooltip.extDB") }}</span>
$t('tooltip.extDB')
}}</span>
</v-tooltip> </v-tooltip>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
<x-btn <x-btn
v-else-if="_isUIAllowed('projectCreate',true)" v-else-if="_isUIAllowed('projectCreate', true)"
v-ge="['home', 'project-new']" v-ge="['home', 'project-new']"
outlined outlined
data-v-step="1" data-v-step="1"
@ -110,33 +84,35 @@
@click="onCreateProject('xcdb')" @click="onCreateProject('xcdb')"
> >
<!-- New Project --> <!-- New Project -->
{{ $t('title.newProj') }} {{ $t("title.newProj") }}
</x-btn> </x-btn>
<span v-else /> <span v-else />
</template> </template>
<script> <script>
export default { export default {
name: 'CreateNewProjectBtn', name: "CreateNewProjectBtn",
computed: { computed: {
connectToExternalDB() { connectToExternalDB() {
return this.$store.state.project && this.$store.state.project.projectInfo && this.$store.state.project.projectInfo.connectToExternalDB return (
} this.$store.state.project &&
this.$store.state.project.projectInfo &&
this.$store.state.project.projectInfo.connectToExternalDB
);
},
}, },
methods: { methods: {
onCreateProject(xcdb) { onCreateProject(xcdb) {
if (xcdb === 'xcdb') { if (xcdb === "xcdb") {
this.$router.push('/project/xcdb') this.$router.push("/project/xcdb");
this.$tele.emit('project:create:xcdb:trigger') this.$e("c:project:create:xcdb");
} else { } else {
this.$router.push('/project/0') this.$router.push("/project/0");
this.$tele.emit('project:create:extdb:trigger') this.$e("c:project:create:extdb");
} }
} },
} },
} };
</script> </script>
<style scoped> <style scoped></style>
</style>

396
packages/nc-gui/components/projectTabs.vue

@ -1,5 +1,9 @@
<template> <template>
<v-container fluid class="ph-no-capture project-container ma-0 pa-0 " style="position: relative"> <v-container
fluid
class="ph-no-capture project-container ma-0 pa-0"
style="position: relative"
>
<v-tabs <v-tabs
ref="projectTabs" ref="projectTabs"
v-model="activeTab" v-model="activeTab"
@ -11,16 +15,20 @@
next-icon="mdi-arrow-right-bold-box-outline" next-icon="mdi-arrow-right-bold-box-outline"
prev-icon="mdi-arrow-left-bold-box-outline" prev-icon="mdi-arrow-left-bold-box-outline"
show-arrows show-arrows
:class="{'dark-them' : $store.state.windows.darkTheme}" :class="{ 'dark-them': $store.state.windows.darkTheme }"
> >
<v-tabs-slider color="" /> <v-tabs-slider color="" />
<v-tab <v-tab
v-for="(tab,index) in tabs" v-for="(tab, index) in tabs"
:key="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" :key="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
class="divider project-tab xc-border-right" class="divider project-tab xc-border-right"
:title="tab.name" :title="tab.name"
:href="`#${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" :href="`#${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
@change="tabActivated(tab)" @change="tabActivated(tab)"
> >
<v-icon v-if="treeViewIcons[tab._nodes.type]" icon :small="true"> <v-icon v-if="treeViewIcons[tab._nodes.type]" icon :small="true">
@ -29,9 +37,13 @@
<span <span
class="flex-grow-1 caption font-weight-bold text-capitalize mx-2" class="flex-grow-1 caption font-weight-bold text-capitalize mx-2"
style=" style="
white-space: nowrap; white-space: nowrap;
overflow: hidden;max-width:140px;text-overflow:ellipsis" overflow: hidden;
>{{ tab.name }}</span> max-width: 140px;
text-overflow: ellipsis;
"
>{{ tab.name }}</span
>
<v-icon icon :small="true" @click="removeTab(index)"> <v-icon icon :small="true" @click="removeTab(index)">
mdi-close mdi-close
</v-icon> </v-icon>
@ -41,141 +53,147 @@
<v-tabs-items :value="activeTab"> <v-tabs-items :value="activeTab">
<v-tab-item <v-tab-item
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
:key="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" :key="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${
:value="`${(tab._nodes && tab._nodes.type) || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" (tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
:value="`${(tab._nodes && tab._nodes.type) || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
eager eager
:transition="false" :transition="false"
style="height:100%" style="height: 100%"
:reverse-transition="false" :reverse-transition="false"
> >
<div <div v-if="tab._nodes.type === 'table'" style="height: 100%">
v-if="tab._nodes.type === 'table'"
style="height:100%"
>
<!-- <sqlLogAndOutput :hide="hideLogWindows">--> <!-- <sqlLogAndOutput :hide="hideLogWindows">-->
<TableView <TableView
:ref="'tabs'+index" :ref="'tabs' + index"
:is-active="activeTab === `${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" :is-active="
:tab-id="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" activeTab ===
`${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`
"
:tab-id="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
:hide-log-windows.sync="hideLogWindows" :hide-log-windows.sync="hideLogWindows"
:nodes="tab._nodes" :nodes="tab._nodes"
/> />
<!-- </sqlLogAndOutput>--> <!-- </sqlLogAndOutput>-->
</div> </div>
<div v-else-if="tab._nodes.type === 'view'" style="height:100%"> <div v-else-if="tab._nodes.type === 'view'" style="height: 100%">
<!-- <sqlLogAndOutput>--> <!-- <sqlLogAndOutput>-->
<!-- <ViewTab :ref="'tabs'+index" :nodes="tab._nodes" />--> <!-- <ViewTab :ref="'tabs'+index" :nodes="tab._nodes" />-->
<TableView <TableView
:ref="'tabs'+index" :ref="'tabs' + index"
:is-active="activeTab === `${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" :is-active="
:tab-id="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${(tab._nodes && tab._nodes.dbAlias) || ''}||${tab.name}`" activeTab ===
`${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`
"
:tab-id="`${pid}||${(tab._nodes && tab._nodes).type || ''}||${
(tab._nodes && tab._nodes.dbAlias) || ''
}||${tab.name}`"
:hide-log-windows.sync="hideLogWindows" :hide-log-windows.sync="hideLogWindows"
:nodes="tab._nodes" :nodes="tab._nodes"
is-view is-view
/> />
<!-- </sqlLogAndOutput>--> <!-- </sqlLogAndOutput>-->
</div> </div>
<div v-else-if="tab._nodes.type === 'function'" style="height:100%"> <div v-else-if="tab._nodes.type === 'function'" style="height: 100%">
<sqlLogAndOutput> <sqlLogAndOutput>
<FunctionTab :ref="'tabs'+index" :nodes="tab._nodes" /> <FunctionTab :ref="'tabs' + index" :nodes="tab._nodes" />
</sqlLogAndOutput> </sqlLogAndOutput>
</div> </div>
<div v-else-if="tab._nodes.type === 'procedure'" style="height:100%"> <div v-else-if="tab._nodes.type === 'procedure'" style="height: 100%">
<sqlLogAndOutput> <sqlLogAndOutput>
<ProcedureTab :ref="'tabs'+index" :nodes="tab._nodes" /> <ProcedureTab :ref="'tabs' + index" :nodes="tab._nodes" />
</sqlLogAndOutput> </sqlLogAndOutput>
</div> </div>
<div v-else-if="tab._nodes.type === 'sequence'" style="height:100%"> <div v-else-if="tab._nodes.type === 'sequence'" style="height: 100%">
<sqlLogAndOutput> <sqlLogAndOutput>
<SequenceTab :ref="'tabs'+index" :nodes="tab._nodes" /> <SequenceTab :ref="'tabs' + index" :nodes="tab._nodes" />
</sqlLogAndOutput> </sqlLogAndOutput>
</div> </div>
<div v-else-if="tab._nodes.type === 'db'" style="height:100%"> <div v-else-if="tab._nodes.type === 'db'" style="height: 100%">
<audit-tab <audit-tab
:ref="'tabs'+index" :ref="'tabs' + index"
class="backgroundColor" class="backgroundColor"
:nodes="tab._nodes" :nodes="tab._nodes"
/> />
</div> </div>
<div v-else-if="tab._nodes.type === 'seedParserDir'" style="height:100%"> <div
v-else-if="tab._nodes.type === 'seedParserDir'"
style="height: 100%"
>
<sqlLogAndOutput> <sqlLogAndOutput>
<SeedTab :ref="'tabs'+index" :nodes="tab._nodes" /> <SeedTab :ref="'tabs' + index" :nodes="tab._nodes" />
</sqlLogAndOutput> </sqlLogAndOutput>
</div> </div>
<div v-else-if="tab._nodes.type === 'migrationsDir'" style="height:100%"> <div
v-else-if="tab._nodes.type === 'migrationsDir'"
style="height: 100%"
>
<audit-tab <audit-tab
:ref="'tabs'+index" :ref="'tabs' + index"
class="backgroundColor" class="backgroundColor"
:nodes="tab._nodes" :nodes="tab._nodes"
/> />
</div> </div>
<div v-else-if="tab._nodes.type === 'apisDir'" style="height:100%"> <div v-else-if="tab._nodes.type === 'apisDir'" style="height: 100%">
<ApisTab <ApisTab
:ref="'tabs'+index" :ref="'tabs' + index"
class="backgroundColor" class="backgroundColor"
:nodes="tab._nodes" :nodes="tab._nodes"
/> />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'apiClientDir'" v-else-if="tab._nodes.type === 'apiClientDir'"
style="height:100%" style="height: 100%"
> >
<ApiClientTab :ref="'tabs'+index" :nodes="tab._nodes" /> <ApiClientTab :ref="'tabs' + index" :nodes="tab._nodes" />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'apiClientSwaggerDir'" v-else-if="tab._nodes.type === 'apiClientSwaggerDir'"
style="height:100%" style="height: 100%"
> >
<ApiClientSwaggerTab :ref="'tabs'+index" :nodes="tab._nodes" /> <ApiClientSwaggerTab :ref="'tabs' + index" :nodes="tab._nodes" />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'sqlClientDir'" v-else-if="tab._nodes.type === 'sqlClientDir'"
style="height:100%" style="height: 100%"
> >
<sqlLogAndOutput> <sqlLogAndOutput>
<SqlClientTab :ref="'tabs'+index" :nodes="tab._nodes" /> <SqlClientTab :ref="'tabs' + index" :nodes="tab._nodes" />
</sqlLogAndOutput> </sqlLogAndOutput>
</div> </div>
<div <div v-else-if="tab._nodes.type === 'terminal'" style="height: 100%">
v-else-if="tab._nodes.type === 'terminal'" <x-term :ref="'tabs' + index" style="height: 100%" />
style="height:100%"
>
<x-term :ref="'tabs'+index" style="height: 100%" />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'graphqlClientDir'" v-else-if="tab._nodes.type === 'graphqlClientDir'"
style="height:100%" style="height: 100%"
> >
<graphql-client <graphql-client class="backgroundColor" style="height: 100%" />
class="backgroundColor"
style="height: 100%"
/>
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'swaggerClientDir'" v-else-if="tab._nodes.type === 'swaggerClientDir'"
style="height:100%" style="height: 100%"
> >
<swagger-client style="height: 100%" /> <swagger-client style="height: 100%" />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'grpcClient'" v-else-if="tab._nodes.type === 'grpcClient'"
style="height:100%" style="height: 100%"
> >
<grpc-client style="height: 100%" /> <grpc-client style="height: 100%" />
</div> </div>
<div <div v-else-if="tab._nodes.type === 'meta'" style="height: 100%">
v-else-if="tab._nodes.type === 'meta'" <xc-meta class="backgroundColor" style="height: 100%" />
style="height:100%"
>
<xc-meta
class="backgroundColor"
style="height: 100%"
/>
</div> </div>
<div <div v-else-if="tab._nodes.type === 'roles'" style="height: 100%">
v-else-if="tab._nodes.type === 'roles'"
style="height:100%"
>
<auth-tab <auth-tab
v-if="_isUIAllowed('team-auth')" v-if="_isUIAllowed('team-auth')"
class="backgroundColor" class="backgroundColor"
@ -183,10 +201,7 @@
style="height: 100%" style="height: 100%"
/> />
</div> </div>
<div <div v-else-if="tab._nodes.type === 'acl'" style="height: 100%">
v-else-if="tab._nodes.type === 'acl'"
style="height:100%"
>
<global-acl <global-acl
class="backgroundColor" class="backgroundColor"
:nodes="tab._nodes" :nodes="tab._nodes"
@ -195,7 +210,7 @@
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'projectSettings'" v-else-if="tab._nodes.type === 'projectSettings'"
style="height:100%" style="height: 100%"
> >
<project-settings <project-settings
v-if="_isUIAllowed('settings')" v-if="_isUIAllowed('settings')"
@ -206,7 +221,7 @@
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'disableOrEnableModel'" v-else-if="tab._nodes.type === 'disableOrEnableModel'"
style="height:100%" style="height: 100%"
> >
<disable-or-enable-models <disable-or-enable-models
v-if="_isUIAllowed('project-metadata')" v-if="_isUIAllowed('project-metadata')"
@ -215,28 +230,19 @@
style="height: 100%" style="height: 100%"
/> />
</div> </div>
<div <div v-else-if="tab._nodes.type === 'cronJobs'" style="height: 100%">
v-else-if="tab._nodes.type === 'cronJobs'"
style="height:100%"
>
<cron-jobs :nodes="tab._nodes" style="height: 100%" /> <cron-jobs :nodes="tab._nodes" style="height: 100%" />
</div> </div>
<div <div
v-else-if="tab._nodes.type === 'projectInfo'" v-else-if="tab._nodes.type === 'projectInfo'"
style="height:100%" style="height: 100%"
> >
<xc-info :nodes="tab._nodes" class="h-100" /> <xc-info :nodes="tab._nodes" class="h-100" />
</div> </div>
<div <div v-else-if="tab._nodes.type === 'appStore'" style="height: 100%">
v-else-if="tab._nodes.type === 'appStore'" <app-store :nodes="tab._nodes" class="backgroundColor h-100" />
style="height:100%"
>
<app-store
:nodes="tab._nodes"
class="backgroundColor h-100"
/>
</div> </div>
<div v-else style="height:100%"> <div v-else style="height: 100%">
<h1>{{ tab.name }}</h1> <h1>{{ tab.name }}</h1>
<h1>{{ tab._nodes }}</h1> <h1>{{ tab._nodes }}</h1>
</div> </div>
@ -249,7 +255,7 @@
v-if="_isUIAllowed('addTable')" v-if="_isUIAllowed('addTable')"
:tooltip="$t('tooltip.addTable')" :tooltip="$t('tooltip.addTable')"
icon-class="add-btn" icon-class="add-btn"
:color="[ 'white','grey lighten-2']" :color="['white', 'grey lighten-2']"
@click="dialogCreateTableShowMethod" @click="dialogCreateTableShowMethod"
> >
mdi-plus-box mdi-plus-box
@ -260,7 +266,10 @@
<dlg-table-create <dlg-table-create
v-if="dialogCreateTableShow" v-if="dialogCreateTableShow"
v-model="dialogCreateTableShow" v-model="dialogCreateTableShow"
@create="$emit('tableCreate',$event); dialogCreateTableShow =false; teleTblCreate()" @create="
$emit('tableCreate', $event);
dialogCreateTableShow = false;
"
/> />
<!-- <screensaver v-if="showScreensaver && !($store.state.project.projectInfo && $store.state.project.projectInfo.ncMin)" class="screensaver" />--> <!-- <screensaver v-if="showScreensaver && !($store.state.project.projectInfo && $store.state.project.projectInfo.ncMin)" class="screensaver" />-->
@ -268,33 +277,33 @@
</template> </template>
<script> <script>
import { mapGetters, mapMutations } from 'vuex' import { mapGetters, mapMutations } from "vuex";
import treeViewIcons from '../helpers/treeViewIcons' import treeViewIcons from "../helpers/treeViewIcons";
import TableView from './project/table' import TableView from "./project/table";
import FunctionTab from './project/function' import FunctionTab from "./project/function";
import ProcedureTab from './project/procedure' import ProcedureTab from "./project/procedure";
import SequenceTab from './project/sequence' import SequenceTab from "./project/sequence";
import SeedTab from './project/seed' import SeedTab from "./project/seed";
import SqlClientTab from './project/sqlClient' import SqlClientTab from "./project/sqlClient";
import ApisTab from './project/apis' import ApisTab from "./project/apis";
import ApiClientTab from './project/apiClientOld' import ApiClientTab from "./project/apiClientOld";
import sqlLogAndOutput from './project/sqlLogAndOutput' import sqlLogAndOutput from "./project/sqlLogAndOutput";
import graphqlClient from './project/graphqlClient' import graphqlClient from "./project/graphqlClient";
import xTerm from './xTerm' import xTerm from "./xTerm";
import ApiClientSwaggerTab from './project/apiClientSwagger' import ApiClientSwaggerTab from "./project/apiClientSwagger";
import XcMeta from './project/settings/xcMeta' import XcMeta from "./project/settings/xcMeta";
import XcInfo from './project/xcInfo' import XcInfo from "./project/xcInfo";
import SwaggerClient from '@/components/project/swaggerClient' import SwaggerClient from "@/components/project/swaggerClient";
import DlgTableCreate from '@/components/utils/dlgTableCreate' import DlgTableCreate from "@/components/utils/dlgTableCreate";
import AppStore from '@/components/project/appStore' import AppStore from "@/components/project/appStore";
import AuthTab from '@/components/authTab' import AuthTab from "@/components/authTab";
import CronJobs from '@/components/project/cronJobs' import CronJobs from "@/components/project/cronJobs";
import DisableOrEnableModels from '@/components/project/projectMetadata/disableOrEnableModels' import DisableOrEnableModels from "@/components/project/projectMetadata/disableOrEnableModels";
import ProjectSettings from '@/components/project/projectSettings' import ProjectSettings from "@/components/project/projectSettings";
import GrpcClient from '@/components/project/grpcClient' import GrpcClient from "@/components/project/grpcClient";
import GlobalAcl from '@/components/globalAcl' import GlobalAcl from "@/components/globalAcl";
import AuditTab from '~/components/project/auditTab' import AuditTab from "~/components/project/auditTab";
export default { export default {
components: { components: {
@ -324,148 +333,148 @@ export default {
SequenceTab, SequenceTab,
sqlLogAndOutput, sqlLogAndOutput,
xTerm, xTerm,
graphqlClient graphqlClient,
}, },
data() { data() {
return { return {
dialogCreateTableShow: false, dialogCreateTableShow: false,
test: '', test: "",
treeViewIcons, treeViewIcons,
hideLogWindows: false, hideLogWindows: false,
showScreensaver: false showScreensaver: false,
} };
}, },
methods: { methods: {
dialogCreateTableShowMethod() { dialogCreateTableShowMethod() {
this.dialogCreateTableShow = true this.dialogCreateTableShow = true;
this.$tele.emit('table:create:trigger:mdi-plus-box') this.$e("c:table:create:navbar");
},
teleTblCreate() {
this.$tele.emit('table:create:submit')
}, },
checkInactiveState() { checkInactiveState() {
let position = 0 let position = 0;
let idleTime = 0 let idleTime = 0;
// Increment the idle time counter every minute. // Increment the idle time counter every minute.
let idleInterval = setInterval(timerIncrement, 1000) let idleInterval = setInterval(timerIncrement, 1000);
const self = this const self = this;
// Zero the idle timer on mouse movement. // Zero the idle timer on mouse movement.
document.addEventListener('mousemove', (e) => { document.addEventListener("mousemove", (e) => {
self.showScreensaver = false self.showScreensaver = false;
idleTime = 0 idleTime = 0;
clearInterval(idleInterval) clearInterval(idleInterval);
idleInterval = setInterval(timerIncrement, 1000) idleInterval = setInterval(timerIncrement, 1000);
}) });
document.addEventListener('keypress', (e) => { document.addEventListener("keypress", (e) => {
self.showScreensaver = false self.showScreensaver = false;
idleTime = 0 idleTime = 0;
clearInterval(idleInterval) clearInterval(idleInterval);
idleInterval = setInterval(timerIncrement, 1000) idleInterval = setInterval(timerIncrement, 1000);
}) });
function timerIncrement() { function timerIncrement() {
idleTime = idleTime + 1 idleTime = idleTime + 1;
if (idleTime > 120) { if (idleTime > 120) {
const title = document.title const title = document.title;
function scrolltitle() { function scrolltitle() {
document.title = title + Array(position).fill(' .').join('') document.title = title + Array(position).fill(" .").join("");
position = ++position % 3 position = ++position % 3;
if (self.showScreensaver) { if (self.showScreensaver) {
window.setTimeout(scrolltitle, 400) window.setTimeout(scrolltitle, 400);
} else { } else {
document.title = title document.title = title;
} }
} }
self.showScreensaver = self.$store.state.windows.screensaver self.showScreensaver = self.$store.state.windows.screensaver;
scrolltitle() scrolltitle();
clearInterval(idleInterval) clearInterval(idleInterval);
} }
} }
}, },
async handleKeyDown(event) { async handleKeyDown(event) {
const activeTabEleKey = `tabs${this.activeTab}` const activeTabEleKey = `tabs${this.activeTab}`;
let isHandled = false let isHandled = false;
if (this.$refs[activeTabEleKey] && if (
this.$refs[activeTabEleKey] &&
this.$refs[activeTabEleKey][0] && this.$refs[activeTabEleKey][0] &&
this.$refs[activeTabEleKey][0].handleKeyDown) { this.$refs[activeTabEleKey][0].handleKeyDown
isHandled = await this.$refs[activeTabEleKey][0].handleKeyDown(event) ) {
isHandled = await this.$refs[activeTabEleKey][0].handleKeyDown(event);
} }
if (!isHandled) { if (!isHandled) {
switch ([this._isMac ? event.metaKey : event.ctrlKey, event.key].join('_')) { switch (
case 'true_w' : [this._isMac ? event.metaKey : event.ctrlKey, event.key].join("_")
this.removeTab(this.activeTab) ) {
event.preventDefault() case "true_w":
event.stopPropagation() this.removeTab(this.activeTab);
break event.preventDefault();
event.stopPropagation();
break;
} }
} }
}, },
...mapMutations({ ...mapMutations({
setActiveTab: 'tabs/active', setActiveTab: "tabs/active",
removeTab: 'tabs/remove', removeTab: "tabs/remove",
updateActiveTabx: 'tabs/activeTabCtx' updateActiveTabx: "tabs/activeTabCtx",
}), }),
tabActivated(tab) { tabActivated(tab) {},
}
}, },
computed: { computed: {
...mapGetters({ tabs: 'tabs/list', activeTabCtx: 'tabs/activeTabCtx' }), ...mapGetters({ tabs: "tabs/list", activeTabCtx: "tabs/activeTabCtx" }),
pid() { pid() {
return this.$route.params.project_id return this.$route.params.project_id;
}, },
activeTab: { activeTab: {
set(tab) { set(tab) {
if (!tab) { if (!tab) {
return this.$router.push({ return this.$router.push({
query: {} query: {},
}) });
} }
const [type, dbalias, name] = tab.split('||') const [type, dbalias, name] = tab.split("||");
this.$router.push({ this.$router.push({
query: { query: {
...this.$route.query, ...this.$route.query,
type, type,
dbalias, dbalias,
name name,
} },
}) });
}, },
get() { get() {
return [this.$route.query.type, this.$route.query.dbalias, this.$route.query.name].join('||') return [
} this.$route.query.type,
} this.$route.query.dbalias,
this.$route.query.name,
].join("||");
},
},
}, },
beforeCreated() { beforeCreated() {},
}, watch: {},
watch: {
},
created() { created() {
document.addEventListener('keydown', this.handleKeyDown) document.addEventListener("keydown", this.handleKeyDown);
/** /**
* Listening for tab change so that we can hide/show projectlogs based on tab * Listening for tab change so that we can hide/show projectlogs based on tab
*/ */
}, },
mounted() { mounted() {},
}, beforeDestroy() {},
beforeDestroy() {
},
destroyed() { destroyed() {
document.removeEventListener('keydown', this.handleKeyDown) document.removeEventListener("keydown", this.handleKeyDown);
}, },
directives: {}, directives: {},
validate({ params }) { validate({ params }) {
return true return true;
}, },
head() { head() {
return {} return {};
}, },
props: {} props: {},
} };
</script> </script>
<style scoped> <style scoped>
@ -520,11 +529,11 @@ export default {
.powered-by .powered-by-close { .powered-by .powered-by-close {
opacity: 0; opacity: 0;
transition: .4s opacity; transition: 0.4s opacity;
} }
.powered-by a { .powered-by a {
transition: .1s font-weight; transition: 0.1s font-weight;
} }
.powered-by:hover .powered-by-close { .powered-by:hover .powered-by-close {
@ -543,10 +552,9 @@ export default {
position: absolute; position: absolute;
} }
/deep/ .project-tab:first-of-type{ /deep/ .project-tab:first-of-type {
margin-left: 0 !important; margin-left: 0 !important;
} }
</style> </style>
<!-- <!--
/** /**

8
packages/nc-gui/components/settings/settingsModal.vue

@ -34,7 +34,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
v-t="['settings:team-auth']" v-t="['c:settings:team-auth']"
value="roles" value="roles"
dense dense
class="body-2 nc-settings-teamauth" class="body-2 nc-settings-teamauth"
@ -61,7 +61,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
v-t="['settings:appstore']" v-t="['c:settings:appstore']"
dense dense
class="body-2 nc-settings-appstore" class="body-2 nc-settings-appstore"
value="appStore" value="appStore"
@ -87,7 +87,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
v-t="['settings:proj-metadata']" v-t="['c:settings:proj-metadata']"
dense dense
class="body-2 nc-settings-projmeta" class="body-2 nc-settings-projmeta"
value="disableOrEnableModel" value="disableOrEnableModel"
@ -113,7 +113,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-list-item <v-list-item
v-t="['settings:audit']" v-t="['c:settings:audit']"
dense dense
class="body-2 nc-settings-audit" class="body-2 nc-settings-audit"
value="audit" value="audit"

1384
packages/nc-gui/components/templates/editor.vue

File diff suppressed because it is too large Load Diff

136
packages/nc-gui/components/utils/language.vue

@ -1,15 +1,13 @@
<template> <template>
<div> <div>
<v-menu top offset-y> <v-menu top offset-y>
<template #activator="{on}"> <template #activator="{ on }">
<v-icon size="20" class="ml-2 nc-menu-translate" v-on="on"> <v-icon size="20" class="ml-2 nc-menu-translate" v-on="on">
mdi-translate mdi-translate
</v-icon> </v-icon>
</template> </template>
<v-list dense class="nc-language-list"> <v-list dense class="nc-language-list">
<v-list-item-group <v-list-item-group v-model="language">
v-model="language"
>
<v-list-item <v-list-item
v-for="lan in languages" v-for="lan in languages"
:key="lan.value" :key="lan.value"
@ -25,9 +23,13 @@
</v-list-item-group> </v-list-item-group>
<v-divider /> <v-divider />
<v-list-item> <v-list-item>
<a href="https://github.com/nocodb/nocodb/tree/master/packages/nc-gui/lang" target="_blank" class="caption"> <a
href="https://github.com/nocodb/nocodb/tree/master/packages/nc-gui/lang"
target="_blank"
class="caption"
>
<!--Help translate--> <!--Help translate-->
{{ $t('activity.translate') }} {{ $t("activity.translate") }}
</a> </a>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -37,101 +39,103 @@
<script> <script>
export default { export default {
name: 'Language', name: "Language",
data: () => ({ data: () => ({
labels: { labels: {
de: 'Deutsch', de: "Deutsch",
en: 'English', en: "English",
es: 'Español', es: "Español",
fa: 'فارسی', fa: "فارسی",
fr: 'Français', fr: "Français",
id: 'Bahasa Indonesia', id: "Bahasa Indonesia",
ja: '日本語', ja: "日本語",
it_IT: 'Italiano', it_IT: "Italiano",
ko: '한국인', ko: "한국인",
lv: 'Latviešu', lv: "Latviešu",
nl: 'Nederlandse', nl: "Nederlandse",
ru: 'Pусский', ru: "Pусский",
zh_CN: '大陆简体', zh_CN: "大陆简体",
zh_HK: '香港繁體', zh_HK: "香港繁體",
zh_TW: '臺灣正體', zh_TW: "臺灣正體",
sv: 'Svenska', sv: "Svenska",
tr: 'Turkish', tr: "Turkish",
da: 'Dansk', da: "Dansk",
vi: 'Tiếng Việt', vi: "Tiếng Việt",
no: 'Norsk', no: "Norsk",
iw: ִברִית', iw: ִברִית",
fi: 'Suomalainen', fi: "Suomalainen",
uk: 'Українська', uk: "Українська",
hr: 'Hrvatski', hr: "Hrvatski",
th: 'ไทย', th: "ไทย",
sl: 'Slovenščina', sl: "Slovenščina",
pt_BR: 'Português (Brasil)' pt_BR: "Português (Brasil)",
} },
}), }),
computed: { computed: {
languages() { languages() {
return ((this.$i18n && this.$i18n.availableLocales) || ['en']).sort() return ((this.$i18n && this.$i18n.availableLocales) || ["en"]).sort();
}, },
language: { language: {
get() { get() {
return this.$store.state.windows.language return this.$store.state.windows.language;
}, },
set(val) { set(val) {
this.$store.commit('windows/MutLanguage', val) this.$store.commit("windows/MutLanguage", val);
this.applyDirection() this.applyDirection();
} },
} },
}, },
mounted() { mounted() {
this.applyDirection() this.applyDirection();
}, },
methods: { methods: {
applyDirection() { applyDirection() {
document.body.style.direction = this.isRtlLang() ? 'rtl' : 'ltr' document.body.style.direction = this.isRtlLang() ? "rtl" : "ltr";
}, },
isRtlLang() { isRtlLang() {
return ['fa'].includes(this.language) return ["fa"].includes(this.language);
}, },
changeLan(lan) { changeLan(lan) {
this.language = lan this.language = lan;
const count = 200 const count = 200;
const defaults = { const defaults = {
origin: { y: 0.7 } origin: { y: 0.7 },
} };
function fire(particleRatio, opts) { function fire(particleRatio, opts) {
window.confetti(Object.assign({}, defaults, opts, { window.confetti(
particleCount: Math.floor(count * particleRatio) Object.assign({}, defaults, opts, {
})) particleCount: Math.floor(count * particleRatio),
})
);
} }
fire(0.25, { fire(0.25, {
spread: 26, spread: 26,
startVelocity: 55 startVelocity: 55,
}) });
fire(0.2, { fire(0.2, {
spread: 60 spread: 60,
}) });
fire(0.35, { fire(0.35, {
spread: 100, spread: 100,
decay: 0.91, decay: 0.91,
scalar: 0.8 scalar: 0.8,
}) });
fire(0.1, { fire(0.1, {
spread: 120, spread: 120,
startVelocity: 25, startVelocity: 25,
decay: 0.92, decay: 0.92,
scalar: 1.2 scalar: 1.2,
}) });
fire(0.1, { fire(0.1, {
spread: 120, spread: 120,
startVelocity: 45 startVelocity: 45,
}) });
this.$tele.emit(`toolbar:lang:${lan}`) this.$e("c:navbar:lang", { lang: lan });
} },
} },
} };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -139,7 +143,7 @@ export default {
.nc-language-list { .nc-language-list {
max-height: 90vh; max-height: 90vh;
overflow: auto; overflow: auto;
.v-list-item{ .v-list-item {
min-height: 30px !important; min-height: 30px !important;
} }
} }

1
packages/nc-gui/global.d.ts vendored

@ -6,6 +6,7 @@ declare module "vue/types/options" {
$tele: { $tele: {
emit: (event: string, data?) => void emit: (event: string, data?) => void
}, },
$e: (event: string, data?) => void,
$api: Api<any> $api: Api<any>
} }
} }

552
packages/nc-gui/layouts/default.vue

@ -15,7 +15,7 @@
<v-tooltip bottom> <v-tooltip bottom>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn <v-btn
v-t="['toolbar:home']" v-t="['c:navbar:home']"
to="/projects" to="/projects"
icon icon
class="pa-1 pr-0 brand-icon nc-noco-brand-icon" class="pa-1 pr-0 brand-icon nc-noco-brand-icon"
@ -25,29 +25,36 @@
</v-btn> </v-btn>
</template> </template>
<!-- Home --> <!-- Home -->
{{ $t('general.home') }} {{ $t("general.home") }}
<span <span class="caption font-weight-light pointer"
class="caption font-weight-light pointer" >(v{{
>(v{{ $store.state.project.projectInfo &&
$store.state.project.projectInfo && $store.state.project.projectInfo.version $store.state.project.projectInfo.version
}})</span> }})</span
>
</v-tooltip> </v-tooltip>
<span class="body-1 ml-n1" @click="$router.push('/projects')"> {{ brandName }}</span> <span class="body-1 ml-n1" @click="$router.push('/projects')">
{{ brandName }}</span
>
</v-toolbar-title> </v-toolbar-title>
<!-- <v-toolbar-items />--> <!-- <v-toolbar-items />-->
<!-- loading --> <!-- loading -->
<span v-show="$nuxt.$loading.show" class="caption grey--text ml-3">{{ $t('general.loading') }} <v-icon small color="grey">mdi-spin mdi-loading</v-icon></span> <span v-show="$nuxt.$loading.show" class="caption grey--text ml-3"
>{{ $t("general.loading") }}
<v-icon small color="grey">mdi-spin mdi-loading</v-icon></span
>
<span <span v-shortkey="['ctrl', 'shift', 'd']" @shortkey="openDiscord" />
v-shortkey="[ 'ctrl','shift', 'd']"
@shortkey="openDiscord"
/>
</div> </div>
<div v-if="isDashboard" class="text-capitalize text-center title" style="flex: 1"> <div
{{ $store.getters['project/GtrProjectName'] }} v-if="isDashboard"
class="text-capitalize text-center title"
style="flex: 1"
>
{{ $store.getters["project/GtrProjectName"] }}
</div> </div>
<div style="flex: 1" class="d-flex justify-end"> <div style="flex: 1" class="d-flex justify-end">
@ -68,13 +75,13 @@
mdi-account-supervisor-outline mdi-account-supervisor-outline
</v-icon> </v-icon>
<!-- Share --> <!-- Share -->
{{ $t('activity.share') }} {{ $t("activity.share") }}
</x-btn> </x-btn>
<share-or-invite-modal v-model="shareModal" /> <share-or-invite-modal v-model="shareModal" />
</div> </div>
<span <span
v-shortkey="[ 'ctrl','shift', 'd']" v-shortkey="['ctrl', 'shift', 'd']"
@shortkey="$router.push('/')" @shortkey="$router.push('/')"
/> />
<x-btn <x-btn
@ -84,43 +91,45 @@
tooltip="Enable/Disable Models" tooltip="Enable/Disable Models"
@click="cronTabAdd()" @click="cronTabAdd()"
> >
<v-icon size="20"> <v-icon size="20"> mdi-timetable </v-icon> &nbsp; Crons
mdi-timetable
</v-icon> &nbsp;
Crons
</x-btn> </x-btn>
</template> </template>
<template v-else> <template v-else>
<span <span
v-shortkey="[ 'ctrl','shift', 'c']" v-shortkey="['ctrl', 'shift', 'c']"
@shortkey="settingsTabAdd" @shortkey="settingsTabAdd"
/> />
<span <span v-shortkey="['ctrl', 'shift', 'b']" @shortkey="changeTheme" />
v-shortkey="[ 'ctrl','shift', 'b']"
@shortkey="changeTheme"
/>
</template> </template>
<preview-as class="mx-1" /> <preview-as class="mx-1" />
<v-menu <v-menu v-if="isAuthenticated" offset-y>
v-if="isAuthenticated"
offset-y
>
<template #activator="{ on }"> <template #activator="{ on }">
<v-icon v-ge="['Profile','']" text class="font-weight-bold nc-menu-account icon" v-on="on"> <v-icon
v-ge="['Profile', '']"
text
class="font-weight-bold nc-menu-account icon"
v-on="on"
>
<!-- <v-icon></v-icon>--> <!-- <v-icon></v-icon>-->
mdi-dots-vertical mdi-dots-vertical
</v-icon> </v-icon>
</template> </template>
<v-list dense class="nc-user-menu"> <v-list dense class="nc-user-menu">
<template> <template>
<v-list-item v-t="['toolbar:user:email']" v-ge="['Settings','']" dense to="/user/settings"> <v-list-item
v-t="['c:navbar:user:email']"
v-ge="['Settings', '']"
dense
to="/user/settings"
>
<v-list-item-title> <v-list-item-title>
<v-icon small> <v-icon small> mdi-at </v-icon>&nbsp;
mdi-at <span class="font-weight-bold caption">{{
</v-icon>&nbsp; <span class="font-weight-bold caption">{{ userEmail }}</span> userEmail
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
@ -130,73 +139,82 @@
<!-- "Auth token copied to clipboard" --> <!-- "Auth token copied to clipboard" -->
<v-list-item <v-list-item
v-if="isDashboard" v-if="isDashboard"
v-t="['toolbar:user:copy-auth-token']" v-t="['a:navbar:user:copy-auth-token']"
v-clipboard="$store.state.users.token" v-clipboard="$store.state.users.token"
dense dense
@click.stop="$toast.success($t('msg.toast.authToken')).goAway(3000)" @click.stop="
$toast.success($t('msg.toast.authToken')).goAway(3000)
"
> >
<v-list-item-title> <v-list-item-title>
<v-icon key="terminal-dash" small> <v-icon key="terminal-dash" small> mdi-content-copy </v-icon
mdi-content-copy >&nbsp;
</v-icon>&nbsp; <span class="font-weight-regular caption">{{
<span class="font-weight-regular caption">{{ $t('activity.account.authToken') }}</span> $t("activity.account.authToken")
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="swaggerOrGraphiqlUrl" v-if="swaggerOrGraphiqlUrl"
v-t="['toolbar:user:swagger']" v-t="['a:navbar:user:swagger']"
dense dense
@click.stop="openUrl(`${$axios.defaults.baseURL}${swaggerOrGraphiqlUrl}`)" @click.stop="
openUrl(`${$axios.defaults.baseURL}${swaggerOrGraphiqlUrl}`)
"
> >
<v-list-item-title> <v-list-item-title>
<v-icon key="terminal-dash" small> <v-icon key="terminal-dash" small>
{{ isGql ? 'mdi-graphql' : 'mdi-code-json' }} {{ isGql ? "mdi-graphql" : "mdi-code-json" }} </v-icon
</v-icon>&nbsp; >&nbsp;
<span class="font-weight-regular caption"> <span class="font-weight-regular caption">
{{ isGql ? 'GraphQL APIs' : 'Swagger APIs Doc' }}</span> {{ isGql ? "GraphQL APIs" : "Swagger APIs Doc" }}</span
>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-divider /> <v-divider />
<v-list-item <v-list-item
v-if="isDashboard" v-if="isDashboard"
v-t="['toolbar:user:copy-proj-info']" v-t="['c:navbar:user:copy-proj-info']"
v-ge="['Sign Out','']" v-ge="['Sign Out', '']"
dense dense
@click="copyProjectInfo" @click="copyProjectInfo"
> >
<v-list-item-title> <v-list-item-title>
<v-icon small> <v-icon small> mdi-content-copy </v-icon>&nbsp;
mdi-content-copy <span class="font-weight-regular caption">{{
</v-icon>&nbsp; <span class="font-weight-regular caption">{{ $t('activity.account.projInfo') }}</span> $t("activity.account.projInfo")
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-divider v-if="isDashboard" /> <v-divider v-if="isDashboard" />
<v-list-item <v-list-item
v-if="isDashboard" v-if="isDashboard"
v-t="['toolbar:user:themes']" v-t="['c:navbar:user:themes']"
dense dense
@click.stop="settingsTabAdd" @click.stop="settingsTabAdd"
> >
<v-list-item-title> <v-list-item-title>
<v-icon key="terminal-dash" small> <v-icon key="terminal-dash" small> mdi-palette </v-icon
mdi-palette >&nbsp;
</v-icon>&nbsp; <span class="font-weight-regular caption">{{
<span class="font-weight-regular caption">{{ $t('activity.account.themes') }}</span> $t("activity.account.themes")
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-divider v-if="isDashboard" /> <v-divider v-if="isDashboard" />
<v-list-item <v-list-item
v-t="['toolbar:user:sign-out']" v-t="['a:navbar:user:sign-out']"
v-ge="['Sign Out','']" v-ge="['Sign Out', '']"
dense dense
@click="MtdSignOut" @click="MtdSignOut"
> >
<v-list-item-title> <v-list-item-title>
<v-icon small> <v-icon small> mdi-logout </v-icon>&nbsp;
mdi-logout <span class="font-weight-regular caption">{{
</v-icon>&nbsp; <span class="font-weight-regular caption">{{ $t('general.signOut') }}</span> $t("general.signOut")
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</template> </template>
@ -204,27 +222,40 @@
</v-menu> </v-menu>
<v-menu v-else offset-y open-on-hover> <v-menu v-else offset-y open-on-hover>
<template #activator="{ on }"> <template #activator="{ on }">
<v-btn v-ge="['Profile','']" text class=" font-weight-bold nc-menu-account" v-on="on"> <v-btn
v-ge="['Profile', '']"
text
class="font-weight-bold nc-menu-account"
v-on="on"
>
<!-- Menu--> <!-- Menu-->
<v-icon>mdi-account</v-icon> <v-icon>mdi-account</v-icon>
<v-icon>arrow_drop_down</v-icon> <v-icon>arrow_drop_down</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list dense> <v-list dense>
<v-list-item v-if="!user && !isThisMobile" dense to="/user/authentication/signup"> <v-list-item
v-if="!user && !isThisMobile"
dense
to="/user/authentication/signup"
>
<v-list-item-title> <v-list-item-title>
<v-icon small> <v-icon small> mdi-account-plus-outline </v-icon> &nbsp;
mdi-account-plus-outline <span class="font-weight-regular caption">{{
</v-icon> &nbsp; <span $t("general.signUp")
class="font-weight-regular caption" }}</span>
>{{ $t('general.signUp') }}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item v-if="!user && !isThisMobile" dense to="/user/authentication/signin"> <v-list-item
v-if="!user && !isThisMobile"
dense
to="/user/authentication/signin"
>
<v-list-item-title> <v-list-item-title>
<v-icon small> <v-icon small> mdi-login </v-icon> &nbsp;
mdi-login <span class="font-weight-regular caption">{{
</v-icon> &nbsp; <span class="font-weight-regular caption">{{ $t('general.signIn') }}</span> $t("general.signIn")
}}</span>
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -271,19 +302,19 @@
</template> </template>
<script> <script>
import ReleaseInfo from '@/components/releaseInfo' import { mapGetters, mapActions, mapMutations } from "vuex";
import { mapGetters, mapActions, mapMutations } from 'vuex' import ReleaseInfo from "@/components/releaseInfo";
import 'splitpanes/dist/splitpanes.css' import "splitpanes/dist/splitpanes.css";
import XBtn from '../components/global/xBtn' import XBtn from "../components/global/xBtn";
import dlgUnexpectedError from '../components/utils/dlgUnexpectedError' import dlgUnexpectedError from "../components/utils/dlgUnexpectedError";
import settings from '../components/settings' import settings from "../components/settings";
import { copyTextToClipboard } from '@/helpers/xutils' import { copyTextToClipboard } from "@/helpers/xutils";
import Snackbar from '~/components/snackbar' import Snackbar from "~/components/snackbar";
import Language from '~/components/utils/language' import Language from "~/components/utils/language";
import Loader from '~/components/loader' import Loader from "~/components/loader";
import PreviewAs from '~/components/previewAs' import PreviewAs from "~/components/previewAs";
import ShareOrInviteModal from '~/components/auth/shareOrInviteModal' import ShareOrInviteModal from "~/components/auth/shareOrInviteModal";
import ImportantAnnouncement from '../components/importantAnnouncement.vue' import ImportantAnnouncement from "../components/importantAnnouncement.vue";
export default { export default {
components: { components: {
@ -296,7 +327,7 @@ export default {
Snackbar, Snackbar,
dlgUnexpectedError, dlgUnexpectedError,
settings, settings,
ImportantAnnouncement ImportantAnnouncement,
}, },
data: () => ({ data: () => ({
clickCount: true, clickCount: true,
@ -304,10 +335,10 @@ export default {
swaggerOrGraphiqlUrl: null, swaggerOrGraphiqlUrl: null,
showScreensaver: false, showScreensaver: false,
roleIcon: { roleIcon: {
owner: 'mdi-account-star', owner: "mdi-account-star",
creator: 'mdi-account-hard-hat', creator: "mdi-account-hard-hat",
editor: 'mdi-account-edit', editor: "mdi-account-edit",
viewer: 'mdi-eye-outline' viewer: "mdi-eye-outline",
}, },
showAppStore: false, showAppStore: false,
showChangeEnv: false, showChangeEnv: false,
@ -324,72 +355,72 @@ export default {
drawer: null, drawer: null,
fixed: false, fixed: false,
right: true, right: true,
title: 'Xgene', title: "Xgene",
isHydrated: false, isHydrated: false,
snackbar: false, snackbar: false,
timeout: 10000, timeout: 10000,
rolesList: null, rolesList: null,
shareModal: false shareModal: false,
}), }),
computed: { computed: {
...mapGetters({ ...mapGetters({
logo: 'plugins/brandLogo', logo: "plugins/brandLogo",
brandName: 'plugins/brandName', brandName: "plugins/brandName",
projects: 'project/list', projects: "project/list",
tabs: 'tabs/list', tabs: "tabs/list",
sqldMgr: 'sqlMgr/sqlMgr', sqldMgr: "sqlMgr/sqlMgr",
GetPendingStatus: 'notification/GetPendingStatus', GetPendingStatus: "notification/GetPendingStatus",
isAuthenticated: 'users/GtrIsAuthenticated', isAuthenticated: "users/GtrIsAuthenticated",
isAdmin: 'users/GtrIsAdmin', isAdmin: "users/GtrIsAdmin",
isDocker: 'project/GtrIsDocker', isDocker: "project/GtrIsDocker",
isFirstLoad: 'project/GtrIsFirstLoad', isFirstLoad: "project/GtrIsFirstLoad",
isGql: 'project/GtrProjectIsGraphql', isGql: "project/GtrProjectIsGraphql",
isRest: 'project/GtrProjectIsRest', isRest: "project/GtrProjectIsRest",
isGrpc: 'project/GtrProjectIsGrpc', isGrpc: "project/GtrProjectIsGrpc",
role: 'users/GtrRole', role: "users/GtrRole",
userEmail: 'users/GtrUserEmail' userEmail: "users/GtrUserEmail",
}), }),
user() { user() {
return this.$store.getters['users/GtrUser'] return this.$store.getters["users/GtrUser"];
},
isThisMobile() {
// just an example, could be one specific value if that's all you need
return this.isHydrated ? this.$vuetify.breakpoint.smAndDown : false;
}, },
isThisMobile() { // just an example, could be one specific value if that's all you need
return this.isHydrated ? this.$vuetify.breakpoint.smAndDown : false
}
}, },
watch: { watch: {
'$route.path'(path, oldPath) { "$route.path"(path, oldPath) {
try { try {
if (oldPath === path) { if (oldPath === path) {
return return;
} }
const recaptcha = this.$recaptchaInstance const recaptcha = this.$recaptchaInstance;
if (path.startsWith('/user/')) { if (path.startsWith("/user/")) {
recaptcha.showBadge() recaptcha.showBadge();
} else { } else {
recaptcha.hideBadge() recaptcha.hideBadge();
} }
} catch (e) { } catch (e) {}
}
}, },
'$route.params.project_id'(newId, oldId) { "$route.params.project_id"(newId, oldId) {
if (newId && newId !== oldId) { if (newId && newId !== oldId) {
this.loadProjectInfo() this.loadProjectInfo();
} }
if (!newId) { if (!newId) {
this.swaggerOrGraphiqlUrl = null this.swaggerOrGraphiqlUrl = null;
} }
} },
}, },
mounted() { mounted() {
this.selectedEnv = this.$store.getters['project/GtrActiveEnv'] this.selectedEnv = this.$store.getters["project/GtrActiveEnv"];
this.loadProjectInfo() this.loadProjectInfo();
}, },
methods: { methods: {
...mapActions({ changeActiveTab: 'tabs/changeActiveTab' }), ...mapActions({ changeActiveTab: "tabs/changeActiveTab" }),
...mapMutations({ ...mapMutations({
toggleLogWindow: 'windows/MutToggleLogWindow', toggleLogWindow: "windows/MutToggleLogWindow",
toggleOutputWindow: 'windows/MutToggleOutputWindow', toggleOutputWindow: "windows/MutToggleOutputWindow",
toggleTreeviewWindow: 'windows/MutToggleTreeviewWindow' toggleTreeviewWindow: "windows/MutToggleTreeviewWindow",
}), }),
async loadProjectInfo() { async loadProjectInfo() {
// if (this.$route.params.project_id) { // if (this.$route.params.project_id) {
@ -406,23 +437,23 @@ export default {
// } // }
}, },
setPreviewUSer(previewAs) { setPreviewUSer(previewAs) {
this.previewAs = previewAs this.previewAs = previewAs;
window.location.reload() window.location.reload();
}, },
showAppStoreIcon() { showAppStoreIcon() {
this.showAppStore = true this.showAppStore = true;
this.$toast.info('Apps unlocked').goAway(5000) this.$toast.info("Apps unlocked").goAway(5000);
}, },
isProjectInfoLoaded() { isProjectInfoLoaded() {
return this.$store.state.project.projectInfo !== null return this.$store.state.project.projectInfo !== null;
}, },
githubClickHandler(e) { githubClickHandler(e) {
// e.preventDefault(); // e.preventDefault();
// shell.openExternal(e.path.find(e => e.href).href); // shell.openExternal(e.path.find(e => e.href).href);
}, },
openUrl(url) { openUrl(url) {
window.open(url, '_blank') window.open(url, "_blank");
}, },
openPricingPage() { openPricingPage() {
// shell.openExternal(process.env.serverUrl + '/pricing') // shell.openExternal(process.env.serverUrl + '/pricing')
@ -437,240 +468,253 @@ export default {
// shell.openExternal('https://github.com/NocoDB/NocoDB') // shell.openExternal('https://github.com/NocoDB/NocoDB')
}, },
dialogDebugCancel() { dialogDebugCancel() {
this.dialogDebug = false this.dialogDebug = false;
}, },
dialogDebugShow() { dialogDebugShow() {
this.dialogDebug = true this.dialogDebug = true;
}, },
errorDialogCancel() { errorDialogCancel() {
this.dialogErrorShow = false this.dialogErrorShow = false;
}, },
errorDialogReport() { errorDialogReport() {
this.dialogErrorShow = false this.dialogErrorShow = false;
}, },
loadChat() { loadChat() {
if (!window.Tawk_API) { if (!window.Tawk_API) {
const s1 = document.createElement('script') const s1 = document.createElement("script");
const s0 = document.getElementsByTagName('script')[0] const s0 = document.getElementsByTagName("script")[0];
s1.async = true s1.async = true;
s1.src = 'https://embed.tawk.to/5d81b8de9f6b7a4457e23ba7/default' s1.src = "https://embed.tawk.to/5d81b8de9f6b7a4457e23ba7/default";
s1.charset = 'UTF-8' s1.charset = "UTF-8";
s1.setAttribute('crossorigin', '*') s1.setAttribute("crossorigin", "*");
s0.parentNode.insertBefore(s1, s0) s0.parentNode.insertBefore(s1, s0);
setTimeout(() => window.Tawk_API && window.Tawk_API.maximize(), 2000) setTimeout(() => window.Tawk_API && window.Tawk_API.maximize(), 2000);
} else { } else {
window.Tawk_API.maximize() window.Tawk_API.maximize();
} }
}, },
handleMigrationsMenuClick(item, closeMenu = true, sqlEditor = false) { handleMigrationsMenuClick(item, closeMenu = true, sqlEditor = false) {},
},
apiClientTabAdd() { apiClientTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) { // if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex(el => el.key === 'apiClientDir') const tabIndex = this.tabs.findIndex((el) => el.key === "apiClientDir");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'API Client', key: 'apiClientDir' } const item = { name: "API Client", key: "apiClientDir" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'apiClientDir' item._nodes.type = "apiClientDir";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
apiClientSwaggerTabAdd() { apiClientSwaggerTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) { // if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex(el => el.key === 'apiClientSwaggerDir') const tabIndex = this.tabs.findIndex(
(el) => el.key === "apiClientSwaggerDir"
);
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'API Client', key: 'apiClientSwaggerDir' } const item = { name: "API Client", key: "apiClientSwaggerDir" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'apiClientSwaggerDir' item._nodes.type = "apiClientSwaggerDir";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
projectInfoTabAdd() { projectInfoTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'projectInfo') const tabIndex = this.tabs.findIndex((el) => el.key === "projectInfo");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Info', key: 'projectInfo' } const item = { name: "Info", key: "projectInfo" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'projectInfo' item._nodes.type = "projectInfo";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
xcMetaTabAdd() { xcMetaTabAdd() {
// if (this.$route.path.indexOf('dashboard') > -1) { // if (this.$route.path.indexOf('dashboard') > -1) {
const tabIndex = this.tabs.findIndex(el => el.key === 'meta') const tabIndex = this.tabs.findIndex((el) => el.key === "meta");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Meta', key: 'meta' } const item = { name: "Meta", key: "meta" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'meta' item._nodes.type = "meta";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
apiClientSwaggerOpen() { apiClientSwaggerOpen() {
this.$router.push('/apiClient') this.$router.push("/apiClient");
}, },
graphqlClientTabAdd() { graphqlClientTabAdd() {
window.open(this.swaggerOrGraphiqlUrl, '_blank') window.open(this.swaggerOrGraphiqlUrl, "_blank");
}, },
swaggerClientTabAdd() { swaggerClientTabAdd() {
window.open(this.swaggerOrGraphiqlUrl, '_blank') window.open(this.swaggerOrGraphiqlUrl, "_blank");
}, },
grpcTabAdd() { grpcTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'grpcClient') const tabIndex = this.tabs.findIndex((el) => el.key === "grpcClient");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'gRPC Client', key: 'grpcClient' } const item = { name: "gRPC Client", key: "grpcClient" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'grpcClient' item._nodes.type = "grpcClient";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
rolesTabAdd() { rolesTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'roles') const tabIndex = this.tabs.findIndex((el) => el.key === "roles");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Team & Auth ', key: 'roles' } const item = { name: "Team & Auth ", key: "roles" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'roles' item._nodes.type = "roles";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
setTimeout(() => { setTimeout(() => {
this.$eventBus.$emit('show-add-user') this.$eventBus.$emit("show-add-user");
}, 200) }, 200);
}, },
settingsTabAdd() { settingsTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'projectSettings') const tabIndex = this.tabs.findIndex(
(el) => el.key === "projectSettings"
);
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Themes', key: 'projectSettings' } const item = { name: "Themes", key: "projectSettings" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'projectSettings' item._nodes.type = "projectSettings";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
aclTabAdd() { aclTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'acl') const tabIndex = this.tabs.findIndex((el) => el.key === "acl");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'ACL', key: 'acl' } const item = { name: "ACL", key: "acl" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'acl' item._nodes.type = "acl";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
disableOrEnableModelTabAdd() { disableOrEnableModelTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'disableOrEnableModel') const tabIndex = this.tabs.findIndex(
(el) => el.key === "disableOrEnableModel"
);
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Meta Management', key: 'disableOrEnableModel' } const item = { name: "Meta Management", key: "disableOrEnableModel" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'disableOrEnableModel' item._nodes.type = "disableOrEnableModel";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
cronTabAdd() { cronTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'cronJobs') const tabIndex = this.tabs.findIndex((el) => el.key === "cronJobs");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'Cron Jobs', key: 'cronJobs' } const item = { name: "Cron Jobs", key: "cronJobs" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'cronJobs' item._nodes.type = "cronJobs";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
appsTabAdd() { appsTabAdd() {
const tabIndex = this.tabs.findIndex(el => el.key === 'appStore') const tabIndex = this.tabs.findIndex((el) => el.key === "appStore");
if (tabIndex !== -1) { if (tabIndex !== -1) {
this.changeActiveTab(tabIndex) this.changeActiveTab(tabIndex);
} else { } else {
const item = { name: 'App Store', key: 'appStore' } const item = { name: "App Store", key: "appStore" };
item._nodes = { env: '_noco' } item._nodes = { env: "_noco" };
item._nodes.type = 'appStore' item._nodes.type = "appStore";
this.$store.dispatch('tabs/ActAddTab', item) this.$store.dispatch("tabs/ActAddTab", item);
} }
}, },
async codeGenerateMvc() { async codeGenerateMvc() {
try { try {
await this.sqlMgr.projectGenerateBackend({ await this.sqlMgr.projectGenerateBackend({
env: '_noco' env: "_noco",
}) });
this.$toast.success('Yay, REST APIs with MVC generated').goAway(4000) this.$toast.success("Yay, REST APIs with MVC generated").goAway(4000);
} catch (e) { } catch (e) {
this.$toast.error('Error generating REST APIs code :' + e).goAway(4000) this.$toast.error("Error generating REST APIs code :" + e).goAway(4000);
throw e throw e;
} }
}, },
cookieStatus(status) { cookieStatus(status) {
this.status = status this.status = status;
}, },
cookieClickedAccept() { cookieClickedAccept() {
this.status = 'accept' this.status = "accept";
}, },
cookieClickedDecline() { cookieClickedDecline() {
this.status = 'decline' this.status = "decline";
// localStorage.removeItem('vue-cookie-accept-decline') // localStorage.removeItem('vue-cookie-accept-decline')
}, },
removeCookie() { removeCookie() {
// console.log('Cookie removed') // console.log('Cookie removed')
localStorage.removeItem('vue-cookie-accept-decline') localStorage.removeItem("vue-cookie-accept-decline");
this.status = 'Cookie removed, refresh the page.' this.status = "Cookie removed, refresh the page.";
}, },
MtdContactUs() { MtdContactUs() {
this.snackbar = true this.snackbar = true;
}, },
MtdHiring() { MtdHiring() {
this.$router.push('/info/hiring') this.$router.push("/info/hiring");
}, },
MtdFaq() { MtdFaq() {
this.$router.push('/info/faq') this.$router.push("/info/faq");
}, },
MtdTos() { MtdTos() {
this.$router.push('/info/tos') this.$router.push("/info/tos");
}, },
async MtdSignOut() { async MtdSignOut() {
await this.$store.dispatch('users/ActSignOut') await this.$store.dispatch("users/ActSignOut");
this.$router.push('/user/authentication/signin') this.$router.push("/user/authentication/signin");
}, },
MtdToggleDrawer() { MtdToggleDrawer() {
if (!this.$store.getters['users/GtrUser']) { if (!this.$store.getters["users/GtrUser"]) {
this.drawer = false this.drawer = false;
} else { } else {
this.drawer = !this.drawer this.drawer = !this.drawer;
} }
// console.log('Toggling drawer', this.drawer); // console.log('Toggling drawer', this.drawer);
}, },
changeTheme() { changeTheme() {
this.$store.dispatch('windows/ActToggleDarkMode', !this.$store.state.windows.darkTheme) this.$store.dispatch(
this.$tele.emit('toolbar:theme') "windows/ActToggleDarkMode",
!this.$store.state.windows.darkTheme
);
this.$e("c:navbar:theme");
}, },
async copyProjectInfo() { async copyProjectInfo() {
try { try {
const data = (await this.$api.project.metaGet(this.$store.state.project.projectId)) const data = await this.$api.project.metaGet(
copyTextToClipboard(Object.entries(data).map(([k, v]) => `${k}: **${v}**`).join('\n')) this.$store.state.project.projectId
this.$toast.info('Copied project info to clipboard').goAway(3000) );
copyTextToClipboard(
Object.entries(data)
.map(([k, v]) => `${k}: **${v}**`)
.join("\n")
);
this.$toast.info("Copied project info to clipboard").goAway(3000);
} catch (e) { } catch (e) {
console.log(e) console.log(e);
this.$toast.error(e.message).goAway(3000) this.$toast.error(e.message).goAway(3000);
} }
} },
} },
};
}
</script> </script>
<style scoped> <style scoped>
a { a {
@ -718,10 +762,10 @@ a {
border-radius: 50%; border-radius: 50%;
} }
/deep/ .nc-user-menu .v-list-item--dense, /deep/ .nc-user-menu .v-list--dense .v-list-item { /deep/ .nc-user-menu .v-list-item--dense,
min-height: 35px /deep/ .nc-user-menu .v-list--dense .v-list-item {
min-height: 35px;
} }
</style> </style>
<!-- <!--

2
packages/nc-gui/pages/project/xcdb.vue

@ -69,7 +69,7 @@
</div> </div>
<div class="text-center"> <div class="text-center">
<v-btn <v-btn
v-t="['project:create:xcdb:submit']" v-t="['a:project:create:xcdb']"
class="mt-3" class="mt-3"
large large
:loading="loading" :loading="loading"

716
packages/nc-gui/pages/projects/index.vue

File diff suppressed because it is too large Load Diff

158
packages/nc-gui/pages/projects/list.vue

@ -1,14 +1,6 @@
<template> <template>
<div <div class="d-flex" style="height: calc(100vh - 52px)">
class="d-flex " <v-navigation-drawer left permanent width="max(300px,20%)" class="h-100">
style="height:calc(100vh - 52px)"
>
<v-navigation-drawer
left
permanent
width="max(300px,20%)"
class="h-100"
>
<div class="d-flex flex-column h-100"> <div class="d-flex flex-column h-100">
<div class="advance-menu flex-grow-1 pt-8"> <div class="advance-menu flex-grow-1 pt-8">
<v-list <v-list
@ -31,7 +23,10 @@
</v-icon> </v-icon>
<span <span
class="font-weight-medium ml-3" class="font-weight-medium ml-3"
:class="{'textColor--text text--lighten-2':item.title!==activePage}" :class="{
'textColor--text text--lighten-2':
item.title !== activePage,
}"
> >
{{ item.title }} {{ item.title }}
</span> </span>
@ -41,7 +36,7 @@
</v-list> </v-list>
</div> </div>
<v-divider /> <v-divider />
<extras class="pl-6 " /> <extras class="pl-6" />
<div class="sponsor ml-2 mb-2 mr-2"> <div class="sponsor ml-2 mb-2 mr-2">
<sponsor-mini nav /> <sponsor-mini nav />
</div> </div>
@ -49,7 +44,9 @@
</v-navigation-drawer> </v-navigation-drawer>
<v-container class="flex-grow-1 py-9 px-15 h-100" style="overflow-y: auto"> <v-container class="flex-grow-1 py-9 px-15 h-100" style="overflow-y: auto">
<div class="d-flex"> <div class="d-flex">
<h2 class="display-1 ml-5 mb-7 font-weight-medium textColor--text text--lighten-2 flex-grow-1"> <h2
class="display-1 ml-5 mb-7 font-weight-medium textColor--text text--lighten-2 flex-grow-1"
>
{{ activePage }} {{ activePage }}
</h2> </h2>
@ -58,35 +55,53 @@
</div> </div>
</div> </div>
<v-divider class="mb-3" /> <v-divider class="mb-3" />
<div v-if="projectList &&projectList.length" class="nc-project-item-container d-flex d-100"> <div
v-if="projectList && projectList.length"
class="nc-project-item-container d-flex d-100"
>
<div <div
v-for="(project,i) in projectList" v-for="(project, i) in projectList"
:key="project.id" :key="project.id"
class=" nc-project-item elevation-0 d-flex align-center justify-center flex-column py-5" class="nc-project-item elevation-0 d-flex align-center justify-center flex-column py-5"
> >
<div <div
class="nc-project-thumbnail pointer text-uppercase d-flex align-center justify-center" class="nc-project-thumbnail pointer text-uppercase d-flex align-center justify-center"
:style="{backgroundColor:getTextColor(i)}" :style="{ backgroundColor: getTextColor(i) }"
@click="openProject(project)" @click="openProject(project)"
> >
{{ project.title.split(' ').map(w => w[0]).slice(0, 2).join('') }} {{
project.title
<v-icon class="nc-project-star-icon" small color="white" v-on="on" @click.stop> .split(" ")
.map((w) => w[0])
.slice(0, 2)
.join("")
}}
<v-icon
class="nc-project-star-icon"
small
color="white"
v-on="on"
@click.stop
>
mdi-star-outline mdi-star-outline
</v-icon> </v-icon>
<v-menu bottom offset-y> <v-menu bottom offset-y>
<template #activator="{on}"> <template #activator="{ on }">
<v-icon class="nc-project-option-menu-icon" color="white" v-on="on" @click.stop> <v-icon
class="nc-project-option-menu-icon"
color="white"
v-on="on"
@click.stop
>
mdi-menu-down mdi-menu-down
</v-icon> </v-icon>
</template> </template>
<v-list dense> <v-list dense>
<v-list-item> <v-list-item>
<v-list-item-title> <v-list-item-title>
<v-icon small color="red"> <v-icon small color="red"> mdi-delete-outline </v-icon>
mdi-delete-outline
</v-icon>
Delete Delete
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
@ -94,30 +109,36 @@
</v-menu> </v-menu>
</div> </div>
<div class="text-center pa-2 nc-project-title body-2 font-weight-medium "> <div
class="text-center pa-2 nc-project-title body-2 font-weight-medium"
>
{{ project.title }} {{ project.title }}
</div> </div>
</div> </div>
<div class="pointer nc-project-item nc-project-item elevation-0 d-flex align-center justify-center flex-column"> <div
class="pointer nc-project-item nc-project-item elevation-0 d-flex align-center justify-center flex-column"
>
<create-new-project-btn> <create-new-project-btn>
<template #default="{on}"> <template #default="{ on }">
<div <div
class="nc-project-thumbnail nc-add-project text-uppercase d-flex align-center justify-center grey lighten-2 " class="nc-project-thumbnail nc-add-project text-uppercase d-flex align-center justify-center grey lighten-2"
v-on="on" v-on="on"
> >
+ +
</div> </div>
</template> </template>
</create-new-project-btn> </create-new-project-btn>
<div class="text-center pa-2 nc-project-title body-2 font-weight-medium "> <div
class="text-center pa-2 nc-project-title body-2 font-weight-medium"
>
Add project Add project
</div> </div>
</div> </div>
</div> </div>
<div <div
v-else v-else
class="px-4 py-10 text-center textColor--text text--lighten-3 caption backgroundColor " class="px-4 py-10 text-center textColor--text text--lighten-3 caption backgroundColor"
> >
Please create a project Please create a project
</div> </div>
@ -126,49 +147,53 @@
</template> </template>
<script> <script>
import colors from "~/mixins/colors";
import colors from '~/mixins/colors' import CreateNewProjectBtn from "~/components/projectList/createNewProjectBtn";
import CreateNewProjectBtn from '~/components/projectList/createNewProjectBtn' import Extras from "~/components/project/spreadsheet/components/extras";
import Extras from '~/components/project/spreadsheet/components/extras' import SponsorMini from "~/components/sponsorMini";
import SponsorMini from '~/components/sponsorMini'
export default { export default {
name: 'List', name: "List",
components: { SponsorMini, Extras, CreateNewProjectBtn }, components: { SponsorMini, Extras, CreateNewProjectBtn },
mixins: [colors], mixins: [colors],
data: () => ({ data: () => ({
projectList: null, projectList: null,
activePage: null, activePage: null,
navDrawerOptions: [{ navDrawerOptions: [
title: 'My NocoDB', {
icon: 'mdi-folder-outline' title: "My NocoDB",
}, { icon: "mdi-folder-outline",
title: 'Shared With Me', },
icon: 'mdi-account-group' {
}, { title: "Shared With Me",
title: 'Recent', icon: "mdi-account-group",
icon: 'mdi-clock-outline' },
}, { {
title: 'Starred', title: "Recent",
icon: 'mdi-star' icon: "mdi-clock-outline",
}] },
{
title: "Starred",
icon: "mdi-star",
},
],
}), }),
mounted() { mounted() {
this.loadProjectList() this.loadProjectList();
}, },
methods: { methods: {
async loadProjectList() { async loadProjectList() {
const { list, pageInfo } = await this.$api.project.list() const { list, pageInfo } = await this.$api.project.list();
this.projectList = list this.projectList = list;
}, },
async openProject(project) { async openProject(project) {
await this.$router.push({ await this.$router.push({
path: `/nc/${project.id}` path: `/nc/${project.id}`,
}) });
this.$tele.emit(`project:open:${this.projects.length}`) this.$e("a:project:open", { count: this.projects.length });
} },
} },
} };
</script> </script>
<style scoped> <style scoped>
@ -193,20 +218,22 @@ export default {
position: relative; position: relative;
} }
.nc-project-option-menu-icon,.nc-project-star-icon { .nc-project-option-menu-icon,
.nc-project-star-icon {
position: absolute; position: absolute;
opacity: 0; opacity: 0;
transition: .3s opacity; transition: 0.3s opacity;
} }
.nc-project-star-icon{ .nc-project-star-icon {
top:8px; top: 8px;
right: 10px; right: 10px;
} }
.nc-project-option-menu-icon{ .nc-project-option-menu-icon {
bottom: 5px; bottom: 5px;
right: 5px; right: 5px;
} }
.nc-project-thumbnail:hover .nc-project-option-menu-icon,.nc-project-thumbnail:hover .nc-project-star-icon{ .nc-project-thumbnail:hover .nc-project-option-menu-icon,
.nc-project-thumbnail:hover .nc-project-star-icon {
opacity: 1; opacity: 1;
} }
.nc-project-title { .nc-project-title {
@ -225,5 +252,4 @@ export default {
.nc-project-thumbnail:hover { .nc-project-thumbnail:hover {
background-image: linear-gradient(#0002, #0002); background-image: linear-gradient(#0002, #0002);
} }
</style> </style>

2
packages/nc-gui/pages/user/authentication/signin.vue

@ -79,7 +79,6 @@
<!-- </v-btn>--> <!-- </v-btn>-->
<v-btn <v-btn
v-t="['login:sign-in']"
v-ge="['Sign In', '']" v-ge="['Sign In', '']"
color="primary" color="primary"
large large
@ -370,6 +369,7 @@ export default {
} else { } else {
this.$router.push('/projects') this.$router.push('/projects')
} }
this.$e('a:auth:sign-in')
}, },
MtdOnSigninGoogle(e) { MtdOnSigninGoogle(e) {

2
packages/nc-gui/pages/user/authentication/signup/index.vue

@ -68,7 +68,6 @@
<!-- </vue-recaptcha>--> <!-- </vue-recaptcha>-->
<v-btn <v-btn
v-t="['login:sign-up']"
v-ge="['Sign Up ','']" v-ge="['Sign Up ','']"
color="primary" color="primary"
class="btn--large" class="btn--large"
@ -379,6 +378,7 @@ export default {
this.$router.push('/projects?toast') this.$router.push('/projects?toast')
} }
this.signUpButtonLoading = false this.signUpButtonLoading = false
this.$e('a:auth:sign-up')
}, },
MtdOnReset() { MtdOnReset() {

8
packages/nc-gui/plugins/tele.js

@ -45,6 +45,7 @@ export default function({
const tele = { const tele = {
emit(evt, data) { emit(evt, data) {
// debugger
if (socket) { if (socket) {
socket.emit('event', { socket.emit('event', {
event: evt, event: evt,
@ -56,6 +57,7 @@ export default function({
} }
inject('tele', tele) inject('tele', tele)
inject('e', tele.emit)
function getListener(binding) { function getListener(binding) {
return function(e) { return function(e) {
@ -76,7 +78,11 @@ export default function({
Vue.directive('t', { Vue.directive('t', {
bind(el, binding, vnode) { bind(el, binding, vnode) {
if (vnode.componentInstance) { if (vnode.componentInstance) {
vnode.componentInstance.$on('click', getListener(binding)) if (vnode.componentInstance.$el) {
vnode.componentInstance.$el.addEventListener('click', getListener(binding))
} else {
vnode.componentInstance.$on('click', getListener(binding))
}
} else { } else {
el.addEventListener('click', getListener(binding)) el.addEventListener('click', getListener(binding))
} }

Loading…
Cancel
Save