mirror of https://github.com/nocodb/nocodb
591 changed files with 159642 additions and 47437 deletions
@ -0,0 +1,26 @@
|
||||
name: "Publish : Api Docs" |
||||
|
||||
on: |
||||
push: |
||||
branches: [ master ] |
||||
paths: |
||||
- "scripts/sdk/swagger.json" |
||||
|
||||
jobs: |
||||
copy-file: |
||||
runs-on: ubuntu-latest |
||||
steps: |
||||
- name: Checkout |
||||
uses: actions/checkout@v2 |
||||
|
||||
- name: Pushes generated output |
||||
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658 |
||||
env: |
||||
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }} |
||||
with: |
||||
source_file: 'scripts/sdk/swagger.json' |
||||
destination_repo: 'nocodb/noco-apis-doc' |
||||
destination_folder: 'src' |
||||
user_email: 'oof1lab@gmail.com' |
||||
user_name: 'o1lab' |
||||
commit_message: 'Autorelease from github.com/nocodb/nc' |
@ -0,0 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="Drop metadb" type="NodeJSConfigurationType" path-to-js-file="deleteMetaDb.js" working-dir="$PROJECT_DIR$/packages/nocodb/src/example"> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,14 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="Build Nc Common" type="js.build_tools.npm"> |
||||
<package-json value="$PROJECT_DIR$/packages/nocodb-sdk/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="build" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs> |
||||
<env name="NC_DISABLE_CACHE" value="true" /> |
||||
</envs> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,12 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="Run GUI" type="js.build_tools.npm"> |
||||
<package-json value="$PROJECT_DIR$/packages/nc-gui/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="dev" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs /> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,14 @@
|
||||
<component name="ProjectRunConfigurationManager"> |
||||
<configuration default="false" name="Run NocoDB Mysql" type="js.build_tools.npm" activateToolWindowBeforeRun="false"> |
||||
<package-json value="$PROJECT_DIR$/packages/nocodb/package.json" /> |
||||
<command value="run" /> |
||||
<scripts> |
||||
<script value="watch:run:mysql" /> |
||||
</scripts> |
||||
<node-interpreter value="project" /> |
||||
<envs> |
||||
<env name="NC_DISABLE_CACHE" value="true" /> |
||||
</envs> |
||||
<method v="2" /> |
||||
</configuration> |
||||
</component> |
@ -0,0 +1,42 @@
|
||||
# Setup |
||||
|
||||
#### Setting up dev environment |
||||
|
||||
- Clone `nocodb/nocodb` GitHub repo and checkout to `feat/v2` branch |
||||
```sh |
||||
git clone https://github.com/nocodb/nc |
||||
cd nocodb |
||||
``` |
||||
|
||||
- Navigate to `nocodb-sdk` package folder, install and build the package |
||||
```sh |
||||
cd packages/nocodb-sdk |
||||
npm install |
||||
npm run build |
||||
``` |
||||
|
||||
#### Running backend |
||||
|
||||
```sh |
||||
# Navigate to `nocodb` package and install dependencies |
||||
cd packages/nocodb |
||||
npm install |
||||
|
||||
# requires sqlite3 |
||||
|
||||
npm run watch:run |
||||
|
||||
# if you have mysql on localhost (set its password as password) |
||||
# npm run watch:run:mysql |
||||
``` |
||||
|
||||
#### Running frontend |
||||
|
||||
```sh |
||||
# Navigate to `nc-gui` package and install dependencies |
||||
cd packages/nc-gui |
||||
npm install |
||||
npm run dev |
||||
``` |
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
{ |
||||
"root": true, |
||||
"parser": "@typescript-eslint/parser", |
||||
"parserOptions": { "project": "./tsconfig.json" }, |
||||
"env": { "es6": true }, |
||||
"ignorePatterns": ["node_modules", "build", "coverage"], |
||||
"plugins": ["import", "eslint-comments", "functional"], |
||||
"extends": [ |
||||
"eslint:recommended", |
||||
"plugin:eslint-comments/recommended", |
||||
"plugin:@typescript-eslint/recommended", |
||||
"plugin:import/typescript", |
||||
"plugin:functional/lite", |
||||
"prettier", |
||||
"prettier/@typescript-eslint" |
||||
], |
||||
"globals": { "BigInt": true, "console": true, "WebAssembly": true }, |
||||
"rules": { |
||||
"@typescript-eslint/explicit-module-boundary-types": "off", |
||||
"eslint-comments/disable-enable-pair": [ |
||||
"error", |
||||
{ "allowWholeFile": true } |
||||
], |
||||
"eslint-comments/no-unused-disable": "error", |
||||
"import/order": [ |
||||
"error", |
||||
{ "newlines-between": "always", "alphabetize": { "order": "asc" } } |
||||
], |
||||
"sort-imports": [ |
||||
"error", |
||||
{ "ignoreDeclarationSort": true, "ignoreCase": true } |
||||
] |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -1,2 +0,0 @@
|
||||
export * from './lib/XcUIBuilder'; |
||||
export * from './lib/XcNotification'; |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,285 @@
|
||||
<template> |
||||
<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'] }} |
||||
|
||||
<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> |
||||
<template v-else> |
||||
<!-- Invite Team --> |
||||
{{ $t('activity.inviteTeam') }} |
||||
</template> |
||||
</div> |
||||
<div class="pa-4 nc-invite-container"> |
||||
<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)" |
||||
> |
||||
<template #append> |
||||
<v-icon color="green" class="ml-2"> |
||||
mdi-content-copy |
||||
</v-icon> |
||||
</template> |
||||
<div class="ellipsis d-100"> |
||||
{{ inviteUrl }} |
||||
</div> |
||||
</v-alert> |
||||
|
||||
<p class="caption grey--text mt-3"> |
||||
{{ $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(', ')) }}. |
||||
</p> |
||||
|
||||
<div class="text-right"> |
||||
<!--tooltip="Invite more users"--> |
||||
<x-btn |
||||
:tooltip="$t('tooltip.inviteMore')" |
||||
small |
||||
outlined |
||||
btn.class="grey--text" |
||||
@click="clickInviteMore" |
||||
> |
||||
<v-icon small color="grey" class="mr-1"> |
||||
mdi-account-multiple-plus-outline |
||||
</v-icon> |
||||
<!--Invite more--> |
||||
{{ $t('activity.inviteMore') }} |
||||
</x-btn> |
||||
</div> |
||||
|
||||
<!-- todo: show error message if failed--> |
||||
</div> |
||||
<template v-else> |
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveUser"> |
||||
<v-row class="my-0"> |
||||
<v-col cols="8" class="py-0"> |
||||
<!--hint="You can add multiple comma(,) separated emails"--> |
||||
<v-text-field |
||||
ref="email" |
||||
v-model="selectedUser.email" |
||||
:disabled="!!selectedUser.id" |
||||
dense |
||||
validate-on-blur |
||||
outlined |
||||
:rules="emailRules" |
||||
class="caption" |
||||
:hint="$t('msg.info.addMultipleUsers')" |
||||
label="Email" |
||||
@input="edited=true" |
||||
> |
||||
<template #label> |
||||
<span class="caption"> |
||||
<!-- Email --> |
||||
{{ $t('labels.email') }} |
||||
</span> |
||||
</template> |
||||
</v-text-field> |
||||
</v-col> |
||||
<v-col cols="4" class="py-0"> |
||||
<!--label="Select User Role"--> |
||||
<v-combobox |
||||
v-model="selectedRoles" |
||||
outlined |
||||
:rules="roleRules" |
||||
class="role-select caption" |
||||
hide-details="auto" |
||||
:items="roles" |
||||
:label="$t('labels.selectUserRole')" |
||||
dense |
||||
deletable-chips |
||||
@change="edited = true" |
||||
> |
||||
<template #selection="{item}"> |
||||
<v-chip small :color="rolesColors[item]"> |
||||
{{ item }} |
||||
</v-chip> |
||||
</template> |
||||
<template #item="{item}"> |
||||
<div> |
||||
<div>{{ item }}</div> |
||||
<div class="mb-2 caption grey--text"> |
||||
{{ roleDescriptions[item] }} |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</v-combobox> |
||||
</v-col> |
||||
</v-row> |
||||
</v-form> |
||||
<div class="text-center mt-0"> |
||||
<x-btn |
||||
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' }} |
||||
</v-icon> |
||||
{{ selectedUser.id ? $t('general.save') : $t('activity.invite') }} |
||||
</x-btn> |
||||
</div> |
||||
</template> |
||||
<!-- </v-card-actions>--> |
||||
</div> |
||||
</v-card-text> |
||||
<v-card-text> |
||||
<share-base /> |
||||
</v-card-text> |
||||
</v-card> |
||||
</v-dialog> |
||||
</template> |
||||
|
||||
<script> |
||||
import { isEmail } from '~/helpers' |
||||
import { enumColor } from '~/components/project/spreadsheet/helpers/colors' |
||||
import ShareBase from '~/components/base/shareBase' |
||||
|
||||
export default { |
||||
name: 'ShareOrInviteModal', |
||||
components: { ShareBase }, |
||||
props: { |
||||
value: Boolean |
||||
}, |
||||
data: () => ({ |
||||
roles: ['creator', 'editor', 'commenter', 'viewer'], |
||||
selectedUser: {}, |
||||
invite_token: null, |
||||
valid: null, |
||||
emailRules: [ |
||||
v => !!v || 'E-mail is required', |
||||
(v) => { |
||||
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' |
||||
], |
||||
userList: [], |
||||
roleDescriptions: {}, |
||||
deleteUserType: '' |
||||
}), |
||||
computed: { |
||||
userEditDialog: { |
||||
get() { |
||||
return this.value |
||||
}, |
||||
set(v) { |
||||
this.$emit('input', v) |
||||
} |
||||
}, |
||||
inviteUrl() { |
||||
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 |
||||
return this.roles.reduce((o, r, i) => { |
||||
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] |
||||
}, |
||||
set(roles) { |
||||
if (this.selectedUser) { |
||||
this.selectedUser.roles = roles // .filter(Boolean).join(',') |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
methods: { |
||||
|
||||
async saveUser() { |
||||
this.validate = true |
||||
await this.$nextTick() |
||||
if (this.loading || !this.$refs.form.validate() || !this.selectedUser) { |
||||
return |
||||
} |
||||
this.$tele.emit(`user-mgmt:add:${this.selectedUser.roles}`) |
||||
|
||||
if (!this.edited) { |
||||
this.userEditDialog = false |
||||
} |
||||
|
||||
try { |
||||
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'] |
||||
}) |
||||
} 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'] |
||||
})) |
||||
} |
||||
this.$toast.success('Successfully updated the user details').goAway(3000) |
||||
this.$emit('saved') |
||||
if (data && data.invite_token) { |
||||
this.invite_token = data |
||||
// todo: bring anim |
||||
// this.simpleAnim() |
||||
return |
||||
} |
||||
} catch (e) { |
||||
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000) |
||||
} |
||||
|
||||
this.userEditDialog = false |
||||
}, |
||||
|
||||
clickInviteMore() { |
||||
this.$tele.emit('user-mgmt: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) |
||||
|
||||
this.$tele.emit('user-mgmt:copy-url') |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
|
||||
</style> |
@ -0,0 +1,46 @@
|
||||
<template> |
||||
<div> |
||||
<gh-btns-star |
||||
icon="mark-github" |
||||
slug="nocodb/nocodb" |
||||
show-count |
||||
:class="{'dark' : isDark}" |
||||
> |
||||
{{ ghStarText }} |
||||
</gh-btns-star> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: 'GithubStarBtn', |
||||
data: () => ({ ghStarText: 'Star' }), |
||||
mounted() { |
||||
setInterval(() => this.ghStarText = this.ghStarText === 'Star' ? 'Fork' : 'Star', 60000) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
/deep/ .gh-button-container{ |
||||
background: #fff2; |
||||
border-radius: 4px; |
||||
margin:0 |
||||
} |
||||
/deep/ .gh-button-container:not(.dark) > a { |
||||
background: transparent !important; |
||||
color: #cdcdcd !important; |
||||
} |
||||
/deep/ .gh-button-container > a:first-child{ |
||||
border-left-color: transparent; |
||||
border-top-color: transparent; |
||||
border-bottom-color: transparent; |
||||
} |
||||
/deep/ .gh-button-container > a:last-child{ |
||||
border-color: transparent; |
||||
} |
||||
|
||||
/deep/ .gh-button, /deep/ .social-count{ |
||||
padding:1px 5px; |
||||
} |
||||
</style> |
@ -0,0 +1,167 @@
|
||||
<template> |
||||
<div> |
||||
<v-menu offset-y> |
||||
<template #activator="{on}"> |
||||
<v-btn |
||||
v-show="isDashboard && _isUIAllowed('previewAs')" |
||||
small |
||||
light |
||||
color="#fff3" |
||||
class="white--text nc-btn-preview" |
||||
v-on="on" |
||||
> |
||||
<v-icon small class="mr-1"> |
||||
mdi-play-circle |
||||
</v-icon> |
||||
Preview |
||||
<v-icon small> |
||||
mdi-menu-down |
||||
</v-icon> |
||||
</v-btn> |
||||
</template> |
||||
<v-list dense> |
||||
<template v-for="(role) in rolesList"> |
||||
<v-list-item |
||||
:key="role.title" |
||||
:class="`pointer nc-preview-${role.title}`" |
||||
@click="setPreviewUser(role.title)" |
||||
> |
||||
<v-list-item-title> |
||||
<v-icon |
||||
small |
||||
class="mr-1" |
||||
:color="role.title === previewAs ? 'x-active' : ''" |
||||
> |
||||
{{ roleIcon[role.title] }} |
||||
</v-icon> |
||||
<span |
||||
class="caption text-capitalize" |
||||
:class="{ 'x-active--text': role.title === previewAs }" |
||||
>{{ role.title }}</span> |
||||
</v-list-item-title> |
||||
</v-list-item> |
||||
</template> |
||||
|
||||
<template v-if="previewAs"> |
||||
<!-- <v-divider></v-divider>--> |
||||
<v-list-item @click="setPreviewUser(null)"> |
||||
<v-icon small class="mr-1"> |
||||
mdi-close |
||||
</v-icon> |
||||
<!-- Reset Preview --> |
||||
<span class="caption nc-preview-reset">{{ $t('activity.resetReview') }}</span> |
||||
</v-list-item> |
||||
</template> |
||||
</v-list> |
||||
</v-menu> |
||||
|
||||
<v-menu |
||||
:position-x="position.x" |
||||
:position-y="position.y" |
||||
:value="previewAs" |
||||
activator="" |
||||
: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"> |
||||
<v-icon style="cursor: move" color="white" @mousedown="mouseDown"> |
||||
mdi-drag |
||||
</v-icon> |
||||
|
||||
<v-divider vertical class="mr-2" /> |
||||
|
||||
<div class="d-inline pointer d-flex align-center"> |
||||
<span>Preview as :</span> |
||||
<v-radio-group |
||||
:value="previewAs" |
||||
dense |
||||
row |
||||
class="mt-0 pt-0" |
||||
hide-details |
||||
@change="setPreviewUser($event)" |
||||
> |
||||
<v-radio |
||||
v-for="(role) in rolesList" |
||||
:key="role.title" |
||||
:value="role.title" |
||||
color="white" |
||||
dark |
||||
:class="`ml-1 nc-floating-preview-${role.title}`" |
||||
> |
||||
<template #label> |
||||
<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> |
||||
</div> |
||||
</div> |
||||
</v-menu> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
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' |
||||
}, |
||||
rolesList: [{ title: 'editor' }, { title: 'commenter' }, { title: 'viewer' }], |
||||
position: { |
||||
x: 9999, y: 9999 |
||||
} |
||||
}), |
||||
computed: { |
||||
previewAs: { |
||||
get() { |
||||
return this.$store.state.users.previewAs |
||||
}, |
||||
set(previewAs) { |
||||
this.$store.commit('users/MutPreviewAs', previewAs) |
||||
} |
||||
} |
||||
}, |
||||
mounted() { |
||||
this.position = { |
||||
y: window.innerHeight - 100, |
||||
x: window.innerWidth / 2 - 250 |
||||
} |
||||
|
||||
window.addEventListener('mouseup', this.mouseUp, false) |
||||
}, |
||||
beforeDestroy() { |
||||
window.removeEventListener('mousemove', this.divMove, true) |
||||
window.removeEventListener('mouseup', this.mouseUp, false) |
||||
}, |
||||
methods: { |
||||
setPreviewUser(previewAs) { |
||||
this.$tele.emit(`preview-as:${previewAs}`) |
||||
if (!process.env.EE) { |
||||
this.$toast.info('Available in Enterprise edition').goAway(3000) |
||||
} else { |
||||
this.previewAs = previewAs |
||||
window.location.reload() |
||||
} |
||||
}, |
||||
mouseUp() { |
||||
window.removeEventListener('mousemove', this.divMove, true) |
||||
}, |
||||
mouseDown(e) { |
||||
window.addEventListener('mousemove', this.divMove, true) |
||||
}, |
||||
|
||||
divMove(e) { |
||||
this.position = { y: e.clientY - 10, x: e.clientX - 18 } |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style scoped> |
||||
</style> |
@ -1,212 +1,489 @@
|
||||
<template> |
||||
<div |
||||
class="backgroundColor pa-2 menu-filter-dropdown" |
||||
style="width:530px;" |
||||
class="backgroundColor pa-2" |
||||
:style="{ width: nested ? '100%' : '530px' }" |
||||
> |
||||
<div class="grid" @click.stop> |
||||
<template v-for="(filter,i) in filters" dense> |
||||
<v-icon |
||||
v-if="!filter.readOnly" |
||||
:key="i + '_3'" |
||||
small |
||||
class="nc-filter-item-remove-btn" |
||||
@click.stop="filters.splice(i,1)" |
||||
> |
||||
mdi-close-box |
||||
</v-icon> |
||||
<span v-else :key="i + '_1'" /> |
||||
<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"> |
||||
<v-icon |
||||
v-if="!filter.readOnly" |
||||
:key="i + '_3'" |
||||
small |
||||
class="nc-filter-item-remove-btn" |
||||
@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']" |
||||
solo |
||||
flat |
||||
dense |
||||
hide-details |
||||
placeholder="Group op" |
||||
@click.stop |
||||
@change="saveOrUpdate(filter, i)" |
||||
> |
||||
<template #item="{ item }"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
</template> |
||||
</v-select> |
||||
</div> |
||||
<column-filter |
||||
v-if="filter.id || shared" |
||||
ref="nestedFilter" |
||||
v-model="filter.children" |
||||
:parent-id="filter.id" |
||||
:view-id="viewId" |
||||
nested |
||||
:meta="meta" |
||||
:shared="shared" |
||||
:web-hook="webHook" |
||||
:hook-id="hookId" |
||||
@updated="$emit('updated')" |
||||
@input="$emit('input', filters)" |
||||
/> |
||||
</div> |
||||
<template v-else> |
||||
<v-icon |
||||
v-if="!filter.readOnly" |
||||
:key="i + '_3'" |
||||
small |
||||
class="nc-filter-item-remove-btn" |
||||
@click.stop="deleteFilter(filter, i)" |
||||
> |
||||
mdi-close-box |
||||
</v-icon> |
||||
<span v-else :key="i + '_1'" /> |
||||
<span |
||||
v-if="!i" |
||||
:key="i + '_2'" |
||||
class="caption d-flex align-center" |
||||
>{{ $t("labels.where") }}</span |
||||
> |
||||
|
||||
<span |
||||
v-if="!i" |
||||
:key="i + '_2'" |
||||
class="caption d-flex align-center" |
||||
> |
||||
<!-- where --> |
||||
{{ $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']" |
||||
solo |
||||
flat |
||||
dense |
||||
hide-details |
||||
:disabled="filter.readOnly" |
||||
@click.stop |
||||
@change="filterUpdateCondition(filter, i)" |
||||
> |
||||
<template #item="{ item }"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
</template> |
||||
</v-select> |
||||
|
||||
<v-select |
||||
v-else |
||||
:key="i + '_4'" |
||||
v-model="filter.logicOp" |
||||
class="flex-shrink-1 flex-grow-0 elevation-0 caption " |
||||
:items="['and' ,'or']" |
||||
solo |
||||
flat |
||||
dense |
||||
hide-details |
||||
:disabled="filter.readOnly" |
||||
@click.stop |
||||
> |
||||
<template #item="{item}"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
<v-select |
||||
:key="i + '_6'" |
||||
v-model="filter.fk_column_id" |
||||
class="caption nc-filter-field-select" |
||||
:items="columns" |
||||
:placeholder="$t('objects.field')" |
||||
solo |
||||
flat |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
hide-details |
||||
item-value="id" |
||||
item-text="title" |
||||
@click.stop |
||||
@change="saveOrUpdate(filter, i)" |
||||
> |
||||
<template #item="{ item }"> |
||||
<span |
||||
:class="`caption font-weight-regular nc-filter-fld-${item.title}`" |
||||
> |
||||
{{ item.title }} |
||||
</span> |
||||
</template> |
||||
</v-select> |
||||
<v-select |
||||
:key="'k' + i" |
||||
v-model="filter.comparison_op" |
||||
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" |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
hide-details |
||||
item-value="value" |
||||
@click.stop |
||||
@change="filterUpdateCondition(filter, i)" |
||||
> |
||||
<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" |
||||
/> |
||||
<v-checkbox |
||||
v-else-if="types[filter.field] === 'boolean'" |
||||
:key="i + '_7'" |
||||
v-model="filter.value" |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
@change="saveOrUpdate(filter, i)" |
||||
/> |
||||
<v-text-field |
||||
v-else |
||||
:key="i + '_7'" |
||||
v-model="filter.value" |
||||
solo |
||||
flat |
||||
hide-details |
||||
dense |
||||
class="caption nc-filter-value-select" |
||||
:disabled="filter.readOnly" |
||||
@click.stop |
||||
@input="saveOrUpdate(filter, i)" |
||||
/> |
||||
</template> |
||||
</v-select> |
||||
<v-text-field |
||||
v-if="filter.readOnly" |
||||
:key="i + '_5'" |
||||
v-model="filter.field" |
||||
class="caption " |
||||
:placeholder="$t('objects.field')" |
||||
solo |
||||
flat |
||||
dense |
||||
disabled |
||||
hide-details |
||||
@click.stop |
||||
> |
||||
<template #item="{item}"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
</template> |
||||
</v-text-field> |
||||
<v-select |
||||
v-else |
||||
:key="i + '_6'" |
||||
v-model="filter.field" |
||||
class="caption nc-filter-field-select" |
||||
:items="fieldList" |
||||
:placeholder="$t('objects.field')" |
||||
solo |
||||
flat |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
hide-details |
||||
@click.stop |
||||
> |
||||
<template #item="{item}"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
</template> |
||||
</v-select> |
||||
<v-select |
||||
:key="'k' + i" |
||||
v-model="filter.op" |
||||
class="flex-shrink-1 flex-grow-0 caption nc-filter-operation-select" |
||||
:items="opList" |
||||
:placeholder="$t('labels.operation')" |
||||
solo |
||||
flat |
||||
style="max-width:120px" |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
hide-details |
||||
@click.stop |
||||
> |
||||
<template #item="{item}"> |
||||
<span class="caption font-weight-regular">{{ item }}</span> |
||||
</template> |
||||
</v-select> |
||||
<span v-if="['is null', 'is not null'].includes(filter.op)" :key="'span' + i" /> |
||||
<v-checkbox |
||||
v-else-if="types[filter.field] === 'boolean'" |
||||
:key="i + '_7'" |
||||
v-model="filter.value" |
||||
dense |
||||
:disabled="filter.readOnly" |
||||
/> |
||||
<v-text-field |
||||
v-else |
||||
:key="i + '_7'" |
||||
v-model="filter.value" |
||||
solo |
||||
flat |
||||
hide-details |
||||
dense |
||||
class="caption nc-filter-value-select" |
||||
:disabled="filter.readOnly" |
||||
@click.stop |
||||
/> |
||||
</template> |
||||
</template> |
||||
</div> |
||||
|
||||
<!-- <v-list-item dense class="pt-2 list-btn"> |
||||
<v-btn @click.stop="addFilter" small class="elevation-0 grey--text"> |
||||
<v-icon small color="grey">mdi-plus</v-icon> |
||||
Add Filter |
||||
</v-btn> |
||||
</v-list-item>--> |
||||
|
||||
<v-btn small class="elevation-0 grey--text my-3" @click.stop="addFilter"> |
||||
<v-icon small color="grey"> |
||||
mdi-plus |
||||
</v-icon> |
||||
<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', |
||||
props: ['fieldList', 'value', 'meta'], |
||||
name: "ColumnFilter", |
||||
props: { |
||||
fieldList: [Array], |
||||
meta: Object, |
||||
nested: Boolean, |
||||
parentId: String, |
||||
viewId: String, |
||||
shared: Boolean, |
||||
webHook: Boolean, |
||||
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 not equal", |
||||
value: "neq", |
||||
}, |
||||
{ |
||||
text: "is like", |
||||
value: "like", |
||||
}, |
||||
{ |
||||
text: "is not like", |
||||
value: "nlike", |
||||
}, |
||||
{ |
||||
text: "is empty", |
||||
value: "empty", |
||||
ignoreVal: true, |
||||
}, |
||||
{ |
||||
text: "is not empty", |
||||
value: "notempty", |
||||
ignoreVal: true, |
||||
}, |
||||
{ |
||||
text: "is null", |
||||
value: "null", |
||||
ignoreVal: true, |
||||
}, |
||||
{ |
||||
text: "is not null", |
||||
value: "notnull", |
||||
ignoreVal: true, |
||||
}, |
||||
{ |
||||
text: ">", |
||||
value: "gt", |
||||
}, |
||||
{ |
||||
text: "<", |
||||
value: "lt", |
||||
}, |
||||
{ |
||||
text: ">=", |
||||
value: "gte", |
||||
}, |
||||
{ |
||||
text: "<=", |
||||
value: "lte", |
||||
}, |
||||
], |
||||
}), |
||||
computed: { |
||||
columnsById() { |
||||
return (this.columns || []).reduce((o, c) => ({ ...o, [c.id]: c }), {}); |
||||
}, |
||||
autoApply() { |
||||
return this.$store.state.windows.autoApplyFilter && !this.webHook; |
||||
}, |
||||
columns() { |
||||
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 {} } |
||||
if (!this.meta || !this.meta.columns || !this.meta.columns.length) { |
||||
return {}; |
||||
} |
||||
|
||||
return this.meta.columns.reduce((obj, col) => { |
||||
switch (col.uidt) { |
||||
case UITypes.Number: |
||||
case UITypes.Decimal: |
||||
obj[col._cn] = obj[col.cn] = 'number' |
||||
break |
||||
obj[col.title] = obj[col.column_name] = "number"; |
||||
break; |
||||
case UITypes.Checkbox: |
||||
obj[col._cn] = obj[col.cn] = '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(); |
||||
} |
||||
}, |
||||
filters: { |
||||
handler(v) { |
||||
this.$emit('input', v) |
||||
this.$emit( |
||||
"input", |
||||
v && |
||||
v.filter((f) => (f.fk_column_id && f.comparison_op) || f.is_group) |
||||
); |
||||
}, |
||||
deep: true |
||||
deep: true, |
||||
}, |
||||
value(v) { |
||||
this.filters = v || [] |
||||
} |
||||
}, |
||||
created() { |
||||
this.filters = this.value || [] |
||||
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 && |
||||
this.columnsById[f.fk_column_id].uidt === UITypes.Lookup |
||||
) { |
||||
return !["notempty", "empty", "notnull", "null"].includes(op.value); |
||||
} |
||||
return true; |
||||
}); |
||||
}, |
||||
async applyChanges(nested = false, { hookId } = {}) { |
||||
for (const [i, filter] of Object.entries(this.filters)) { |
||||
if (filter.status === "delete") { |
||||
if (this.hookId || hookId) { |
||||
await this.$api.dbTableFilter.delete(filter.id); |
||||
} else { |
||||
await this.$api.dbTableFilter.delete(filter.id); |
||||
} |
||||
} 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, |
||||
}); |
||||
} else { |
||||
await this.$api.dbTableFilter.update(filter.id, { |
||||
...filter, |
||||
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, |
||||
} |
||||
) |
||||
); |
||||
} else { |
||||
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); |
||||
} |
||||
} |
||||
this.loadFilter(); |
||||
if (!nested) { |
||||
this.$emit("updated"); |
||||
} |
||||
}, |
||||
async loadFilter() { |
||||
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); |
||||
} |
||||
if (this.hookId && this._isUIAllowed("filterSync")) { |
||||
filters = this.parentId |
||||
? await this.$api.dbTableFilter.childrenRead(this.parentId) |
||||
: await this.$api.dbTableWebhookFilter.read(this.hookId); |
||||
} |
||||
|
||||
this.filters = filters; |
||||
}, |
||||
addFilter() { |
||||
this.filters.push({ |
||||
field: '', |
||||
op: '', |
||||
value: '', |
||||
logicOp: 'and' |
||||
}) |
||||
this.filters = this.filters.slice() |
||||
} |
||||
} |
||||
} |
||||
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}`); |
||||
}, |
||||
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); |
||||
}, |
||||
filterUpdateCondition(filter, i) { |
||||
this.saveOrUpdate(filter, i); |
||||
this.$tele.emit( |
||||
`filter:condition:${filter.logical_op}:${filter.comparison_op}` |
||||
); |
||||
}, |
||||
async saveOrUpdate(filter, i) { |
||||
if (this.shared || !this._isUIAllowed("filterSync")) { |
||||
// this.$emit('input', this.filters.filter(f => f.fk_column_id && f.comparison_op)) |
||||
this.$emit("updated"); |
||||
} else if (!this.autoApply) { |
||||
filter.status = "update"; |
||||
} else if (filter.id) { |
||||
await this.$api.dbTableFilter.update(filter.id, { |
||||
...filter, |
||||
fk_parent_id: this.parentId, |
||||
}); |
||||
|
||||
this.$emit("updated"); |
||||
} else { |
||||
this.$set( |
||||
this.filters, |
||||
i, |
||||
await this.$api.dbTableFilter.create(this.viewId, { |
||||
...filter, |
||||
fk_parent_id: this.parentId, |
||||
}) |
||||
); |
||||
|
||||
this.$emit("updated"); |
||||
} |
||||
}, |
||||
async deleteFilter(filter, i) { |
||||
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"); |
||||
} else { |
||||
await this.$api.dbTableFilter.delete(filter.id); |
||||
await this.loadFilter(); |
||||
this.$emit("updated"); |
||||
} |
||||
} else { |
||||
this.filters.splice(i, 1); |
||||
this.$emit("updated"); |
||||
} |
||||
this.$tele.emit("filter:delete"); |
||||
}, |
||||
}, |
||||
}; |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.grid { |
||||
display: grid; |
||||
grid-template-columns:22px 80px auto 110px auto; |
||||
grid-template-columns: 22px 80px auto auto auto; |
||||
column-gap: 6px; |
||||
row-gap: 6px |
||||
row-gap: 6px; |
||||
} |
||||
</style> |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue