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 />
<x-btn
v-ge="['roles','reload']"
v-ge="['roles', 'reload']"
outlined
tooltip="Reload API tokens"
color="primary"
@ -17,11 +17,11 @@
refresh
</v-icon>
<!-- Reload -->
{{ $t('general.reload') }}
{{ $t("general.reload") }}
</x-btn>
<x-btn
v-if="_isUIAllowed('newUser')"
v-ge="['roles','add new']"
v-ge="['roles', 'add new']"
outlined
tooltip="Generate new API token"
color="primary"
@ -33,40 +33,43 @@
mdi-plus
</v-icon>
<!--New Token-->
{{ $t('activity.newToken') }}
{{ $t("activity.newToken") }}
</x-btn>
</v-toolbar>
<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>
<tr class="">
<th class="caption text-center">
<!--Description-->
{{ $t('labels.description') }}
{{ $t("labels.description") }}
</th>
<th class="caption text-center">
<!--Token-->
{{ $t('labels.token') }}
{{ $t("labels.token") }}
</th>
<th class="caption text-center">
<!--Actions-->
{{ $t('labels.action') }}
{{ $t("labels.action") }}
</th>
</tr>
</thead>
<tr v-if="!tokens.length">
<td colspan="3">
<div
class="text-center caption grey--text"
>
<div class="text-center caption grey--text">
No tokens available
</div>
</td>
</tr>
<tr v-for="(token,i) in tokens" :key="i">
<tr v-for="(token, i) in tokens" :key="i">
<td class="caption text-center">
{{ token.description }}
</td>
@ -80,14 +83,19 @@
<x-icon
x-small
icon.class="ml-2"
:tooltip="`${token.show ?'Hide':'Show' } API token`"
@click="$set(token,'show' ,!token.show)"
:tooltip="`${token.show ? 'Hide' : 'Show'} API token`"
@click="$set(token, 'show', !token.show)"
>
{{ token.show ? 'visibility_off' : 'visibility' }}
{{ token.show ? "visibility_off" : "visibility" }}
</x-icon>
<!-- <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
</x-icon>
<x-icon
@ -114,7 +122,7 @@
</v-container>
<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">
<template>Generate Token</template>
</h4>
@ -134,7 +142,7 @@
<v-card-actions class="justify-center">
<x-btn
v-ge="['rows','save']"
v-ge="['rows', 'save']"
tooltip="Generate new api token"
color="primary"
btn.class="mt-5 mb-3 pr-5"
@ -165,21 +173,26 @@ export default {
methods: {
showNewTokenDlg() {
this.newTokenDialog = true
this.$tele.emit('api-mgmt:token:generate:trigger')
this.$e('c:api-token:generate')
},
copyToken(token) {
copyTextToClipboard(token)
this.$toast.info('Copied to clipboard').goAway(1000)
this.$tele.emit('api-mgmt:token:copy')
this.$e('c:api-token:copy')
},
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() {
try {
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.tokenObj = {}
await this.loadApiTokens()
@ -188,11 +201,14 @@ export default {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('api-mgmt:token:generate:submit')
this.$e('a:api-token:generate')
},
async deleteToken(item) {
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.$toast.success('Token deleted successfully').goAway(3000)
await this.loadApiTokens()
@ -201,12 +217,10 @@ export default {
this.$toast.error(e.message).goAway(3000)
}
this.$tele.emit('api-mgmt:token:delete')
this.$e('a:api-token:delete')
}
}
}
</script>
<style scoped>
</style>
<style scoped></style>

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

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

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

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

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

@ -1,32 +1,29 @@
<template>
<div>
<v-icon color="grey" small>
mdi-open-in-new
</v-icon>
<v-icon color="grey" small> mdi-open-in-new </v-icon>
<span class="grey--text caption">
<!-- Shared base link -->
{{ $t('activity.shareBase.link') }}
{{ $t("activity.shareBase.link") }}
</span>
<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">
<span class="nc-url flex-grow-1 caption ">{{ url }}</span>
<span class="nc-url flex-grow-1 caption">{{ url }}</span>
<v-spacer />
<v-divider vertical />
<!-- tooltip="reload" -->
<x-icon
:tooltip="$t('general.reload')"
@click="recreate"
>
<x-icon :tooltip="$t('general.reload')" @click="recreate">
mdi-reload
</x-icon>
<!-- tooltip="copy URL" -->
<x-icon
:tooltip="$t('activity.copyUrl')"
@click="copyUrl"
>
<x-icon :tooltip="$t('activity.copyUrl')" @click="copyUrl">
mdi-content-copy
</x-icon>
@ -51,49 +48,47 @@
<div class="d-flex align-center px-2">
<div>
<v-menu offset-x>
<template #activator="{on}">
<template #activator="{ on }">
<div class="my-2" v-on="on">
<div class="font-weight-bold nc-disable-shared-base">
<span v-if="base.uuid">
<!-- Anyone with the link -->
{{ $t('activity.shareBase.enable') }}
{{ $t("activity.shareBase.enable") }}
</span>
<span v-else>
<!-- Disable shared base -->
{{ $t('activity.shareBase.disable') }}
{{ $t("activity.shareBase.disable") }}
</span>
<v-icon small>
mdi-menu-down-outline
</v-icon>
<v-icon small> mdi-menu-down-outline </v-icon>
</div>
</div>
</template>
<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-icon small class="mr-1">
mdi-link-variant
</v-icon>
<v-icon small class="mr-1"> mdi-link-variant </v-icon>
<span class="caption">
<!-- Anyone with the link -->
{{ $t('activity.shareBase.enable') }}
{{ $t("activity.shareBase.enable") }}
</span>
</v-list-item-title>
</v-list-item>
<v-list-item v-if="base.uuid" dense @click="disableSharedBase">
<v-list-item-title>
<v-icon small class="mr-1">
mdi-link-variant-off
</v-icon>
<v-icon small class="mr-1"> mdi-link-variant-off </v-icon>
<span class="caption">
<!-- Disable shared base -->
{{ $t('activity.shareBase.disable') }}
{{ $t("activity.shareBase.disable") }}
</span>
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<div class=" caption">
<div class="caption">
<template v-if="base.enabled">
<span v-if="base.roles === 'editor'">
Anyone on the internet with this link can edit
@ -101,28 +96,26 @@
</span>
<span v-else-if="base.roles === 'viewer'">
<!-- Anyone on the internet with this link can view -->
{{ $t('msg.info.shareBasePublic') }}
{{ $t("msg.info.shareBasePublic") }}
</span>
</template>
<template v-else>
<!-- Generate publicly shareable readonly base -->
{{ $t('msg.info.shareBasePrivate') }}
{{ $t("msg.info.shareBasePrivate") }}
</template>
</div>
</div>
<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>
<template #activator="{on}">
<template #activator="{ on }">
<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"
>
{{ base.roles || 'Viewer' }}
{{ base.roles || "Viewer" }}
<v-icon small>
mdi-menu-down-outline
</v-icon>
<v-icon small> mdi-menu-down-outline </v-icon>
</div>
</template>
@ -130,13 +123,13 @@
<v-list-item @click="createSharedBase('editor')">
<v-list-item-title>
<!-- Editor -->
{{ $t('objects.roleType.editor') }}
{{ $t("objects.roleType.editor") }}
</v-list-item-title>
</v-list-item>
<v-list-item @click="createSharedBase('viewer')">
<v-list-item-title>
<!-- Viewer -->
{{ $t('objects.roleType.viewer') }}
{{ $t("objects.roleType.viewer") }}
</v-list-item-title>
</v-list-item>
</v-list>
@ -148,79 +141,93 @@
</template>
<script>
import colors from '~/mixins/colors'
import { copyTextToClipboard } from '~/helpers/xutils'
import colors from "~/mixins/colors";
import { copyTextToClipboard } from "~/helpers/xutils";
export default {
name: 'ShareBase',
name: "ShareBase",
mixins: [colors],
data: () => ({
base: {
enable: false
}
enable: false,
},
}),
computed: {
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() {
this.loadSharedBase()
this.loadSharedBase();
},
methods: {
async loadSharedBase() {
try {
// const sharedBase = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// { 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) {
console.log(e)
console.log(e);
}
},
async createSharedBase(roles = 'viewer') {
async createSharedBase(roles = "viewer") {
try {
// 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) {
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() {
try {
await this.$api.project.sharedBaseDisable(this.$store.state.project.projectId)
this.base = {}
await this.$api.project.sharedBaseDisable(
this.$store.state.project.projectId
);
this.base = {};
} 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() {
try {
const sharedBase = (await this.$api.project.sharedBaseCreate(this.$store.state.project.projectId, { roles: this.base.roles || 'viewer' }))
this.base = sharedBase || {}
const sharedBase = await this.$api.project.sharedBaseCreate(
this.$store.state.project.projectId,
{ roles: this.base.roles || "viewer" }
);
this.base = sharedBase || {};
} 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() {
copyTextToClipboard(this.url)
this.$toast.success('Copied shareable base url to clipboard!').goAway(3000)
copyTextToClipboard(this.url);
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() {
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() {
copyTextToClipboard(`<iframe
@ -229,20 +236,19 @@ src="${this.url}?embed"
frameborder="0"
width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"></iframe>`)
this.$toast.success('Copied embeddable html code!').goAway(3000)
this.$tele.emit('shared-base:copy-embed-frame')
}
}
style="background: transparent; border: 1px solid #ddd"></iframe>`);
this.$toast.success("Copied embeddable html code!").goAway(3000);
}
this.$e("c:shared-base:copy-embed-frame");
},
},
};
</script>
<style scoped>
.nc-url-wrapper {
column-gap: 15px;
width: 100%
width: 100%;
}
.nc-url {
@ -259,6 +265,6 @@ style="background: transparent; border: 1px solid #ddd"></iframe>`)
}
/deep/ .nc-url-chip .v-chip__content {
width: 100%
width: 100%;
}
</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>
<div>
<v-menu offset-y>
<template #activator="{on}">
<template #activator="{ on }">
<v-btn
v-show="isDashboard && _isUIAllowed('previewAs')"
small
@ -10,17 +10,13 @@
class="white--text nc-btn-preview"
v-on="on"
>
<v-icon small class="mr-1">
mdi-play-circle
</v-icon>
<v-icon small class="mr-1"> mdi-play-circle </v-icon>
Preview
<v-icon small>
mdi-menu-down
</v-icon>
<v-icon small> mdi-menu-down </v-icon>
</v-btn>
</template>
<v-list dense>
<template v-for="(role) in rolesList">
<template v-for="role in rolesList">
<v-list-item
:key="role.title"
:class="`pointer nc-preview-${role.title}`"
@ -37,7 +33,8 @@
<span
class="caption text-capitalize"
:class="{ 'x-active--text': role.title === previewAs }"
>{{ role.title }}</span>
>{{ role.title }}</span
>
</v-list-item-title>
</v-list-item>
</template>
@ -45,11 +42,11 @@
<template v-if="previewAs">
<!-- <v-divider></v-divider>-->
<v-list-item @click="setPreviewUser(null)">
<v-icon small class="mr-1">
mdi-close
</v-icon>
<v-icon small class="mr-1"> mdi-close </v-icon>
<!-- 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>
</template>
</v-list>
@ -63,7 +60,10 @@
:close-on-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">
mdi-drag
</v-icon>
@ -81,7 +81,7 @@
@change="setPreviewUser($event)"
>
<v-radio
v-for="(role) in rolesList"
v-for="role in rolesList"
:key="role.title"
:value="role.title"
color="white"
@ -89,12 +89,16 @@
:class="`ml-1 nc-floating-preview-${role.title}`"
>
<template #label>
<span class="white--text caption text-capitalize">{{ role.title }}</span>
<span class="white--text caption text-capitalize">{{
role.title
}}</span>
</template>
</v-radio>
</v-radio-group>
<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>
</v-menu>
@ -103,65 +107,69 @@
<script>
export default {
name: 'PreviewAs',
name: "PreviewAs",
data: () => ({
roleIcon: {
owner: 'mdi-account-star',
creator: 'mdi-account-hard-hat',
editor: 'mdi-account-edit',
viewer: 'mdi-eye-outline',
commenter: 'mdi-comment-account-outline'
owner: "mdi-account-star",
creator: "mdi-account-hard-hat",
editor: "mdi-account-edit",
viewer: "mdi-eye-outline",
commenter: "mdi-comment-account-outline",
},
rolesList: [{ title: 'editor' }, { title: 'commenter' }, { title: 'viewer' }],
rolesList: [
{ title: "editor" },
{ title: "commenter" },
{ title: "viewer" },
],
position: {
x: 9999, y: 9999
}
x: 9999,
y: 9999,
},
}),
computed: {
previewAs: {
get() {
return this.$store.state.users.previewAs
return this.$store.state.users.previewAs;
},
set(previewAs) {
this.$store.commit('users/MutPreviewAs', previewAs)
}
}
this.$store.commit("users/MutPreviewAs", previewAs);
},
},
},
mounted() {
this.position = {
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() {
window.removeEventListener('mousemove', this.divMove, true)
window.removeEventListener('mouseup', this.mouseUp, false)
window.removeEventListener("mousemove", this.divMove, true);
window.removeEventListener("mouseup", this.mouseUp, false);
},
methods: {
setPreviewUser(previewAs) {
this.$tele.emit(`preview-as:${previewAs}`)
this.$e("a:navdraw:preview", { role: previewAs });
if (!process.env.EE) {
this.$toast.info('Available in Enterprise edition').goAway(3000)
this.$toast.info("Available in Enterprise edition").goAway(3000);
} else {
this.previewAs = previewAs
window.location.reload()
this.previewAs = previewAs;
window.location.reload();
}
},
mouseUp() {
window.removeEventListener('mousemove', this.divMove, true)
window.removeEventListener("mousemove", this.divMove, true);
},
mouseDown(e) {
window.addEventListener('mousemove', this.divMove, true)
window.addEventListener("mousemove", this.divMove, true);
},
divMove(e) {
this.position = { y: e.clientY - 10, x: e.clientX - 18 }
}
}
}
this.position = { y: e.clientY - 10, x: e.clientX - 18 };
},
},
};
</script>
<style scoped>
</style>
<style scoped></style>

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

@ -5,19 +5,31 @@
</h3>
<v-divider />
<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-if="installPlugin && pluginInstallOverlay"
:dark="$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-dialog>
<dlg-ok-new
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"
type="primary"
@ok="confirmResetPlugin"
@ -29,7 +41,9 @@
:dark="$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-btn color="primary" @click="confirmResetPlugin">
Yes
@ -43,25 +57,22 @@
<v-container class="h-100 app-container">
<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)"-->
<v-card
height="100%"
class="elevatio app-item-card "
>
<div class="install-btn ">
<v-card height="100%" class="elevatio app-item-card">
<div class="install-btn">
<v-btn
v-if="app.parsedInput"
x-small
outlined
class=" caption text-capitalize"
class="caption text-capitalize"
@click="installApp(app)"
>
<v-icon x-small class="mr-1">
mdi-pencil
</v-icon>
{{ $t('general.edit') }}
{{ $t("general.edit") }}
</v-btn>
<v-btn
v-if="app.parsedInput"
@ -75,7 +86,13 @@
</v-icon>
Reset
</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">
mdi-plus
</v-icon>
@ -96,36 +113,35 @@
</v-icon>
</v-avatar>
<div class="flex-grow-1">
<v-card-title
class="title "
v-text="app.title"
/>
<v-card-title class="title" v-text="app.title" />
<v-card-subtitle class="pb-1" v-text="app.description" />
<v-card-actions>
<div class="d-flex justify-space-between d-100 align-center">
<!-- <v-rating-->
<!-- full-icon="mdi-star"-->
<!-- readonly-->
<!-- length="5"-->
<!-- size="15"-->
<!-- :value="5"-->
<!-- />-->
<div
class="d-flex justify-space-between d-100 align-center"
>
<!-- <v-rating-->
<!-- full-icon="mdi-star"-->
<!-- readonly-->
<!-- length="5"-->
<!-- size="15"-->
<!-- :value="5"-->
<!-- />-->
<!-- <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-if="app.price && app.price !== 'Free'">${{ app.price }} / mo</span>-->
<!-- <span class="subtitles" v-else>Free</span>-->
</div>
</v-card-actions>
<!-- <v-card-actions>-->
<!-- <v-btn-->
<!-- outlined-->
<!-- rounded-->
<!-- small-->
<!-- >-->
<!-- Download-->
<!-- </v-btn>-->
<!-- </v-card-actions>-->
<!-- <v-card-actions>-->
<!-- <v-btn-->
<!-- outlined-->
<!-- rounded-->
<!-- small-->
<!-- >-->
<!-- Download-->
<!-- </v-btn>-->
<!-- </v-card-actions>-->
</div>
</div>
</v-card>
@ -203,11 +219,20 @@ export default {
}),
computed: {
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() {
return this.apps.filter(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)))
return this.apps.filter(
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.$tele.emit(`appstore:reset:${this.resetPluginRef.title}`)
this.$e('a:appstore:reset', { app: this.resetPluginRef.title })
},
async saved() {
this.pluginInstallOverlay = false
await this.loadPluginList()
this.$tele.emit(`appstore:install:submit:${this.installPlugin.title}`)
this.$e('a:appstore:install', { app: this.installPlugin.title })
},
async installApp(app) {
this.pluginInstallOverlay = true
this.installPlugin = app
this.$tele.emit(`appstore:install:trigger:${app.title}`)
this.$e('c:appstore:install', { app: app.title })
},
async resetApp(app) {
this.pluginUninstallModal = true
@ -254,18 +279,15 @@ export default {
p.parsedInput = p.input && JSON.parse(p.input)
return p
})
} catch (e) {
}
} catch (e) {}
}
}
}
</script>
<style scoped lang="scss">
.app-item-card {
transition: .4s background-color;
transition: 0.4s background-color;
position: relative;
overflow-x: hidden;
@ -274,7 +296,7 @@ export default {
opacity: 0;
right: -100%;
top: 10px;
transition: .4s opacity, .4s right;
transition: 0.4s opacity, 0.4s right;
}
&:hover .install-btn {
@ -284,7 +306,7 @@ export default {
}
.app-item-card {
transition: .4s background-color, .4s transform;
transition: 0.4s background-color, 0.4s transform;
&:hover {
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 {
transform: scale(.75);
transform: scale(0.75);
}
.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;
}
}
@ -319,7 +342,6 @@ export default {
height: 100%;
overflow-y: auto;
}
</style>
<!--
/**

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -32,19 +32,19 @@
>
<v-list-item>
<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
</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
</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
</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
</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
</v-icon>
</div>

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

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

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

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

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

@ -1,10 +1,20 @@
<template>
<v-menu offset-y max-width="350">
<template #activator="{on}">
<v-icon v-if="value === 'locked'" small class="mx-1 nc-view-lock-menu" v-on="on">
<template #activator="{ on }">
<v-icon
v-if="value === 'locked'"
small
class="mx-1 nc-view-lock-menu"
v-on="on"
>
mdi-lock-outline
</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
</v-icon>
<v-icon v-else small class="mx-1 nc-view-lock-menu" v-on="on">
@ -12,7 +22,11 @@
</v-icon>
</template>
<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-icon v-if="!value || value === 'collaborative'" small>
mdi-check-bold
@ -26,51 +40,62 @@
Collaborative view
</v-list-item-title>
<v-list-item-subtitle 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
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-content>
</v-list-item>
<v-list-item two-line class="pb-4" @click="changeLockType('locked')">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === 'locked'" small>
mdi-check-bold
</v-icon>
<v-icon v-if="value === 'locked'" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content class="pb-1">
<v-list-item-title>
<v-icon small class="mt-n1" color="primary">
mdi-lock
</v-icon>
<v-icon small class="mt-n1" color="primary"> mdi-lock </v-icon>
Locked View
</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.
</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>
<v-list-item three-line @click="changeLockType('personal')">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="value === 'personal'" small>
mdi-check-bold
</v-icon>
<v-icon v-if="value === 'personal'" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<v-icon small class="mt-n1" color="primary">
mdi-account
</v-icon>
<v-icon small class="mt-n1" color="primary"> mdi-account </v-icon>
Personal view
</v-list-item-title>
<v-list-item-subtitle 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
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>
<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>
</v-list>
@ -79,24 +104,20 @@
<script>
export default {
name: 'LockMenu',
props: ['value'],
data: () => ({
}),
name: "LockMenu",
props: ["value"],
data: () => ({}),
methods: {
changeLockType(type) {
this.$tele.emit(`lockmenu:${type}`)
if (type === 'personal') {
return this.$toast.info('Coming soon').goAway(3000)
this.$e("a:grid:lockmenu", { lockType: type });
if (type === "personal") {
return this.$toast.info("Coming soon").goAway(3000);
}
this.$emit('input', type)
this.$toast.success(`Successfully Switched to ${type} view`).goAway(3000)
}
}
}
this.$emit("input", type);
this.$toast.success(`Successfully Switched to ${type} view`).goAway(3000);
},
},
};
</script>
<style scoped>
</style>
<style scoped></style>

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

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

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

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

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

@ -106,7 +106,7 @@
{{ $t('general.cancel') }}
</v-btn>
<v-btn
v-t="['vitual:column:delete']"
v-t="['a:column:delete']"
small
color="error"
@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>
<v-tabs
v-model="active"
:height="relationTabs && relationTabs.length ?38:0"
:height="relationTabs && relationTabs.length ? 38 : 0"
class="table-tabs"
:class="{'hidden-tab':!relationTabs || !relationTabs.length}"
:class="{ 'hidden-tab': !relationTabs || !relationTabs.length }"
color="pink"
@change="onTabChange"
>
<template v-if="_isUIAllowed('smartSheet')">
<v-tab v-show="relationTabs && relationTabs.length" class="">
<v-icon small>
mdi-table-edit
</v-icon>&nbsp;<span
class="caption text-capitalize font-weight-bold"
> {{ nodes.title }}</span>
<v-icon small> mdi-table-edit </v-icon>&nbsp;<span
class="caption text-capitalize font-weight-bold"
>
{{ nodes.title }}</span
>
</v-tab>
<v-tab-item
style="height:100%"
>
<v-tab-item style="height: 100%">
<rows-xc-data-table
ref="tabs7"
:is-view="isView"
@ -51,15 +49,15 @@
</template>
<script>
import { mapActions } from 'vuex'
import dlgLabelSubmitCancel from '../utils/dlgLabelSubmitCancel'
import { isMetaTable } from '@/helpers/xutils'
import RowsXcDataTable from '@/components/project/spreadsheet/rowsXcDataTable'
import { mapActions } from "vuex";
import dlgLabelSubmitCancel from "../utils/dlgLabelSubmitCancel";
import { isMetaTable } from "@/helpers/xutils";
import RowsXcDataTable from "@/components/project/spreadsheet/rowsXcDataTable";
export default {
components: {
RowsXcDataTable,
dlgLabelSubmitCancel
dlgLabelSubmitCancel,
},
data() {
return {
@ -74,90 +72,90 @@ export default {
loadRows: false,
loadColumnsMock: false,
relationTabs: [],
deleteId: null
}
deleteId: null,
};
},
methods: {
async handleKeyDown(event) {
const activeTabEleKey = `tabs${this.active}`
if (this.$refs[activeTabEleKey] &&
const activeTabEleKey = `tabs${this.active}`;
if (
this.$refs[activeTabEleKey] &&
this.$refs[activeTabEleKey].handleKeyDown
) {
await this.$refs[activeTabEleKey].handleKeyDown(event)
await this.$refs[activeTabEleKey].handleKeyDown(event);
}
},
...mapActions({
removeTableTab: 'tabs/removeTableTab',
loadTablesFromParentTreeNode: 'project/loadTablesFromParentTreeNode'
removeTableTab: "tabs/removeTableTab",
loadTablesFromParentTreeNode: "project/loadTablesFromParentTreeNode",
}),
mtdNewTableUpdate(value) {
this.newTableCopy = value
this.newTableCopy = value;
},
async deleteTable(action = '', id) {
async deleteTable(action = "", id) {
if (id) {
this.deleteId = id
this.deleteId = id;
}
if (action === 'showDialog') {
this.dialogShow = true
} else if (action === 'hideDialog') {
this.dialogShow = false
if (action === "showDialog") {
this.dialogShow = true;
} else if (action === "hideDialog") {
this.dialogShow = false;
} else {
// todo : check relations and triggers
try {
await this.$api.dbTable.delete(this.deleteId)
await this.$api.dbTable.delete(this.deleteId);
this.removeTableTab({
env: this.nodes.env,
dbAlias: this.nodes.dbAlias,
table_name: this.nodes.table_name
})
table_name: this.nodes.table_name,
});
await this.loadTablesFromParentTreeNode({
_nodes: {
...this.nodes
}
})
...this.nodes,
},
});
this.$store.commit('meta/MutMeta', {
this.$store.commit("meta/MutMeta", {
key: this.nodes.table_name,
value: null
})
this.$store.commit('meta/MutMeta', {
value: null,
});
this.$store.commit("meta/MutMeta", {
key: this.deleteId,
value: null
})
value: null,
});
} catch (e) {
const msg = await this._extractSdkResponseErrorMsg(e)
this.$toast.error(msg).goAway(3000)
const msg = await this._extractSdkResponseErrorMsg(e);
this.$toast.error(msg).goAway(3000);
}
this.dialogShow = false
this.$tele.emit('table:delete:submit')
this.dialogShow = false;
this.$e("a:table:delete");
}
},
onTabChange() {
this.$emit('update:hideLogWindows', this.active === 2)
}
this.$emit("update:hideLogWindows", this.active === 2);
},
},
computed: {
isMetaTable() {
return isMetaTable(this.nodes.table_name)
}
return isMetaTable(this.nodes.table_name);
},
},
mounted() {
this.onTabChange()
this.onTabChange();
},
props: {
nodes: Object,
hideLogWindows: Boolean,
tabId: String,
isActive: Boolean,
isView: Boolean
}
}
isView: Boolean,
},
};
</script>
<style scoped>
/*/deep/ .table-tabs > .v-tabs-items {
border-top: 1px solid #7F828B33;
}*/
@ -166,12 +164,13 @@ export default {
margin-top: -2px;
}
.table-tabs, /deep/ .table-tabs > .v-windows {
.table-tabs,
/deep/ .table-tabs > .v-windows {
height: 100%;
}
/deep/ .v-window-item {
height: 100%
height: 100%;
}
.rel-row-parent {
@ -189,7 +188,6 @@ export default {
overflow: hidden;
color: grey;
}
</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-title>
<v-breadcrumbs
:items="[{
text: nodes.env,
disabled: true,
href: '#'
},{
text: nodes.dbAlias,
disabled: true,
href: '#'
},
{
text: nodes.title + ' (Webhooks)',
disabled: true,
href: '#'
}]"
:items="[
{
text: nodes.env,
disabled: true,
href: '#',
},
{
text: nodes.dbAlias,
disabled: true,
href: '#',
},
{
text: nodes.title + ' (Webhooks)',
disabled: true,
href: '#',
},
]"
divider=">"
small
>
<template #divider>
<v-icon small color="grey lighten-2">
forward
</v-icon>
<v-icon small color="grey lighten-2"> forward </v-icon>
</template>
</v-breadcrumbs>
</v-toolbar-title>
<v-spacer />
<!--tooltip="Close webhooks modal"-->
<x-btn
outlined
small
@click.prevent="$emit('close')"
>
<v-icon small left>
mdi-close-circle-outline
</v-icon>
<x-btn outlined small @click.prevent="$emit('close')">
<v-icon small left> mdi-close-circle-outline </v-icon>
<!-- Close -->
{{ $t('general.close') }}
{{ $t("general.close") }}
</x-btn>
<!--tooltip="Reload hooks"-->
<x-btn
v-ge="['hooks','reload']"
v-ge="['hooks', 'reload']"
outlined
color="primary"
small
@click.prevent="loadHooksList"
>
<v-icon small left>
mdi-reload
</v-icon>
<v-icon small left> mdi-reload </v-icon>
<!-- Reload -->
{{ $t('general.reload') }}
{{ $t("general.reload") }}
</x-btn>
<!--:tooltip="$t('tooltip.saveChanges')"-->
<x-btn
v-ge="['hooks','add new']"
v-ge="['hooks', 'add new']"
outlined
color="primary"
small
@click.prevent="addNewHook"
>
<v-icon small left>
mdi-plus
</v-icon>
<v-icon small left> mdi-plus </v-icon>
<!--Add New-->
{{ $t('activity.addWebhook') }}
{{ $t("activity.addWebhook") }}
</x-btn>
<!-- <x-btn outlined tooltip="Save Changes"
@ -84,12 +75,7 @@
</x-btn>-->
</v-toolbar>
<v-form
ref="form"
v-model="valid"
class="mx-auto"
lazy-validation
>
<v-form ref="form" v-model="valid" class="mx-auto" lazy-validation>
<v-container fluid>
<v-row>
<v-col cols="7">
@ -102,30 +88,30 @@
<th />
<th>
<!--Title-->
{{ $t('general.title') }}
{{ $t("general.title") }}
</th>
<th>
<!--Event-->
{{ $t('general.event') }}
{{ $t("general.event") }}
</th>
<th>
<!--Condition-->
{{ $t('general.condition') }}
{{ $t("general.condition") }}
</th>
<th>
<!--Notify Via-->
{{ $t('labels.notifyVia') }}
{{ $t("labels.notifyVia") }}
</th>
<th>
<!--Action-->
{{ $t('labels.action') }}
{{ $t("labels.action") }}
</th>
</tr>
</thead>
<tbody>
<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>
<v-radio :value="i" />
</td>
@ -136,12 +122,18 @@
mdi-check-bold
</v-icon>
</td>
<td>{{ item.notification && item.notification.type }}</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
</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
</x-icon>-->
</td>
@ -151,17 +143,15 @@
<td colspan="6" class="text-center py-5">
<!--:tooltip="$t('tooltip.saveChanges')"-->
<x-btn
v-ge="['hooks','add new']"
v-ge="['hooks', 'add new']"
outlined
color="primary"
small
@click.prevent="addNewHook"
>
<v-icon small left>
mdi-plus
</v-icon>
<v-icon small left> mdi-plus </v-icon>
<!--Add New Webhook-->
{{ $t('activity.addWebhook') }}
{{ $t("activity.addWebhook") }}
</x-btn>
</td>
</tr>
@ -177,20 +167,17 @@
Webhook
<v-spacer />
<x-btn
v-ge="['hooks','save']"
v-ge="['hooks', 'save']"
outlined
tooltip="Save"
color="primary"
small
:disabled="loading || !valid || !hook.event"
@click.prevent="saveHooks"
>
<v-icon small left>
save
</v-icon>
<v-icon small left> save </v-icon>
<!-- Save -->
{{ $t('general.save') }}
{{ $t("general.save") }}
</x-btn>
</v-card-title>
<v-card-text>
@ -201,7 +188,7 @@
dense
:label="$t('general.title')"
required
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
/>
<webhook-event
@ -242,12 +229,12 @@
:label="$t('general.notification')"
required
:items="notificationList"
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
class="caption"
:prepend-inner-icon="notificationIcon[hook.notification.type]"
@change="onNotTypeChange"
>
<template #item="{item}">
<template #item="{ item }">
<v-list-item-icon>
<v-icon small>
{{ notificationIcon[item] }}
@ -267,7 +254,7 @@
<v-combobox
v-if="slackChannels"
v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="slackChannels"
item-text="channel"
label="Select Slack channels"
@ -281,7 +268,7 @@
<v-combobox
v-if="teamsChannels"
v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="teamsChannels"
item-text="channel"
label="Select Teams channels"
@ -295,7 +282,7 @@
<v-combobox
v-if="discordChannels"
v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="discordChannels"
item-text="channel"
label="Select Discord channels"
@ -309,7 +296,7 @@
<v-combobox
v-if="mattermostChannels"
v-model="notification.channels"
:rules="[v => !!v || `${$t('general.required')}`]"
:rules="[(v) => !!v || `${$t('general.required')}`]"
:items="mattermostChannels"
item-text="channel"
label="Select Mattermost channels"
@ -330,7 +317,10 @@
dense
outlined
:label="input.label"
:rules="[v => !input.required || !!v || `${$t('general.required')}`]"
:rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/>
<v-text-field
v-else
@ -340,7 +330,10 @@
dense
outlined
:label="input.label"
:rules="[v => !input.required || !!v || `${$t('general.required')}`]"
:rules="[
(v) =>
!input.required || !!v || `${$t('general.required')}`,
]"
/>
</template>
</template>
@ -348,25 +341,27 @@
<v-card-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>
<template #activator="{on}">
<v-icon
small
color="grey"
class="ml-2"
v-on="on"
>mdi-information</v-icon>
<template #activator="{ on }">
<v-icon small color="grey" class="ml-2" v-on="on"
>mdi-information</v-icon
>
</template>
<span class="caption">
<strong>data</strong> : Row data <br>
<strong>user</strong> : User information<br>
<strong>data</strong> : Row data <br />
<strong>user</strong> : User information<br />
</span>
</v-tooltip>
<br>
<a href="https://docs.nocodb.com/developer-resources/webhooks/">
<br />
<a
href="https://docs.nocodb.com/developer-resources/webhooks/"
>
<!--Document Reference-->
{{ $t('labels.docReference') }}
{{ $t("labels.docReference") }}
</a>
</span>
@ -377,8 +372,8 @@
filters,
notification: {
...hook.notification,
payload: notification
}
payload: notification,
},
}"
/>
</v-card-text>
@ -391,22 +386,22 @@
</template>
<script>
import HttpWebhook from './webhook/httpWebhook'
import ColumnFilter from '~/components/project/spreadsheet/components/columnFilter'
import HttpWebhook from "./webhook/httpWebhook";
import ColumnFilter from "~/components/project/spreadsheet/components/columnFilter";
// import FormInput from '~/components/project/appStore/FormInput'
import WebhookEvent from '~/components/project/tableTabs/webhookEvent'
import WebhooksTest from '~/components/project/tableTabs/webhooksTest'
import WebhookEvent from "~/components/project/tableTabs/webhookEvent";
import WebhooksTest from "~/components/project/tableTabs/webhooksTest";
export default {
name: 'Webhooks',
name: "Webhooks",
components: {
WebhooksTest,
HttpWebhook,
WebhookEvent,
// FormInput,
ColumnFilter
ColumnFilter,
},
props: ['nodes'],
props: ["nodes"],
data: () => ({
key: 0,
apps: {},
@ -421,138 +416,148 @@ export default {
meta: null,
loading: false,
notificationList: [
'Email',
'Slack',
'Microsoft Teams',
'Discord',
'Mattermost',
'Twilio',
'Whatsapp Twilio',
'URL'
"Email",
"Slack",
"Microsoft Teams",
"Discord",
"Mattermost",
"Twilio",
"Whatsapp Twilio",
"URL",
],
filters: [],
hook: null,
notification: {},
notificationIcon: {
URL: 'mdi-link',
Email: 'mdi-email',
Slack: 'mdi-slack',
'Microsoft Teams': 'mdi-microsoft-teams',
Discord: 'mdi-discord',
Mattermost: 'mdi-chat',
'Whatsapp Twilio': 'mdi-whatsapp',
Twilio: 'mdi-cellphone-message'
URL: "mdi-link",
Email: "mdi-email",
Slack: "mdi-slack",
"Microsoft Teams": "mdi-microsoft-teams",
Discord: "mdi-discord",
Mattermost: "mdi-chat",
"Whatsapp Twilio": "mdi-whatsapp",
Twilio: "mdi-cellphone-message",
},
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: [],
inputs: {
Email: [
{
key: 'to',
label: 'To Address',
placeholder: 'To Address',
type: 'SingleLineText',
required: true
key: "to",
label: "To Address",
placeholder: "To Address",
type: "SingleLineText",
required: true,
},
{
key: 'subject',
label: 'Subject',
placeholder: 'Subject',
type: 'SingleLineText',
required: true
}, {
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
key: "subject",
label: "Subject",
placeholder: "Subject",
type: "SingleLineText",
required: true,
},
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
],
Slack: [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
Slack: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
],
'Microsoft Teams': [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
"Microsoft Teams": [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
],
Discord: [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
Discord: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
],
Mattermost: [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}
Mattermost: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
],
Twilio: [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}, {
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}],
'Whatsapp Twilio': [{
key: 'body',
label: 'Body',
placeholder: 'Body',
type: 'LongText',
required: true
}, {
key: 'to',
label: 'Comma separated Mobile #',
placeholder: 'Comma separated Mobile #',
type: 'LongText',
required: true
}]
}
Twilio: [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
{
key: "to",
label: "Comma separated Mobile #",
placeholder: "Comma separated Mobile #",
type: "LongText",
required: true,
},
],
"Whatsapp Twilio": [
{
key: "body",
label: "Body",
placeholder: "Body",
type: "LongText",
required: true,
},
{
key: "to",
label: "Comma separated Mobile #",
placeholder: "Comma separated Mobile #",
type: "LongText",
required: true,
},
],
},
}),
async created() {
await this.loadMeta()
await this.loadHooksList()
await this.loadMeta();
await this.loadHooksList();
// todo: load only necessary plugins
await this.loadPluginList()
this.selectedHook = 0
this.onEventChange()
await this.loadPluginList();
this.selectedHook = 0;
this.onEventChange();
},
methods: {
async loadPluginList() {
try {
// 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))
this.apps = plugins.reduce((o, p) => {
p.tags = p.tags ? p.tags.split(',') : []
p.parsedInput = p.input && JSON.parse(p.input)
o[p.title] = p
return o
}, {})
} catch (e) {
}
p.tags = p.tags ? p.tags.split(",") : [];
p.parsedInput = p.input && JSON.parse(p.input);
o[p.title] = p;
return o;
}, {});
} catch (e) {}
},
checkConditionAvail() {
// if (!process.env.EE) {
@ -562,84 +567,102 @@ export default {
// this.hook.condition = []
},
async onNotTypeChange() {
this.notification = {}
if (this.hook.notification.type === 'Slack') {
this.notification = {};
if (this.hook.notification.type === "Slack") {
// const plugin = await this.$store.dispatch('sqlMgr/ActSqlOp', [null, 'xcPluginRead', {
// title: 'Slack'
// }])
// 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', {
// title: 'Microsoft Teams'
// }])
// 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', {
// 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', {
// title: 'Mattermost'
// }])
// 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() {
this.key++
this.key++;
if (!this.hooks || !this.hooks.length) {
return
return;
}
const {
notification: {
payload,
type
} = {},
...hook
} = this.hooks[this.selectedHook] || {}
const { notification: { payload, type } = {}, ...hook } =
this.hooks[this.selectedHook] || {};
this.hook = {
...hook,
notification: {
type
}
}
type,
},
};
// this.enableCondition = !!(this.hook && this.hook.condition && Object.keys(this.hook.condition).length)
await this.onNotTypeChange()
this.notification = payload
if (this.hook.notification.type === 'Slack') {
this.notification.webhook_url = this.notification.webhook_url &&
this.notification.webhook_url.map(v => this.slackChannels.find(s => v.webhook_url === s.webhook_url))
await this.onNotTypeChange();
this.notification = payload;
if (this.hook.notification.type === "Slack") {
this.notification.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') {
this.notification.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 === "Microsoft Teams") {
this.notification.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') {
this.notification.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 === "Discord") {
this.notification.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') {
this.notification.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 === "Mattermost") {
this.notification.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
this.notification.api = this.notification.api
this.notification.api = this.notification.api;
}
},
async saveHooks() {
if (!this.$refs.form.validate() || !this.valid || !this.hook.event) {
return
return;
}
this.loading = true
this.loading = true;
try {
// const res = await this.$store.dispatch('sqlMgr/ActSqlOp', [
// {
@ -656,58 +679,66 @@ export default {
// }
// }
// ])
let res
let res;
if (this.hook.id) {
res = await this.$api.dbTableWebhook.update(this.hook.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification
}
})
payload: this.notification,
},
});
} else {
res = await this.$api.dbTableWebhook.create(this.meta.id, {
...this.hook,
notification: {
...this.hook.notification,
payload: this.notification
}
})
payload: this.notification,
},
});
}
if (!this.hook.id && res) {
this.hook.id = res.id
this.hook.id = res.id;
}
if (this.$refs.filter) {
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) {
this.$toast.error(e.message).goAway(3000)
this.$toast.error(e.message).goAway(3000);
}
this.loading = false
await this.loadHooksList()
this.$tele.emit(`webhooks:save:${this.hook.operation}:${this.hook.condition}:${this.hook.notification.type}`)
this.loading = false;
await this.loadHooksList();
this.$e("a:webhook:add", {
operation: this.hook.operation,
condition: this.hook.condition,
notification: this.hook.notification.type,
});
},
async loadMeta() {
this.loadingMeta = true
this.loadingMeta = true;
// const tableMeta = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
// }, 'tableXcModelGet', {
// tn: this.nodes.table_name
// }] )
this.meta = await this.$store.dispatch('meta/ActLoadMeta', { table_name: this.nodes.table_name })// JSON.parse(tableMeta.meta)
this.fieldList = this.meta.columns.map(c => c.column_name)
this.loadingMeta = false
this.meta = await this.$store.dispatch("meta/ActLoadMeta", {
table_name: this.nodes.table_name,
}); // JSON.parse(tableMeta.meta)
this.fieldList = this.meta.columns.map((c) => c.column_name);
this.loadingMeta = false;
},
async loadHooksList() {
this.key++
this.loading = true
this.key++;
this.loading = true;
// const hooks = await this.$store.dispatch('sqlMgr/ActSqlOp', [{
// env: this.nodes.env,
// dbAlias: this.nodes.dbAlias
@ -715,49 +746,49 @@ export default {
// 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) => {
h.notification = h.notification && JSON.parse(h.notification)
h.notification = h.notification && JSON.parse(h.notification);
// h.condition = h.condition && JSON.parse(h.condition)
return h
})
this.loading = false
return h;
});
this.loading = false;
},
addNewHook() {
this.key++
this.selectedHook = this.hooks.length
this.key++;
this.selectedHook = this.hooks.length;
this.hooks.push({
notification: {
// type:'Email'
}
})
this.onEventChange()
this.$refs.form.resetValidation()
},
});
this.onEventChange();
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) {
try {
if (item.id) {
await this.$api.dbTableWebhook.delete(item.id)
this.hooks.splice(i, 1)
await this.$api.dbTableWebhook.delete(item.id);
this.hooks.splice(i, 1);
} 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) {
this.hook = null
this.hook = null;
}
} 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>
<style scoped>
@ -766,7 +797,7 @@ export default {
/*}*/
/deep/ label {
font-size: 0.75rem !important
font-size: 0.75rem !important;
}
</style>
<!--

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save