Browse Source

Merge pull request #8485 from nocodb/develop

pull/8486/head 0.207.1
github-actions[bot] 2 months ago committed by GitHub
parent
commit
b677943fa3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      .github/workflows/bats-test.yml
  2. 4
      .github/workflows/release-executables.yml
  3. 2
      .github/workflows/sync-to-develop.yml
  4. 2
      docker-compose/setup-script/noco.sh
  5. 2
      docker-compose/setup-script/tests/configure/monitor.bats
  6. 2
      docker-compose/setup-script/tests/configure/restart.bats
  7. 2
      docker-compose/setup-script/tests/configure/scale.bats
  8. 2
      docker-compose/setup-script/tests/configure/setup.sh
  9. 2
      docker-compose/setup-script/tests/configure/start.bats
  10. 2
      docker-compose/setup-script/tests/configure/stop.bats
  11. 2
      docker-compose/setup-script/tests/configure/upgrade.bats
  12. 2
      docker-compose/setup-script/tests/install/default.bats
  13. 2
      docker-compose/setup-script/tests/install/ip.bats
  14. 2
      docker-compose/setup-script/tests/install/redis.bats
  15. 2
      docker-compose/setup-script/tests/install/scale.bats
  16. 2
      docker-compose/setup-script/tests/install/setup.sh
  17. 2
      docker-compose/setup-script/tests/install/watchtower.bats
  18. 3
      packages/nc-gui/app.vue
  19. 8
      packages/nc-gui/assets/nc-icons/alert-triangle.svg
  20. 14
      packages/nc-gui/assets/nc-icons/audit.svg
  21. 5
      packages/nc-gui/assets/nc-icons/message-circle.svg
  22. 3
      packages/nc-gui/assets/style.scss
  23. 2
      packages/nc-gui/components/account/License.vue
  24. 2
      packages/nc-gui/components/account/ResetPassword.vue
  25. 1
      packages/nc-gui/components/account/SignupSettings.vue
  26. 1
      packages/nc-gui/components/account/Token.vue
  27. 18
      packages/nc-gui/components/account/UserList.vue
  28. 14
      packages/nc-gui/components/account/UsersModal.vue
  29. 2
      packages/nc-gui/components/api-client/Headers.vue
  30. 2
      packages/nc-gui/components/api-client/Params.vue
  31. 15
      packages/nc-gui/components/cell/Checkbox.vue
  32. 18
      packages/nc-gui/components/cell/Currency.vue
  33. 18
      packages/nc-gui/components/cell/DatePicker.vue
  34. 13
      packages/nc-gui/components/cell/DateTimePicker.vue
  35. 4
      packages/nc-gui/components/cell/Decimal.vue
  36. 15
      packages/nc-gui/components/cell/Duration.vue
  37. 11
      packages/nc-gui/components/cell/Email.vue
  38. 1
      packages/nc-gui/components/cell/Float.vue
  39. 2
      packages/nc-gui/components/cell/GeoData.vue
  40. 1
      packages/nc-gui/components/cell/Integer.vue
  41. 14
      packages/nc-gui/components/cell/Json.vue
  42. 33
      packages/nc-gui/components/cell/MultiSelect.vue
  43. 4
      packages/nc-gui/components/cell/Percent.vue
  44. 7
      packages/nc-gui/components/cell/PhoneNumber.vue
  45. 11
      packages/nc-gui/components/cell/Rating.vue
  46. 2
      packages/nc-gui/components/cell/ReadOnlyDateTimePicker.vue
  47. 2
      packages/nc-gui/components/cell/ReadOnlyUser.vue
  48. 33
      packages/nc-gui/components/cell/RichText.vue
  49. 24
      packages/nc-gui/components/cell/SingleSelect.vue
  50. 11
      packages/nc-gui/components/cell/Text.vue
  51. 18
      packages/nc-gui/components/cell/TextArea.vue
  52. 1
      packages/nc-gui/components/cell/TimePicker.vue
  53. 18
      packages/nc-gui/components/cell/Url.vue
  54. 23
      packages/nc-gui/components/cell/User.vue
  55. 1
      packages/nc-gui/components/cell/YearPicker.vue
  56. 1
      packages/nc-gui/components/cell/attachment/Carousel.vue
  57. 4
      packages/nc-gui/components/cell/attachment/Image.vue
  58. 3
      packages/nc-gui/components/cell/attachment/Modal.vue
  59. 2
      packages/nc-gui/components/cell/attachment/RenameFile.vue
  60. 119
      packages/nc-gui/components/cell/attachment/index.vue
  61. 5
      packages/nc-gui/components/cell/attachment/sort.ts
  62. 24
      packages/nc-gui/components/cell/attachment/utils.ts
  63. 2
      packages/nc-gui/components/cmd-footer/index.vue
  64. 14
      packages/nc-gui/components/cmd-k/index.vue
  65. 5
      packages/nc-gui/components/cmd-l/index.vue
  66. 19
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  67. 4
      packages/nc-gui/components/dashboard/Sidebar/TopSection/Header.vue
  68. 2
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  69. 1
      packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue
  70. 2
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  71. 152
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  72. 1
      packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue
  73. 1
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  74. 334
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  75. 18
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  76. 23
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  77. 20
      packages/nc-gui/components/dashboard/TreeView/index.vue
  78. 2
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  79. 1
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  80. 1
      packages/nc-gui/components/dashboard/settings/BaseAudit.vue
  81. 58
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  82. 2
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  83. 1
      packages/nc-gui/components/dashboard/settings/Misc.vue
  84. 12
      packages/nc-gui/components/dashboard/settings/Modal.vue
  85. 15
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  86. 1
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  87. 24
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  88. 24
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  89. 15
      packages/nc-gui/components/dlg/AirtableImport.vue
  90. 1
      packages/nc-gui/components/dlg/ColumnDuplicate.vue
  91. 135
      packages/nc-gui/components/dlg/InviteDlg.vue
  92. 2
      packages/nc-gui/components/dlg/KeyboardShortcuts.vue
  93. 1
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  94. 32
      packages/nc-gui/components/dlg/QuickImport.vue
  95. 1
      packages/nc-gui/components/dlg/SharedBaseDuplicate.vue
  96. 15
      packages/nc-gui/components/dlg/TableCreate.vue
  97. 1
      packages/nc-gui/components/dlg/TableDuplicate.vue
  98. 19
      packages/nc-gui/components/dlg/TableRename.vue
  99. 65
      packages/nc-gui/components/dlg/ViewCreate.vue
  100. 4
      packages/nc-gui/components/dlg/ViewDelete.vue
  101. Some files were not shown because too many files have changed in this diff Show More

12
.github/workflows/bats-test.yml

@ -15,25 +15,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install jq
run: |
sudo apt-get update
sudo apt-get install -y jq
- name: Prepare matrix for test files
id: set-matrix
run: |
BATS_FILES=$(find docker-compose/setup-script/tests -name '*.bats')
MATRIX_JSON=$(echo $BATS_FILES | jq -Rsc 'split("\n") | map(select(. != ""))')
echo "matrix=$MATRIX_JSON" >> $GITHUB_ENV
MATRIX_JSON=$(echo $BATS_FILES | tr -d '\n' | jq -Rsc 'split(" ")' | tr '"' "'")
echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
test:
needs: prepare
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
test: ${{fromJson(env.matrix)}}
test: ${{fromJson(needs.prepare.outputs.matrix)}}
steps:
- name: Checkout repository
uses: actions/checkout@v4

4
.github/workflows/release-executables.yml

@ -68,6 +68,10 @@ jobs:
- name : Install nocodb, other dependencies and build executables
run: |
cd ./scripts/pkg-executable
# downgrade sqlite3 to 5.1.6 until build related issues are resolved
# https://github.com/TryGhost/node-sqlite3/issues/1748
node ../downgradeSqlite.js
# Install nocodb version based on provided tag name
npm i -E nocodb@${{ github.event.inputs.tag || inputs.tag }}

2
.github/workflows/sync-to-develop.yml

@ -43,7 +43,7 @@ jobs:
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
revertSDK=true node scripts/upgradeNocodbSdk.js
pnpm bootstrap
pnpm --filter=nocodb-sdk install --no-frozen-lockfile && pnpm --filter=nocodb-sdk run build && pnpm --filter=nocodb --filter=nc-gui --filter=playwright install --no-frozen-lockfile
git add .
git diff-index --quiet HEAD || git commit -m "chore: update sdk path"
git push origin $BRANCH_NAME

2
docker-compose/setup-script/noco.sh

@ -14,7 +14,7 @@ ORANGE='\033[0;33m'
BOLD='\033[1m'
NC='\033[0m'
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
# ***************** GLOBAL VARIABLES END ***********************************
# ******************************************************************************

2
docker-compose/setup-script/tests/configure/monitor.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/configure/restart.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/configure/scale.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/configure/setup.sh

@ -1,7 +1,7 @@
#!/bin/bash
if [ -z "$NOCO_HOME" ]; then
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
fi
if [ -d "$NOCO_HOME" ]; then

2
docker-compose/setup-script/tests/configure/start.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/configure/stop.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/configure/upgrade.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/install/default.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/install/ip.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/install/redis.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/install/scale.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME

2
docker-compose/setup-script/tests/install/setup.sh

@ -1,7 +1,7 @@
#!/bin/bash
if [ -z "$NOCO_HOME" ]; then
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
fi
if [ -d "$NOCO_HOME" ]; then

2
docker-compose/setup-script/tests/install/watchtower.bats

@ -1,6 +1,6 @@
#!/usr/bin/env bats
NOCO_HOME="${HOME}/.nocodb"
NOCO_HOME="./nocodb"
export NOCO_HOME
setup() {

3
packages/nc-gui/app.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import ErrorBoundary from './components/nc/ErrorBoundary.vue'
import { applyNonSelectable, computed, isEeUI, isMac, useCommandPalette, useRouter, useTheme } from '#imports'
import type { CommandPaletteType } from '~/lib'
import type { CommandPaletteType } from '~/lib/types'
const router = useRouter()

8
packages/nc-gui/assets/nc-icons/alert-triangle.svg

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M6.86001 2.57332L1.21335 12C1.09693 12.2016 1.03533 12.4302 1.03467 12.663C1.03402 12.8958 1.09434 13.1247 1.20963 13.327C1.32492 13.5293 1.49116 13.6978 1.69182 13.8159C1.89247 13.934 2.12055 13.9974 2.35335 14H13.6467C13.8795 13.9974 14.1076 13.934 14.3082 13.8159C14.5089 13.6978 14.6751 13.5293 14.7904 13.327C14.9057 13.1247 14.966 12.8958 14.9654 12.663C14.9647 12.4302 14.9031 12.2016 14.7867 12L9.14001 2.57332C9.02117 2.37739 8.85383 2.2154 8.65414 2.10297C8.45446 1.99055 8.22917 1.93149 8.00001 1.93149C7.77086 1.93149 7.54557 1.99055 7.34588 2.10297C7.1462 2.2154 6.97886 2.37739 6.86001 2.57332V2.57332Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8 6V8.66667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8 11.3333H8.00667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

14
packages/nc-gui/assets/nc-icons/audit.svg

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M10.6667 2.66669H12C12.3536 2.66669 12.6928 2.80716 12.9428 3.05721C13.1929 3.30726 13.3333 3.6464 13.3333 4.00002V7M6.50002 14.6667H4.00001C3.64638 14.6667 3.30724 14.5262 3.0572 14.2762C2.80715 14.0261 2.66667 13.687 2.66667 13.3334V4.00002C2.66667 3.6464 2.80715 3.30726 3.0572 3.05721C3.30724 2.80716 3.64638 2.66669 4.00001 2.66669H5.33334"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M10 1.33331H5.99999C5.63181 1.33331 5.33333 1.63179 5.33333 1.99998V3.33331C5.33333 3.7015 5.63181 3.99998 5.99999 3.99998H10C10.3682 3.99998 10.6667 3.7015 10.6667 3.33331V1.99998C10.6667 1.63179 10.3682 1.33331 10 1.33331Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M10 14C11.6569 14 13 12.6569 13 11C13 9.34315 11.6569 8 10 8C8.34315 8 7 9.34315 7 11C7 12.6569 8.34315 14 10 14Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path
d="M13.5286 15.4714C13.7889 15.7318 14.2111 15.7318 14.4714 15.4714C14.7318 15.2111 14.7318 14.7889 14.4714 14.5286L13.5286 15.4714ZM13.4714 13.5286L13 13.0572L12.0572 14L12.5286 14.4714L13.4714 13.5286ZM14.4714 14.5286L13.4714 13.5286L12.5286 14.4714L13.5286 15.4714L14.4714 14.5286Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
packages/nc-gui/assets/nc-icons/message-circle.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path
d="M14.5 7.66669C14.5023 8.5466 14.2967 9.41461 13.9 10.2C13.4296 11.1412 12.7065 11.9328 11.8116 12.4862C10.9168 13.0396 9.8855 13.3329 8.83333 13.3334C7.95342 13.3356 7.08541 13.1301 6.3 12.7334L2.5 14L3.76667 10.2C3.36995 9.41461 3.16437 8.5466 3.16667 7.66669C3.16707 6.61452 3.46041 5.58325 4.01381 4.68839C4.56722 3.79352 5.35884 3.0704 6.3 2.60002C7.08541 2.20331 7.95342 1.99772 8.83333 2.00002H9.16667C10.5562 2.07668 11.8687 2.66319 12.8528 3.64726C13.8368 4.63132 14.4233 5.94379 14.5 7.33335V7.66669Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 737 B

3
packages/nc-gui/assets/style.scss

@ -11,6 +11,7 @@ body {
}
:root {
--toolbar-height: 2.25rem;
--topbar-height: 3.1rem;
--sidebar-bottom-height: 8.5rem;
--new-header-height: 3.5rem;
@ -515,7 +516,7 @@ a {
}
.nc-toolbar-btn {
@apply !shadow-none rounded hover:(ring-1 ring-gray-200 ring-opacity-100 bg-gray-100 !text-gray-800) focus:(ring-1 ring-gray-300 ring-opacity-100 !text-gray-800 bg-gray-100) text-gray-600 text-xs font-medium px-2 border-0;
@apply !shadow-none rounded hover:(bg-gray-50 !text-gray-800) focus:(!text-gray-800 bg-gray-50) text-gray-600 text-xs font-medium px-2 border-0;
}
.nc-toolbar-btn[disabled] {
@apply !text-gray-400 !cursor-not-allowed !hover:ring-0;

2
packages/nc-gui/components/account/License.vue

@ -1,7 +1,5 @@
<script lang="ts" setup>
import { useNuxtApp } from '#imports'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, useApi, useGlobal } from '#imports'
const { api, isLoading } = useApi()

2
packages/nc-gui/components/account/ResetPassword.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { iconMap, message, navigateTo, reactive, ref, useApi, useGlobal, useI18n } from '#imports'
const { api, error } = useApi({ useGlobalInstance: true })
const { t } = useI18n()

1
packages/nc-gui/components/account/SignupSettings.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, useApi } from '#imports'
const { api } = useApi()

1
packages/nc-gui/components/account/Token.vue

@ -2,7 +2,6 @@
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import type { ApiTokenType, RequestParams } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, isEeUI, ref, useApi, useCopy, useNuxtApp } from '#imports'
import { extractNextDefaultName } from '~/helpers/parsers/parserHelpers'
const { api, isLoading } = useApi()

18
packages/nc-gui/components/account/UserList.vue

@ -1,17 +1,6 @@
<script lang="ts" setup>
import { OrgUserRoles } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, UserType } from 'nocodb-sdk'
import type { User } from '#imports'
import {
extractSdkResponseErrorMsg,
iconMap,
useApi,
useCopy,
useDashboard,
useDebounceFn,
useNuxtApp,
useUserSorts,
} from '#imports'
const { api, isLoading } = useApi()
@ -181,7 +170,12 @@ const openDeleteModal = (user: UserType) => {
<div class="max-w-195 mx-auto h-full">
<div class="text-2xl text-left font-weight-bold mb-4" data-rec="true">{{ $t('title.userMgmt') }}</div>
<div class="py-2 flex gap-4 items-center justify-between">
<a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()">
<a-input
v-model:value="searchText"
class="!max-w-90 !rounded-md"
:placeholder="$t('title.searchMembers')"
@change="loadUsers()"
>
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>

14
packages/nc-gui/components/account/UsersModal.vue

@ -2,20 +2,6 @@
import type { VNodeRef } from '@vue/runtime-core'
import type { OrgUserReqType } from 'nocodb-sdk'
import { OrgUserRoles } from 'nocodb-sdk'
import type { User, Users } from '#imports'
import {
Form,
computed,
emailValidator,
extractSdkResponseErrorMsg,
iconMap,
message,
ref,
useCopy,
useDashboard,
useI18n,
useNuxtApp,
} from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
interface Props {

2
packages/nc-gui/components/api-client/Headers.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { iconMap, useVModel } from '#imports'
const props = defineProps<{
modelValue: any[]
}>()

2
packages/nc-gui/components/api-client/Params.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { iconMap, useVModel } from '#imports'
const props = defineProps<{
modelValue: any[]
}>()

15
packages/nc-gui/components/cell/Checkbox.vue

@ -1,19 +1,4 @@
<script setup lang="ts">
import {
ActiveCellInj,
ColumnInj,
EditColumnInj,
IsFormInj,
IsSurveyFormInj,
ReadonlyInj,
getMdiIcon,
inject,
parseProp,
rowHeightInPx,
useBase,
useSelectedCellKeyupListener,
} from '#imports'
interface Props {
// If the previous cell value was a text, the initial checkbox value is a string type
// otherwise it can be either a boolean, or a string representing a boolean, i.e '0' or '1'

18
packages/nc-gui/components/cell/Currency.vue

@ -1,20 +1,10 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
computed,
inject,
parseProp,
useVModel,
} from '#imports'
interface Props {
modelValue: number | null | undefined
placeholder?: string
hidePrefix?: boolean
}
const props = defineProps<Props>()
@ -104,7 +94,7 @@ onMounted(() => {
<template>
<div
v-if="isForm && !isEditColumn"
v-if="isForm && !isEditColumn && !hidePrefix"
class="nc-currency-code h-full !bg-gray-100 border-r border-gray-200 px-3 mr-1 flex items-center"
>
<span>
@ -118,7 +108,7 @@ onMounted(() => {
type="number"
class="nc-cell-field h-full border-none rounded-md py-1 outline-none focus:outline-none focus:ring-0"
:class="isForm && !isEditColumn ? 'flex flex-1' : 'w-full'"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
:disabled="readOnly"
@blur="onBlur"
@keydown.enter="onKeydownEnter"

18
packages/nc-gui/components/cell/DatePicker.vue

@ -1,24 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isDateMonthFormat, isSystemColumn } from 'nocodb-sdk'
import {
ActiveCellInj,
CellClickHookInj,
ColumnInj,
EditColumnInj,
EditModeInj,
ReadonlyInj,
computed,
inject,
onClickOutside,
onMounted,
onUnmounted,
parseProp,
ref,
useGlobal,
useI18n,
watch,
} from '#imports'
interface Props {
modelValue?: string | null

13
packages/nc-gui/components/cell/DateTimePicker.vue

@ -1,19 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { dateFormats, isSystemColumn, timeFormats } from 'nocodb-sdk'
import {
ActiveCellInj,
CellClickHookInj,
ColumnInj,
EditColumnInj,
IsFormInj,
ReadonlyInj,
inject,
parseProp,
ref,
useBase,
watch,
} from '#imports'
interface Props {
modelValue?: string | null

4
packages/nc-gui/components/cell/Decimal.vue

@ -1,12 +1,12 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type
// for sqlite, when we clear a cell or empty the cell, it returns ""
// otherwise, it is null type
modelValue?: number | null | string
placeholder?: string
}
interface Emits {
@ -102,7 +102,7 @@ watch(isExpandedFormOpen, () => {
class="nc-cell-field outline-none py-1 border-none rounded-md w-full h-full"
type="number"
:step="precision"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
style="letter-spacing: 0.06rem"
@blur="editEnabled = false"
@keydown.down.stop="onKeyDown"

15
packages/nc-gui/components/cell/Duration.vue

@ -1,20 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
computed,
convertDurationToSeconds,
convertMS2Duration,
durationOptions,
inject,
parseProp,
ref,
} from '#imports'
interface Props {
modelValue: number | string | null | undefined

11
packages/nc-gui/components/cell/Email.vue

@ -1,16 +1,5 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
computed,
inject,
useI18n,
validateEmail,
} from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
interface Props {

1
packages/nc-gui/components/cell/Float.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type

2
packages/nc-gui/components/cell/GeoData.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { GeoLocationType } from 'nocodb-sdk'
import { Modal as AModal, iconMap, latLongToJoinedString, useVModel } from '#imports'
import { Modal as AModal } from '#imports'
interface Props {
modelValue?: string | null

1
packages/nc-gui/components/cell/Integer.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, inject, useVModel } from '#imports'
interface Props {
// when we set a number, then it is number type

14
packages/nc-gui/components/cell/Json.vue

@ -1,19 +1,5 @@
<script setup lang="ts">
import NcModal from '../nc/Modal.vue'
import {
ActiveCellInj,
EditModeInj,
IsFormInj,
JsonExpandInj,
ReadonlyInj,
RowHeightInj,
computed,
inject,
ref,
useSelectedCellKeyupListener,
useVModel,
watch,
} from '#imports'
interface Props {
modelValue: string | Record<string, any> | undefined

33
packages/nc-gui/components/cell/MultiSelect.vue

@ -3,34 +3,7 @@ import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2'
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionType, SelectOptionsType } from 'nocodb-sdk'
import type { FormFieldsLimitOptionsType } from '~/lib'
import {
ActiveCellInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj,
RowHeightInj,
computed,
enumColor,
extractSdkResponseErrorMsg,
h,
iconMap,
inject,
isDrawerOrModalExist,
onMounted,
reactive,
ref,
rowHeightTruncateLines,
useBase,
useEventListener,
useMetas,
useRoles,
useSelectedCellKeyupListener,
watch,
} from '#imports'
import type { FormFieldsLimitOptionsType } from '~/lib/types'
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {
@ -221,7 +194,9 @@ watch(isOpen, (n, _o) => {
if (!n) searchVal.value = ''
if (editAllowed.value) {
if (n) {
if (!n) {
aselect.value?.$el?.querySelector('input')?.blur()
} else {
aselect.value?.$el?.querySelector('input')?.focus()
}
}

4
packages/nc-gui/components/cell/Percent.vue

@ -1,9 +1,9 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, ReadonlyInj, inject, useVModel } from '#imports'
interface Props {
modelValue?: number | string | null
placeholder?: string
}
const props = defineProps<Props>()
@ -145,7 +145,7 @@ const onTabPress = (e: KeyboardEvent) => {
v-model="vModel"
class="nc-cell-field w-full !border-none !outline-none focus:ring-0 py-1"
:type="inputType"
:placeholder="isEditColumn ? $t('labels.optional') : ''"
:placeholder="placeholder !== undefined ? placeholder : isEditColumn ? $t('labels.optional') : ''"
@blur="onBlur"
@focus="onFocus"
@keydown.down.stop

7
packages/nc-gui/components/cell/PhoneNumber.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import isMobilePhone from 'validator/lib/isMobilePhone'
import { EditColumnInj, EditModeInj, IsExpandedFormOpenInj, IsFormInj, IsSurveyFormInj, computed, inject } from '#imports'
interface Props {
modelValue: string | null | number | undefined
@ -23,7 +22,7 @@ const isEditColumn = inject(EditColumnInj, ref(false))
const column = inject(ColumnInj)!
const isSurveyForm = inject(IsSurveyFormInj, ref(false))
const isForm = inject(IsFormInj)!
const readOnly = inject(ReadonlyInj, ref(false))
@ -34,7 +33,7 @@ const vModel = computed({
get: () => value,
set: (val) => {
localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && isMobilePhone(val)) || !val || isSurveyForm.value) {
if (!parseProp(column.value.meta)?.validate || (val && isMobilePhone(val)) || !val || isForm.value) {
emit('update:modelValue', val)
}
},
@ -44,8 +43,6 @@ const validPhoneNumber = computed(() => vModel.value && isMobilePhone(vModel.val
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)!
const focus: VNodeRef = (el) =>
!isExpandedFormOpen.value && !isEditColumn.value && !isForm.value && (el as HTMLInputElement)?.focus()

11
packages/nc-gui/components/cell/Rating.vue

@ -1,15 +1,4 @@
<script setup lang="ts">
import {
ActiveCellInj,
ColumnInj,
IsExpandedFormOpenInj,
ReadonlyInj,
computed,
inject,
parseProp,
useSelectedCellKeyupListener,
} from '#imports'
interface Props {
modelValue?: number | null | undefined
}

2
packages/nc-gui/components/cell/ReadOnlyDateTimePicker.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { ActiveCellInj, EditModeInj, ReadonlyInj, provide, ref } from '#imports'
interface Props {
modelValue?: string | null
}

2
packages/nc-gui/components/cell/ReadOnlyUser.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { ActiveCellInj, EditModeInj, ReadonlyInj, provide, ref } from '#imports'
interface Props {
modelValue?: string | null
}

33
packages/nc-gui/components/cell/RichText.vue

@ -7,18 +7,9 @@ import { marked } from 'marked'
import { generateJSON } from '@tiptap/html'
import Underline from '@tiptap/extension-underline'
import Placeholder from '@tiptap/extension-placeholder'
import { TaskItem } from '@/helpers/dbTiptapExtensions/task-item'
import { Link } from '@/helpers/dbTiptapExtensions/links'
import { TaskItem } from '~/helpers/dbTiptapExtensions/task-item'
import { Link } from '~/helpers/dbTiptapExtensions/links'
import type { RichTextBubbleMenuOptions } from '#imports'
import {
IsExpandedFormOpenInj,
IsFormInj,
IsGridInj,
IsSurveyFormInj,
ReadonlyInj,
RowHeightInj,
rowHeightTruncateLines,
} from '#imports'
const props = withDefaults(
defineProps<{
@ -41,7 +32,7 @@ const props = withDefaults(
const emits = defineEmits(['update:value', 'focus', 'blur'])
const { isFormField, hiddenBubbleMenuOptions } = toRefs(props)
const { fullMode, isFormField, hiddenBubbleMenuOptions } = toRefs(props)
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
@ -228,16 +219,16 @@ if (isFormField.value) {
})
}
watch(editorDom, () => {
if (!editorDom.value) return
onMounted(() => {
if (fullMode.value || isFormField.value || isForm.value) {
setEditorContent(vModel.value, true)
setEditorContent(vModel.value, true)
if ((isForm.value && !isSurveyForm.value) || isFormField.value) return
// Focus editor after editor is mounted
setTimeout(() => {
editor.value?.chain().focus().run()
}, 50)
if (fullMode.value || isSurveyForm.value) {
nextTick(() => {
editor.value?.chain().focus().run()
})
}
}
})
useEventListener(

24
packages/nc-gui/components/cell/SingleSelect.vue

@ -3,29 +3,7 @@ import type { Select as AntSelect } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import tinycolor from 'tinycolor2'
import type { SelectOptionType } from 'nocodb-sdk'
import type { FormFieldsLimitOptionsType } from '~/lib'
import {
ActiveCellInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsFormInj,
IsKanbanInj,
IsSurveyFormInj,
ReadonlyInj,
computed,
enumColor,
extractSdkResponseErrorMsg,
iconMap,
inject,
isDrawerOrModalExist,
ref,
useBase,
useEventListener,
useRoles,
useSelectedCellKeyupListener,
watch,
} from '#imports'
import type { FormFieldsLimitOptionsType } from '~/lib/types'
interface Props {
modelValue?: string | undefined

11
packages/nc-gui/components/cell/Text.vue

@ -1,16 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
RowHeightInj,
inject,
ref,
useVModel,
} from '#imports'
interface Props {
modelValue?: string | null

18
packages/nc-gui/components/cell/TextArea.vue

@ -1,23 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
IsGridInj,
ReadonlyInj,
RowHeightInj,
computed,
iconMap,
inject,
onClickOutside,
ref,
useGlobal,
useVModel,
watch,
} from '#imports'
const props = defineProps<{
modelValue?: string | number

1
packages/nc-gui/components/cell/TimePicker.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { ActiveCellInj, EditColumnInj, IsFormInj, ReadonlyInj, inject, onClickOutside, useBase, watch } from '#imports'
interface Props {
modelValue?: string | null | undefined

18
packages/nc-gui/components/cell/Url.vue

@ -1,23 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import {
CellUrlDisableOverlayInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsExpandedFormOpenInj,
IsFormInj,
ReadonlyInj,
computed,
inject,
isValidURL,
message,
parseProp,
ref,
useCellUrlConfig,
useI18n,
watch,
} from '#imports'
interface Props {
modelValue?: string | null

23
packages/nc-gui/components/cell/User.vue

@ -4,28 +4,7 @@ import tinycolor from 'tinycolor2'
import { Checkbox, CheckboxGroup, Radio, RadioGroup } from 'ant-design-vue'
import type { Select as AntSelect } from 'ant-design-vue'
import type { UserFieldRecordType } from 'nocodb-sdk'
import type { FormFieldsLimitOptionsType } from '~/lib'
import {
ActiveCellInj,
CellClickHookInj,
ColumnInj,
EditColumnInj,
EditModeInj,
IsKanbanInj,
ReadonlyInj,
RowHeightInj,
computed,
h,
inject,
isDrawerOrModalExist,
onMounted,
ref,
rowHeightTruncateLines,
useEventListener,
useRoles,
useSelectedCellKeyupListener,
watch,
} from '#imports'
import type { FormFieldsLimitOptionsType } from '~/lib/types'
import MdiCloseCircle from '~icons/mdi/close-circle'
interface Props {

1
packages/nc-gui/components/cell/YearPicker.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { isSystemColumn } from 'nocodb-sdk'
import { ActiveCellInj, EditColumnInj, IsFormInj, ReadonlyInj, computed, inject, onClickOutside, ref, watch } from '#imports'
interface Props {
modelValue?: number | string | null

1
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { onKeyDown } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { computed, iconMap, isImage, ref, useAttachment, useEventListener } from '#imports'
const { selectedImage, visibleItems, downloadFile } = useAttachmentCell()!

4
packages/nc-gui/components/cell/attachment/Image.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { iconMap } from '#imports'
interface Props {
srcs: string[]
alt?: string
@ -16,7 +14,7 @@ const onError = () => index.value++
<template>
<LazyNuxtImg
v-if="index < props.srcs.length"
class="m-auto object-cover"
class="m-auto h-full max-h-full w-auto object-cover"
:src="props.srcs[index]"
:alt="props?.alt || ''"
placeholder

3
packages/nc-gui/components/cell/attachment/Modal.vue

@ -2,7 +2,6 @@
import { onKeyDown, useEventListener } from '@vueuse/core'
import { useAttachmentCell } from './utils'
import { useSortable } from './sort'
import { iconMap, isImage, ref, useAttachment, useDropZone, useRoles, watch } from '#imports'
const { isUIAllowed } = useRoles()
@ -170,7 +169,7 @@ const handleFileDelete = (i: number) => {
<LazyCellAttachmentImage
v-if="isImage(item.title, item.mimetype)"
:srcs="getPossibleAttachmentSrc(item)"
class="object-cover h-64 m-auto justify-center"
class="max-h-full h-64 m-auto justify-center"
@click.stop="onClick(item)"
/>

2
packages/nc-gui/components/cell/attachment/RenameFile.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { onKeyStroke, onMounted, reactive, ref, useI18n } from '#imports'
const props = defineProps<{
title: string
}>()

119
packages/nc-gui/components/cell/attachment/index.vue

@ -2,27 +2,6 @@
import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import {
ActiveCellInj,
CurrentCellInj,
DropZoneRef,
IsExpandedFormOpenInj,
IsGalleryInj,
IsGridInj,
IsKanbanInj,
IsSurveyFormInj,
RowHeightInj,
iconMap,
inject,
isImage,
ref,
useAttachment,
useDropZone,
useSelectedCellKeyupListener,
useSmartsheetRowStoreOrThrow,
useSmartsheetStoreOrThrow,
watch,
} from '#imports'
interface Props {
modelValue?: string | Record<string, any>[] | null
@ -75,6 +54,7 @@ const {
selectedImage,
isReadonly,
storedFiles,
removeFile,
} = useProvideAttachmentCell(updateModelValue)
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue, isReadonly)
@ -194,6 +174,26 @@ const keydownSpace = (e: KeyboardEvent) => {
e.stopPropagation()
}
}
const { isUIAllowed } = useRoles()
const isConfirmModalOpen = ref(false)
const filetoDelete = reactive({
title: '',
i: 0,
})
function onRemoveFileClick(title: any, i: number) {
isConfirmModalOpen.value = true
filetoDelete.i = i
filetoDelete.title = title
}
const handleFileDelete = (i: number) => {
removeFile(i)
isConfirmModalOpen.value = false
filetoDelete.i = 0
filetoDelete.title = ''
}
</script>
<template>
@ -276,14 +276,14 @@ const keydownSpace = (e: KeyboardEvent) => {
}"
>
<template v-for="(item, i) of visibleItems" :key="item.url || item.title">
<NcTooltip placement="bottom">
<NcTooltip placement="bottom" class="nc-attachment-item">
<template #title>
<div class="text-center w-full">{{ item.title }}</div>
</template>
<div v-if="isImage(item.title, item.mimetype ?? item.type)">
<div
class="nc-attachment flex items-center flex-col flex-wrap justify-center"
:class="{ 'ml-2': active }"
class="nc-attachment flex items-center flex-col flex-wrap justify-center flex-auto"
:class="{ 'ml-2': active, '!w-30': isForm || isExpandedForm }"
@click="() => onImageClick(item)"
>
<LazyCellAttachmentImage
@ -292,10 +292,9 @@ const keydownSpace = (e: KeyboardEvent) => {
:class="{
'h-5.5': !isGrid && (!rowHeight || rowHeight === 1),
'h-4.5': isGrid && (!rowHeight || rowHeight === 1),
'w-8.8': rowHeight === 1,
'h-8 w-12.8': rowHeight === 2,
'h-16.8 w-20.8': rowHeight === 4,
'h-20.8 !w-30': isForm || isExpandedForm || rowHeight === 6,
'h-8': rowHeight === 2,
'h-16.8': rowHeight === 4,
'h-20.8': rowHeight === 6 || isForm || isExpandedForm,
}"
:srcs="getPossibleAttachmentSrc(item)"
/>
@ -303,21 +302,38 @@ const keydownSpace = (e: KeyboardEvent) => {
</div>
<div
v-else
class="nc-attachment flex items-center justify-center"
:class="{ 'ml-2': active }"
class="nc-attachment flex items-center justify-center px-4"
:class="{
'h-5.5': !isGrid && (!rowHeight || rowHeight === 1),
'h-4.5': isGrid && (!rowHeight || rowHeight === 1),
'h-8': rowHeight === 2,
'h-16.8': rowHeight === 4,
'h-20.8 !w-30': rowHeight === 6 || isForm || isExpandedForm,
'ml-2': active,
}"
@click="openAttachment(item)"
>
<component :is="FileIcon(item.icon)" v-if="item.icon" />
<component :is="FileIcon(item.icon)" v-if="item.icon" :class="{ 'h-13 w-13': isForm || isExpandedForm }" />
<IcOutlineInsertDriveFile v-else />
<IcOutlineInsertDriveFile v-else :class="{ 'h-13 w-13': isForm || isExpandedForm }" />
</div>
<a-tooltip v-if="isForm || isExpandedForm">
<template #title> {{ $t('title.removeFile') }} </template>
<component
:is="iconMap.closeCircle"
v-if="isSharedForm || (isUIAllowed('dataEdit') && !isPublic)"
class="nc-attachment-remove"
@click.stop="onRemoveFileClick(item.title, i)"
/>
</a-tooltip>
</NcTooltip>
</template>
</div>
<div
v-if="active || (isForm && visibleItems.length)"
class="xs:hidden h-6 w-5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
class="xs:hidden h-6 w-5.5 group cursor-pointer flex gap-1 items-center active:(ring ring-accent ring-opacity-100) rounded border-none p-1 hover:(bg-primary bg-opacity-10) dark:(!bg-slate-500)"
>
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
@ -326,7 +342,7 @@ const keydownSpace = (e: KeyboardEvent) => {
<component
:is="iconMap.expand"
class="flex-none transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-[0.75rem]"
class="flex-none transform dark:(!text-white) group-hover:(!text-grey-800 scale-120) text-gray-500 text-sm"
@click.stop="onExpand"
/>
</NcTooltip>
@ -334,6 +350,27 @@ const keydownSpace = (e: KeyboardEvent) => {
</template>
<LazyCellAttachmentModal />
<LazyGeneralDeleteModal
v-if="isForm || isExpandedForm"
v-model:visible="isConfirmModalOpen"
entity-name="File"
:on-delete="() => handleFileDelete(filetoDelete.i)"
>
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="file" class="nc-view-icon"></GeneralIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ filetoDelete.title }}
</div>
</div>
</span>
</template>
</LazyGeneralDeleteModal>
</div>
</template>
@ -341,7 +378,7 @@ const keydownSpace = (e: KeyboardEvent) => {
.nc-cell {
.nc-attachment-cell {
.nc-attachment {
@apply min-h-5.5 min-w-[1.8rem] !ring-1 !ring-gray-300 !rounded;
@apply min-h-5.5 !ring-1 !ring-gray-300 !rounded;
}
.ghost,
@ -355,5 +392,17 @@ const keydownSpace = (e: KeyboardEvent) => {
}
}
}
.nc-attachment-item {
@apply relative;
.nc-attachment-remove {
@apply absolute right-0.8 top-0.8 rounded hidden p-0.5 bg-white text-lg leading-none;
box-shadow: 0px 0px 4px #bbb;
}
&:hover .nc-attachment-remove {
@apply block;
}
}
}
</style>

5
packages/nc-gui/components/cell/attachment/sort.ts

@ -2,7 +2,6 @@ import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { MaybeRef } from '@vueuse/core'
import { watchPostEffect } from '@vue/runtime-core'
import { unref } from '#imports'
export function useSortable(
element: MaybeRef<HTMLElement | undefined>,
@ -52,7 +51,9 @@ export function useSortable(
const _element = unref(element)
onCleanup(() => {
if (_element && sortable) sortable.destroy()
if (_element && sortable?.el) {
sortable.destroy()
}
})
if (_element && !unref(isReadonly)) initSortable(_element)

24
packages/nc-gui/components/cell/attachment/utils.ts

@ -3,30 +3,6 @@ import { populateUniqueFileName } from 'nocodb-sdk'
import DOMPurify from 'isomorphic-dompurify'
import { saveAs } from 'file-saver'
import RenameFile from './RenameFile.vue'
import {
ColumnInj,
EditModeInj,
IsFormInj,
IsPublicInj,
MetaInj,
NOCO,
ReadonlyInj,
computed,
extractImageSrcFromRawHtml,
inject,
isImage,
message,
parseProp,
ref,
storeToRefs,
useApi,
useAttachment,
useBase,
useFileDialog,
useI18n,
useInjectionState,
watch,
} from '#imports'
import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'

2
packages/nc-gui/components/cmd-footer/index.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { CommandPaletteType } from '~/lib'
import type { CommandPaletteType } from '~/lib/types'
defineProps<{
activeCmd: CommandPaletteType

14
packages/nc-gui/components/cmd-k/index.vue

@ -1,8 +1,7 @@
<script lang="ts" setup>
import { useMagicKeys, whenever } from '@vueuse/core'
import { commandScore } from './command-score'
import { type ComputedRef, type VNode, iconMap, onClickOutside, useCommandPalette } from '#imports'
import type { CommandPaletteType } from '~/lib'
import type { CommandPaletteType } from '~/lib/types'
interface CmdAction {
id: string
@ -74,6 +73,7 @@ const nestedScope = computed(() => {
id: parent,
label: parentEl?.title,
icon: parentEl?.icon,
iconColor: parent.startsWith('ws-') ? parentEl?.iconColor : null,
})
parent = parentEl?.parent || 'root'
}
@ -333,6 +333,9 @@ defineExpose({
v-if="el.icon && el.id.startsWith('ws')"
:workspace="{
id: el.id.split('-')[1],
meta: {
color: el.iconColor,
},
}"
hide-label
size="small"
@ -409,6 +412,9 @@ defineExpose({
v-if="act.icon && act.id.startsWith('ws')"
:workspace="{
id: act.id.split('-')[2],
meta: {
color: act?.iconColor,
},
}"
class="mr-2"
size="small"
@ -421,14 +427,14 @@ defineExpose({
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
'!text-maroon-500': act.icon === 'calendar',
'!text-maroon-500 w-4 h-4': act.icon === 'calendar',
}"
class="cmdk-action-icon"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />

5
packages/nc-gui/components/cmd-l/index.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { onKeyUp, useDebounceFn, useVModel } from '@vueuse/core'
import { iconMap, onClickOutside } from '#imports'
import type { CommandPaletteType } from '~/lib'
import type { CommandPaletteType } from '~/lib/types'
const props = defineProps<{
open: boolean
@ -207,7 +206,7 @@ onMounted(() => {
<div class="cmdk-action-content">
<div class="flex w-1/2 items-center">
<div class="flex gap-2">
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" class="mt-0.5" />
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" class="mt-0.5 w-4 !min-h-4" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.viewName }}

19
packages/nc-gui/components/dashboard/Sidebar/TopSection.vue

@ -47,24 +47,29 @@ const navigateToSettings = () => {
</div>
</template>
<template v-else-if="!isSharedBase">
<div class="xs:hidden flex flex-col p-1 gap-y-0.5 mt-0.25 mb-0.5 truncate">
<div class="xs:hidden flex flex-col p-1 mt-0.25 mb-0.5 truncate">
<DashboardSidebarTopSectionHeader />
<NcButton
v-if="isUIAllowed('workspaceSettings')"
v-e="['c:team:settings']"
type="text"
size="small"
class="nc-sidebar-top-button !xs:hidden"
size="xsmall"
class="nc-sidebar-top-button !xs:hidden my-0.5 !h-7"
data-testid="nc-sidebar-team-settings-btn"
:centered="false"
:class="{
'!text-brand-500 !bg-brand-50 !hover:bg-brand-50': isWorkspaceSettingsPageOpened,
'!hover:bg-gray-200': !isWorkspaceSettingsPageOpened,
'!text-brand-600 !bg-brand-50 !hover:bg-brand-50': isWorkspaceSettingsPageOpened,
'!hover:(bg-gray-200 text-gray-700)': !isWorkspaceSettingsPageOpened,
}"
@click="navigateToSettings"
>
<div class="flex items-center gap-2">
<div
class="flex items-center gap-2"
:class="{
'font-semibold': isWorkspaceSettingsPageOpened,
}"
>
<GeneralIcon icon="settings" class="!h-4" />
<div>{{ $t('title.teamAndSettings') }}</div>
</div>
@ -73,7 +78,7 @@ const navigateToSettings = () => {
v-model:is-open="isCreateProjectOpen"
modal
type="text"
class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden"
class="nc-sidebar-top-button !hover:(bg-gray-200 text-gray-700) !xs:hidden !h-7 my-0.5"
data-testid="nc-sidebar-create-base-btn"
>
<div class="gap-x-2 flex flex-row w-full items-center !font-normal">

4
packages/nc-gui/components/dashboard/Sidebar/TopSection/Header.vue

@ -6,8 +6,8 @@ const { commandPalette } = useCommandPalette()
<NcButton
v-e="['c:quick-actions']"
type="text"
size="small"
class="nc-sidebar-top-button w-full !hover:bg-gray-200 !rounded-md !xs:hidden"
size="xsmall"
class="nc-sidebar-top-button w-full !hover:bg-gray-200 !rounded-md !xs:hidden !h-7 my-0.5"
data-testid="nc-sidebar-search-btn"
:centered="false"
@click="commandPalette?.open()"

2
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { computed, navigateTo, onMounted, ref, storeToRefs, useGlobal, useSidebarStore, useUsers, watch } from '#imports'
const { user, signOut, appInfo } = useGlobal()
// So watcher in users store is triggered
useUsers()

1
packages/nc-gui/components/dashboard/TreeView/AddNewTableNode.vue

@ -4,7 +4,6 @@ import { storeToRefs } from 'pinia'
import { toRef } from '@vue/reactivity'
import { resolveComponent } from '@vue/runtime-core'
import { ref } from 'vue'
import { ProjectRoleInj, useDialog, useRoles } from '#imports'
const props = withDefaults(
defineProps<{

2
packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue

@ -158,7 +158,7 @@ async function onOpenModal({
<NcMenuItem data-testid="sidebar-view-create-calendar" @click="onOpenModal({ type: ViewTypes.CALENDAR })">
<div class="item">
<div class="item-inner">
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" />
<GeneralViewIcon :meta="{ type: ViewTypes.CALENDAR }" class="!w-4 !h-4" />
<div>{{ $t('objects.viewType.calendar') }}</div>
</div>

152
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -4,37 +4,6 @@ import { message } from 'ant-design-vue'
import { stringifyRolesObj } from 'nocodb-sdk'
import type { BaseType, SourceType, TableType } from 'nocodb-sdk'
import { LoadingOutlined } from '@ant-design/icons-vue'
import {
NcProjectType,
ProjectInj,
ProjectRoleInj,
ToggleDialogInj,
TreeViewInj,
computed,
extractSdkResponseErrorMsg,
h,
inject,
navigateTo,
navigateToBlankTargetOpenOption,
openLink,
ref,
resolveComponent,
storeToRefs,
useBase,
useBases,
useCopy,
useDialog,
useGlobal,
useI18n,
useMagicKeys,
useNuxtApp,
useRoles,
useRouter,
useTablesStore,
useTabs,
useToggle,
} from '#imports'
import type { NcProject } from '#imports'
const indicator = h(LoadingOutlined, {
class: '!text-gray-400',
@ -58,7 +27,9 @@ const basesStore = useBases()
const { isMobileMode } = useGlobal()
const { createProject: _createProject, updateProject, getProjectMetaInfo } = basesStore
const { api } = useApi()
const { createProject: _createProject, updateProject, getProjectMetaInfo, loadProject } = basesStore
const { bases } = storeToRefs(basesStore)
@ -80,6 +51,16 @@ const editMode = ref(false)
const tempTitle = ref('')
const sourceRenameHelpers = ref<
Record<
string,
{
editMode: boolean
tempTitle: string
}
>
>({})
const activeBaseId = ref('')
const isErdModalOpen = ref<Boolean>(false)
@ -133,6 +114,52 @@ const enableEditMode = () => {
})
}
const enableEditModeForSource = (sourceId: string) => {
const source = base.value.sources?.find((s) => s.id === sourceId)
if (!source?.id) return
sourceRenameHelpers.value[source.id] = {
editMode: true,
tempTitle: source.alias || '',
}
nextTick(() => {
const input: HTMLInputElement | null = document.querySelector(`[data-source-rename-input-id="${sourceId}"]`)
if (!input) return
input?.focus()
input?.select()
input?.scrollIntoView()
})
}
const updateSourceTitle = async (sourceId: string) => {
const source = base.value.sources?.find((s) => s.id === sourceId)
if (!source?.id || !sourceRenameHelpers.value[source.id]) return
if (sourceRenameHelpers.value[source.id].tempTitle) {
sourceRenameHelpers.value[source.id].tempTitle = sourceRenameHelpers.value[source.id].tempTitle.trim()
}
if (!sourceRenameHelpers.value[source.id].tempTitle) return
try {
await api.source.update(source.base_id, source.id, {
alias: sourceRenameHelpers.value[source.id].tempTitle,
})
await loadProject(source.base_id, true)
delete sourceRenameHelpers.value[source.id]
$e('a:source:rename')
refreshViewTabTitle?.()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
const updateProjectTitle = async () => {
if (tempTitle.value) {
tempTitle.value = tempTitle.value.trim()
@ -427,7 +454,7 @@ const onTableIdCopy = async () => {
:data-testid="`nc-sidebar-base-${base.title}`"
:data-base-id="base.id"
>
<div class="flex items-center gap-0.75 py-0.25 cursor-pointer" @contextmenu="setMenuContext('base', base)">
<div class="flex items-center gap-0.75 py-0.5 cursor-pointer" @contextmenu="setMenuContext('base', base)">
<div
ref="baseNodeRefs"
:class="{
@ -435,7 +462,7 @@ const onTableIdCopy = async () => {
'hover:bg-gray-200': !(activeProjectId === base.id && baseViewOpen),
}"
:data-testid="`nc-sidebar-base-title-${base.title}`"
class="nc-sidebar-node base-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1 pl-1.5"
class="nc-sidebar-node base-title-node h-7 flex-grow rounded-md group flex items-center w-full pr-1 pl-1.5"
>
<div class="flex items-center mr-1" @click="onProjectClick(base)">
<div class="flex items-center select-none w-6 h-full">
@ -460,7 +487,7 @@ const onTableIdCopy = async () => {
ref="input"
v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent flex-1 mr-4"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode }"
:class="activeProjectId === base.id && baseViewOpen ? '!text-brand-600 !font-semibold' : '!text-gray-700'"
@click.stop
@keyup.enter="updateProjectTitle"
@keyup.esc="updateProjectTitle"
@ -470,7 +497,7 @@ const onTableIdCopy = async () => {
v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none flex-1"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === base.id && baseViewOpen }"
:class="activeProjectId === base.id && baseViewOpen ? 'text-brand-600 font-semibold' : 'text-gray-700'"
show-on-truncate-only
@click="onProjectClick(base)"
>
@ -632,9 +659,9 @@ const onTableIdCopy = async () => {
@click="onProjectClick(base, true, true)"
>
<GeneralIcon
icon="chevronDown"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 rotate-270"
:class="{ '!rotate-180': base.isExpanded }"
icon="chevronRight"
class="group-hover:visible cursor-pointer transform transition-transform duration-200 text-[20px]"
:class="{ '!rotate-90': base.isExpanded }"
/>
</NcButton>
</template>
@ -686,7 +713,7 @@ const onTableIdCopy = async () => {
</template>
<a-collapse-panel :key="`collapse-${source.id}`">
<template #header>
<div class="nc-sidebar-node min-w-20 w-full h-full flex flex-row group py-0.25 pr-6.5 !mr-0">
<div class="nc-sidebar-node min-w-20 w-full h-full flex flex-row group py-0.5 pr-6.5 !mr-0">
<div
v-if="sourceIndex === 0"
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@ -703,12 +730,30 @@ const onTableIdCopy = async () => {
<GeneralBaseLogo
class="flex-none min-w-4 !xs:(min-w-4.25 w-4.25 text-sm) !text-gray-600 !group-hover:text-gray-800"
/>
<input
v-if="source.id && sourceRenameHelpers[source.id]?.editMode"
ref="input"
v-model="sourceRenameHelpers[source.id].tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent flex-1 mr-4"
:class="
activeProjectId === base.id && baseViewOpen ? '!text-brand-600 !font-semibold' : '!text-gray-700'
"
:data-source-rename-input-id="source.id"
@click.stop
@keydown.enter.stop.prevent
@keyup.enter="updateSourceTitle(source.id!)"
@keyup.esc="updateSourceTitle(source.id!)"
@blur="updateSourceTitle(source.id!)"
/>
<NcTooltip
v-else
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{
'text-black font-semibold': activeProjectId === base.id && baseViewOpen && !isMobileMode,
}"
:class="
activeProjectId === base.id && baseViewOpen && !isMobileMode
? 'text-brand-600 font-semibold'
: 'text-gray-700'
"
show-on-truncate-only
>
<template #title> {{ source.alias || '' }}</template>
@ -719,7 +764,13 @@ const onTableIdCopy = async () => {
<NcTooltip class="xs:(hidden) flex items-center mr-1">
<template #title>{{ $t('objects.externalDb') }}</template>
<GeneralIcon icon="info" class="flex-none text-gray-400 hover:text-gray-700 mr-1" />
<GeneralIcon
icon="info"
class="flex-none text-gray-400 hover:text-gray-700 nc-sidebar-node-btn"
:class="{
'!hidden': !isBasesOptionsOpen[source!.id!],
}"
/>
</NcTooltip>
</div>
<div class="flex flex-row items-center gap-x-0.25">
@ -747,6 +798,17 @@ const onTableIdCopy = async () => {
}"
@click="isBasesOptionsOpen[source!.id!] = false"
>
<NcMenuItem
v-if="isUIAllowed('baseRename')"
data-testid="nc-sidebar-source-rename"
@click="enableEditModeForSource(source.id!)"
>
<GeneralIcon icon="rename" class="group-hover:text-black" />
{{ $t('general.rename') }}
</NcMenuItem>
<NcDivider />
<!-- ERD View -->
<NcMenuItem key="erd" @click="openErdView(source)">
<div v-e="['c:source:erd']" class="flex gap-2 items-center">
@ -869,7 +931,7 @@ const onTableIdCopy = async () => {
<style lang="scss" scoped>
:deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 h-7.1 !xs:(pl-7 h-[3rem]) !pr-0.5 !py-0 hover:bg-gray-200 xs:(hover:bg-gray-50) !rounded-md;
@apply !mx-0 !pl-8.75 h-7 !xs:(pl-7 h-[3rem]) !pr-0.5 !py-0 hover:bg-gray-200 xs:(hover:bg-gray-50) !rounded-md;
.ant-collapse-arrow {
@apply !right-1 !xs:(flex-none border-1 border-gray-200 w-6.5 h-6.5 mr-1);

1
packages/nc-gui/components/dashboard/TreeView/ProjectWrapper.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { BaseType } from 'nocodb-sdk'
import { ProjectInj, ProjectRoleInj } from '#imports'
const props = withDefaults(
defineProps<{

1
packages/nc-gui/components/dashboard/TreeView/TableList.vue

@ -3,7 +3,6 @@ import type { BaseType, TableType } from 'nocodb-sdk'
import { storeToRefs } from 'pinia'
import Sortable from 'sortablejs'
import TableNode from './TableNode.vue'
import { toRef, useNuxtApp } from '#imports'
const props = withDefaults(
defineProps<{

334
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -4,8 +4,7 @@ import { toRef } from '@vue/reactivity'
import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia'
import { ProjectRoleInj, TreeViewInj, useCopy, useMagicKeys, useNuxtApp, useRoles, useTabs } from '#imports'
import type { SidebarTableNode } from '~/lib'
import type { SidebarTableNode } from '~/lib/types'
const props = withDefaults(
defineProps<{
@ -218,188 +217,177 @@ const deleteTable = () => {
:class="[`nc-base-tree-tbl nc-base-tree-tbl-${table.title?.replaceAll(' ', '')}`]"
:data-active="openedTableId === table.id"
>
<div
v-e="['a:table:open']"
class="table-context flex items-center gap-1 h-full nc-tree-item-inner nc-sidebar-node pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-13.5': sourceIndex !== 0,
'pl-7.5 xs:(pl-6)': sourceIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<GeneralLoader v-if="table.isViewsLoading" class="flex items-center w-6 h-full !text-gray-600" />
<div
v-else
v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon"
<div class="flex items-center py-0.5">
<div
v-e="['a:table:open']"
class="flex-none flex-1 table-context flex items-center gap-1 h-full nc-tree-item-inner nc-sidebar-node pr-0.75 mb-0.25 rounded-md h-7 w-full group cursor-pointer hover:bg-gray-200"
:class="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-13.5': sourceIndex !== 0,
'pl-7.5 xs:(pl-6)': sourceIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<div class="flex w-auto" :data-testid="`tree-view-table-draggable-handle-${table.title}`">
<GeneralLoader v-if="table.isViewsLoading" class="flex items-center w-6 h-full !text-gray-600" />
<div
v-else
v-e="['c:table:emoji-picker']"
class="flex items-center nc-table-icon"
:class="{
'pointer-events-none': !canUserEditEmote,
}"
@click.stop
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ $t('general.changeIcon') }}
</template>
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="w-4 text-sm"
:class="isTableOpened ? '!text-brand-600/85' : '!text-gray-600/75'"
/>
<MdiEye v-else class="flex w-5 text-sm" :class="isTableOpened ? '!text-brand-600' : '!text-gray-600'" />
</NcTooltip>
</template>
</LazyGeneralEmojiPicker>
</div>
</div>
</div>
<NcTooltip
class="nc-tbl-title nc-sidebar-node-title text-ellipsis overflow-hidden select-none !flex-1"
show-on-truncate-only
>
<template #title>{{ table.title }}</template>
<span
:class="isTableOpened ? 'text-brand-600 !font-medium' : 'text-gray-600'"
:data-testid="`nc-tbl-title-${table.title}`"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ table.title }}
</span>
</NcTooltip>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
<NcButton
v-e="['c:table:option']"
class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-700 hover:text-gray-800"
:class="{
'pointer-events-none': !canUserEditEmote,
'!opacity-100 !inline-block': isOptionsOpen,
}"
data-testid="nc-sidebar-table-context-menu"
type="text"
size="xxsmall"
@click.stop
>
<LazyGeneralEmojiPicker
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<template #default>
<NcTooltip class="flex" placement="topLeft" hide-on-click :disabled="!canUserEditEmote">
<template #title>
{{ $t('general.changeIcon') }}
</template>
<component
:is="iconMap.table"
v-if="table.type === 'table'"
class="w-4 text-sm"
:class="{
'!group-hover:text-gray-700': isUIAllowed('tableSort', { roles: baseRole }),
'!text-gray-700': openedTableId === table.id,
'!text-gray-600/75': openedTableId !== table.id,
}"
/>
<MdiEye
v-else
class="flex w-5 !text-gray-500 text-sm"
:class="{
'group-hover:text-gray-500': isUIAllowed('tableSort', { roles: baseRole }),
'!text-black': openedTableId === table.id,
}"
/>
</NcTooltip>
<MdiDotsHorizontal class="!text-current" />
</NcButton>
<template #overlay>
<NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.tableIdColon', {
tableId: table?.id,
})
}}
</div>
<NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
<GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
</NcButton>
</div>
</NcTooltip>
<template
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
</template>
</LazyGeneralEmojiPicker>
</div>
</div>
</div>
<NcTooltip
class="nc-tbl-title nc-sidebar-node-title text-ellipsis overflow-hidden select-none !flex-1"
show-on-truncate-only
>
<template #title>{{ table.title }}</template>
<span
:class="{
'text-black !font-medium': isTableOpened,
}"
:data-testid="`nc-tbl-title-${table.title}`"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ table.title }}
</span>
</NcTooltip>
</NcMenu>
</template>
</NcDropdown>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']" @click.stop>
<NcButton
v-e="['c:table:option']"
class="nc-sidebar-node-btn nc-tbl-context-menu text-gray-600"
:class="{
'!opacity-100 !inline-block': isOptionsOpen,
}"
data-testid="nc-sidebar-table-context-menu"
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
@click.stop
class="nc-sidebar-node-btn nc-sidebar-expand text-gray-700 hover:text-gray-800"
:class="{
'!opacity-100 !visible': isOptionsOpen,
}"
@click.stop="onExpand"
>
<MdiDotsHorizontal class="!text-gray-600" />
<GeneralIcon
icon="chevronRight"
class="nc-sidebar-source-node-btns cursor-pointer transform transition-transform duration-200 !text-current text-[20px]"
:class="{ '!rotate-90': isExpanded }"
/>
</NcButton>
<template #overlay>
<NcMenu class="!min-w-62.5" :data-testid="`sidebar-table-context-menu-list-${table.title}`">
<NcTooltip>
<template #title> {{ $t('labels.clickToCopyTableID') }} </template>
<div
class="flex items-center justify-between p-2 mx-1.5 rounded-md cursor-pointer hover:bg-gray-100 group"
@click.stop="onTableIdCopy"
>
<div class="flex text-xs font-bold text-gray-500 ml-1">
{{
$t('labels.tableIdColon', {
tableId: table?.id,
})
}}
</div>
<NcButton class="!group-hover:bg-gray-100" size="xsmall" type="secondary">
<GeneralIcon v-if="isTableIdCopied" class="max-h-4 min-w-4" icon="check" />
<GeneralIcon v-else class="max-h-4 min-w-4" else icon="copy" />
</NcButton>
</div>
</NcTooltip>
<template
v-if="
!isSharedBase &&
(isUIAllowed('tableRename', { roles: baseRole }) || isUIAllowed('tableDelete', { roles: baseRole }))
"
>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: baseRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@click="openRenameTableDialog(table, base.sources[sourceIndex].id)"
>
<div v-e="['c:table:rename']" class="flex gap-2 items-center">
<GeneralIcon icon="rename" class="text-gray-700" />
{{ $t('general.rename') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcMenuItem
v-if="
isUIAllowed('tableDuplicate') &&
base.sources?.[sourceIndex] &&
(base.sources[sourceIndex].is_meta || base.sources[sourceIndex].is_local)
"
:data-testid="`sidebar-table-duplicate-${table.title}`"
@click="duplicateTable(table)"
>
<div v-e="['c:table:duplicate']" class="flex gap-2 items-center">
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('tableDelete', { roles: baseRole })"
:data-testid="`sidebar-table-delete-${table.title}`"
class="!text-red-500 !hover:bg-red-50"
@click="deleteTable"
>
<div v-e="['c:table:delete']" class="flex gap-2 items-center">
<GeneralIcon icon="delete" />
{{ $t('general.delete') }} {{ $t('objects.table') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>
<NcButton
v-e="['c:table:toggle-expand']"
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand"
:class="{
'!opacity-100 !visible': isOptionsOpen,
}"
@click.stop="onExpand"
>
<GeneralIcon
icon="chevronDown"
class="nc-sidebar-source-node-btns cursor-pointer transform transition-transform duration-500 !text-gray-600 rotate-270"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
</div>
</div>
<DlgTableDelete
v-if="table.id && base?.id"

18
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -4,22 +4,6 @@ import { ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import {
extractSdkResponseErrorMsg,
isDefaultBase,
message,
onMounted,
parseProp,
ref,
resolveComponent,
useApi,
useCommandPalette,
useDialog,
useNuxtApp,
useUndoRedo,
viewTypeAlias,
watch,
} from '#imports'
interface Emits {
(
@ -91,6 +75,8 @@ function markItem(id: string) {
}
const isDefaultSource = computed(() => {
if (base.value?.sources?.length === 1) return true
const source = base.value?.sources?.find((b) => b.id === table.value.source_id)
if (!source) return false

23
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -2,18 +2,7 @@
import type { VNodeRef } from '@vue/runtime-core'
import type { TableType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity'
import {
IsLockedInj,
isDefaultBase as _isDefaultBase,
inject,
message,
onKeyStroke,
useDebounceFn,
useMagicKeys,
useNuxtApp,
useRoles,
useVModel,
} from '#imports'
import { isDefaultBase as _isDefaultBase } from '#imports'
interface Props {
view: ViewType
@ -64,6 +53,8 @@ provide(MetaInj, injectedTable)
const isLocked = inject(IsLockedInj, ref(false))
const isDefaultBase = computed(() => {
if (base.value?.sources?.length === 1) return true
const source = base.value?.sources?.find((b) => b.id === vModel.value.source_id)
if (!source) return false
@ -219,7 +210,7 @@ watch(isDropdownOpen, async () => {
<template>
<a-menu-item
class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
class="nc-sidebar-node !min-h-7 !max-h-7 !my-0.5 select-none group text-gray-700 !flex !items-center hover:(!bg-gray-200 !text-gray-700) cursor-pointer"
:class="{
'!pl-13.5 !xs:(pl-12)': isDefaultBase,
'!pl-19 ': !isDefaultBase,
@ -243,7 +234,7 @@ watch(isDropdownOpen, async () => {
@emoji-selected="emits('selectIcon', $event)"
>
<template #default>
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 !text-[16px]"></GeneralViewIcon>
</template>
</LazyGeneralEmojiPicker>
</div>
@ -254,7 +245,7 @@ watch(isDropdownOpen, async () => {
v-model:value="_title"
class="!bg-transparent !border-0 !ring-0 !outline-transparent !border-transparent !pl-0 !flex-1 mr-4"
:class="{
'font-medium': activeView?.id === vModel.id,
'font-medium !text-brand-600': activeView?.id === vModel.id,
}"
@blur="onRename"
@keydown.stop="onKeyDown($event)"
@ -264,7 +255,7 @@ watch(isDropdownOpen, async () => {
<div
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,
'font-medium text-brand-600': activeView?.id === vModel.id,
}"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>

20
packages/nc-gui/components/dashboard/TreeView/index.vue

@ -3,26 +3,6 @@ import Draggable from 'vuedraggable'
import type { TableType } from 'nocodb-sdk'
import ProjectWrapper from './ProjectWrapper.vue'
import {
TreeViewInj,
computed,
isDrawerOrModalExist,
isElementInvisible,
isMac,
reactive,
ref,
resolveComponent,
storeToRefs,
useBase,
useBases,
useDialog,
useGlobal,
useNuxtApp,
useRoles,
useRouter,
useTablesStore,
} from '#imports'
const { isUIAllowed } = useRoles()
const { $e } = useNuxtApp()

2
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { extractSdkResponseErrorMsg, iconMap, message, onMounted, useI18n, useNuxtApp } from '#imports'
const { t } = useI18n()
const { $api, $e } = useNuxtApp()

1
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -2,7 +2,6 @@
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import { ProjectIdInj, h, iconMap, onMounted, storeToRefs, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $api } = useNuxtApp()

1
packages/nc-gui/components/dashboard/settings/BaseAudit.vue

@ -2,7 +2,6 @@
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from 'nocodb-sdk'
import { h, iconMap, onMounted, storeToRefs, useBase, useGlobal, useI18n, useNuxtApp } from '#imports'
interface Props {
sourceId: string

58
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import type { SourceType } from 'nocodb-sdk'
import { ClientType, DataSourcesSubTab, isEeUI, storeToRefs, useBase, useCommandPalette, useNuxtApp } from '#imports'
import { ClientType } from '#imports'
interface Props {
state: string
@ -10,7 +10,7 @@ interface Props {
const props = defineProps<Props>()
const emits = defineEmits(['update:state', 'update:reload', 'awaken'])
const emits = defineEmits(['update:state', 'update:reload'])
const vState = useVModel(props, 'state', emits)
@ -20,7 +20,9 @@ const { $api, $e } = useNuxtApp()
const { t } = useI18n()
const { loadProject } = useBases()
const basesStore = useBases()
const { loadProject } = basesStore
const { isDataSourceLimitReached } = storeToRefs(basesStore)
const baseStore = useBase()
const { base } = storeToRefs(baseStore)
@ -37,10 +39,6 @@ const clientType = ref<ClientType>(ClientType.MYSQL)
const isReloading = ref(false)
const forceAwakened = ref(false)
const dataSourcesAwakened = ref(false)
const isDeleteBaseModalOpen = ref(false)
const toBeDeletedBase = ref<SourceType | undefined>()
@ -142,12 +140,6 @@ const moveBase = async (e: any) => {
}
}
const forceAwaken = () => {
forceAwakened.value = !forceAwakened.value
dataSourcesAwakened.value = forceAwakened.value
emits('awaken', forceAwakened.value)
}
watch(
projectPageTab,
() => {
@ -169,20 +161,6 @@ watch(
},
)
watch(
() => sources.value.length,
(l) => {
if (l > 1 && !forceAwakened.value) {
dataSourcesAwakened.value = false
emits('awaken', false)
} else {
dataSourcesAwakened.value = true
emits('awaken', true)
}
},
{ immediate: true },
)
watch(
vState,
async (newState) => {
@ -211,7 +189,7 @@ watch(
vState.value = DataSourcesSubTab.New
break
case DataSourcesSubTab.New:
if (sources.value.length > 1 && !forceAwakened.value) {
if (isDataSourceLimitReached.value) {
vState.value = ''
}
break
@ -292,7 +270,7 @@ const isEditBaseModalOpen = computed({
<div class="flex flex-col w-full overflow-auto">
<div class="flex flex-row w-full justify-end mt-6.5 mb-2">
<NcButton
v-if="dataSourcesAwakened"
v-if="!isDataSourceLimitReached"
size="large"
class="z-10 !px-2"
type="primary"
@ -312,7 +290,7 @@ const isEditBaseModalOpen = computed({
>
<div class="ds-table-head">
<div class="ds-table-row">
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-enabled cursor-pointer">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div>
<div class="ds-table-col ds-table-actions -ml-13">{{ $t('labels.actions') }}</div>
@ -324,7 +302,8 @@ const isEditBaseModalOpen = computed({
<template #header>
<div v-if="sources[0]" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<div class="flex items-center gap-1">
<div v-if="sources.length > 2" class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="sources[0].enabled">{{ $t('activity.hideInUI') }}</template>
@ -332,7 +311,8 @@ const isEditBaseModalOpen = computed({
</template>
<a-switch
:checked="sources[0].enabled ? true : false"
size="default"
class="cursor-pointer"
size="small"
@change="toggleBase(sources[0], $event)"
/>
</a-tooltip>
@ -433,18 +413,23 @@ const isEditBaseModalOpen = computed({
<template #item="{ element: source, index }">
<div v-if="index !== 0" class="ds-table-row border-gray-200">
<div class="ds-table-col ds-table-enabled">
<div class="flex items-center gap-1 cursor-pointer">
<div class="flex items-center gap-1">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<a-tooltip>
<template #title>
<template v-if="source.enabled">{{ $t('activity.hideInUI') }}</template>
<template v-else>{{ $t('activity.showInUI') }}</template>
</template>
<a-switch :checked="source.enabled ? true : false" @change="toggleBase(source, $event)" />
<a-switch
:checked="source.enabled ? true : false"
class="cursor-pointer"
size="small"
@change="toggleBase(source, $event)"
/>
</a-tooltip>
</div>
</div>
<div class="ds-table-col ds-table-name font-medium w-full">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div v-if="source.is_meta || source.is_local">-</div>
<span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
@ -452,7 +437,6 @@ const isEditBaseModalOpen = computed({
</div>
<div class="ds-table-col ds-table-type">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div class="flex items-center gap-2">
<GeneralBaseLogo :source-type="source.type" />
<span class="text-gray-700 capitalize">{{ source.type }}</span>
@ -661,6 +645,6 @@ const isEditBaseModalOpen = computed({
}
.ds-table-handle {
@apply cursor-pointer justify-self-start mr-2;
@apply cursor-pointer justify-self-start mr-2 w-[16px];
}
</style>

2
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -1,6 +1,4 @@
<script setup lang="ts">
import { Empty, extractSdkResponseErrorMsg, h, iconMap, message, storeToRefs, useBase, useI18n, useNuxtApp } from '#imports'
const props = defineProps<{
sourceId: string
}>()

1
packages/nc-gui/components/dashboard/settings/Misc.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface'
import { onMounted } from '@vue/runtime-core'
import { ProjectIdInj, storeToRefs, useBase, useGlobal, watch } from '#imports'
const { includeM2M, showNull } = useGlobal()

12
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import Misc from './Misc.vue'
import { DataSourcesSubTab, iconMap, useNuxtApp, useVModel, watch } from '#imports'
interface Props {
modelValue?: boolean
@ -46,9 +45,9 @@ const { $e } = useNuxtApp()
const { t } = useI18n()
const dataSourcesReload = ref(false)
const { isDataSourceLimitReached } = storeToRefs(useBases())
const dataSourcesAwakened = ref(false)
const dataSourcesReload = ref(false)
const tabsInfo: TabGroup = {
// teamAndAuth: {
@ -140,10 +139,6 @@ const selectedTab = computed(() => tabsInfo[selectedTabKeys.value[0]])
const selectedSubTabKeys = ref<string[]>([firstKeyOfObject(selectedTab.value.subTabs)])
const selectedSubTab = computed(() => selectedTab.value.subTabs[selectedSubTabKeys.value[0]])
const handleAwaken = (val: boolean) => {
dataSourcesAwakened.value = val
}
watch(
() => selectedTabKeys.value[0],
(newTabKey) => {
@ -222,7 +217,7 @@ watch(
</a-breadcrumb>
<div v-if="vDataState === ''" class="flex flex-row justify-end items-center w-full gap-1">
<a-button
v-if="dataSourcesAwakened"
v-if="!isDataSourceLimitReached"
type="primary"
class="self-start !rounded-md nc-btn-new-datasource"
@click="vDataState = DataSourcesSubTab.New"
@ -258,7 +253,6 @@ watch(
class="px-2 pb-2"
:data-testid="`nc-settings-subtab-${selectedSubTab.key}`"
:base-id="baseId"
@awaken="handleAwaken"
/>
<component
:is="selectedSubTab?.body"

15
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -1,20 +1,5 @@
<script setup lang="ts">
import { inject } from '@vue/runtime-core'
import {
Empty,
ProjectIdInj,
computed,
extractSdkResponseErrorMsg,
h,
iconMap,
message,
onMounted,
storeToRefs,
useBase,
useGlobal,
useI18n,
useNuxtApp,
} from '#imports'
type Role = 'editor' | 'commenter' | 'viewer'

1
packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { PluginTestReqType, PluginType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, iconMap, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
const { id } = defineProps<{
id: string

24
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -1,30 +1,14 @@
<script lang="ts" setup>
import { Form, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection } from '#imports'
import {
CertTypes,
type CertTypes,
ClientType,
ProjectIdInj,
type DefaultConnection,
type ProjectCreateForm,
type SQLiteConnection,
SSLUsage,
clientTypes as _clientTypes,
baseTitleValidator,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
generateUniqueName,
getDefaultConnectionConfig,
getTestDatabaseName,
iconMap,
nextTick,
onMounted,
readFile,
ref,
storeToRefs,
useApi,
useI18n,
useNuxtApp,
watch,
} from '#imports'
const props = defineProps<{ open: boolean; connectionType?: ClientType }>()

24
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -2,28 +2,16 @@
import type { SourceType } from 'nocodb-sdk'
import { Form, message } from 'ant-design-vue'
import type { SelectHandler } from 'ant-design-vue/es/vc-select/Select'
import type { DefaultConnection, ProjectCreateForm, SQLiteConnection, SnowflakeConnection } from '#imports'
import {
CertTypes,
type CertTypes,
ClientType,
ProjectIdInj,
type DatabricksConnection,
type DefaultConnection,
type ProjectCreateForm,
type SQLiteConnection,
SSLUsage,
type SnowflakeConnection,
clientTypes as _clientTypes,
baseTitleValidator,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
getDefaultConnectionConfig,
getTestDatabaseName,
iconMap,
onMounted,
readFile,
ref,
storeToRefs,
useApi,
useI18n,
useNuxtApp,
watch,
} from '#imports'
const props = defineProps<{

15
packages/nc-gui/components/dlg/AirtableImport.vue

@ -1,19 +1,6 @@
<script setup lang="ts">
import type { Card as AntCard } from 'ant-design-vue'
import {
Form,
JobStatus,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
iconMap,
message,
nextTick,
onMounted,
ref,
useNuxtApp,
watch,
} from '#imports'
import { JobStatus } from '#imports'
const { modelValue, baseId, sourceId } = defineProps<{
modelValue: boolean

1
packages/nc-gui/components/dlg/ColumnDuplicate.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean

135
packages/nc-gui/components/dlg/InviteDlg.vue

@ -1,8 +1,7 @@
<script lang="ts" setup>
import { ProjectRoles, type RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
import { extractEmail } from '../../helpers/parsers/parserHelpers'
const props = defineProps<{
modelValue: boolean
@ -54,6 +53,28 @@ const emailBadges = ref<Array<string>>([])
const allowedRoles = ref<[]>([])
const isLoading = ref(false)
const organizationStore = useOrganization()
const { listWorkspaces } = organizationStore
const { workspaces } = storeToRefs(organizationStore)
const searchQuery = ref('')
const workSpaceSelectList = computed<WorkspaceType[]>(() => {
return workspaces.value.filter((w: WorkspaceType) => w.title!.toLowerCase().includes(searchQuery.value.toLowerCase()))
})
const checked = reactive<{
[key: string]: boolean
}>({})
const selectedWorkspaces = computed<WorkspaceType[]>(() => {
return workSpaceSelectList.value.filter((ws: WorkspaceType) => checked[ws.id!])
})
const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
@ -225,10 +246,9 @@ const onPaste = (e: ClipboardEvent) => {
inviteData.email = ''
}
const workSpaces = ref<NcWorkspace[]>([])
const inviteCollaborator = async () => {
try {
isLoading.value = true
const payloadData = singleEmailValue.value || emailBadges.value.join(',')
if (!payloadData.includes(',')) {
const validationStatus = validateEmail(payloadData)
@ -246,7 +266,7 @@ const inviteCollaborator = async () => {
await inviteWsCollaborator(payloadData, inviteData.roles, props.workspaceId)
} else if (props.type === 'organization') {
// TODO: Add support for Bulk Workspace Invite
for (const workspace of workSpaces.value) {
for (const workspace of selectedWorkspaces.value) {
await inviteWsCollaborator(payloadData, inviteData.roles, workspace.id)
}
}
@ -259,32 +279,17 @@ const inviteCollaborator = async () => {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
singleEmailValue.value = ''
isLoading.value = false
}
}
const organizationStore = useOrganization()
const { listWorkspaces } = organizationStore
const { workspaces } = storeToRefs(organizationStore)
const workSpaceSelectList = computed(() => {
return workspaces.value.filter((w) => !workSpaces.value.find((ws) => ws.id === w.id))
})
const addToList = (workspaceId: string) => {
workSpaces.value.push(workspaces.value.find((w) => w.id === workspaceId)!)
}
const removeWorkspace = (workspaceId: string) => {
workSpaces.value = workSpaces.value.filter((w) => w.id !== workspaceId)
}
const isOrgSelectMenuOpen = ref(false)
onMounted(async () => {
if (props.type === 'organization') {
await listWorkspaces()
}
})
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles | WorkspaceUserRoles)
</script>
@ -298,7 +303,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
<div class="flex flex-row text-2xl font-bold items-center gap-x-2">
{{
type === 'organization'
? $t('labels.addMembersToOrganization')
@ -338,6 +343,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
id="email"
ref="focusRef"
v-model="inviteData.email"
:disabled="isLoading"
:placeholder="$t('activity.enterEmail')"
class="w-full min-w-36 outline-none px-2"
data-testid="email-input"
@ -361,23 +367,71 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
}}</span>
<template v-if="type === 'organization'">
<NcSelect :placeholder="$t('labels.selectWorkspace')" size="middle" @change="addToList">
<a-select-option v-for="workspace in workSpaceSelectList" :key="workspace.id" :value="workspace.id">
{{ workspace.title }}
</a-select-option>
</NcSelect>
<div class="flex flex-wrap gap-2">
<NcBadge v-for="workspace in workSpaces" :key="workspace.id">
<div class="px-2 flex gap-2 items-center py-1">
<GeneralWorkspaceIcon :workspace="workspace" hide-label size="small" />
<span class="text-gray-600">
{{ workspace.title }}
</span>
<component :is="iconMap.close" class="w-3 h-3" @click="removeWorkspace(workspace.id)" />
<NcDropdown v-model:visible="isOrgSelectMenuOpen">
<NcButton class="!justify-between" full-width size="medium" type="secondary">
<div
:class="{
'!text-gray-600': selectedWorkspaces.length > 0,
}"
class="flex text-gray-500 justify-between items-center w-full"
>
<NcTooltip class="!max-w-130 truncate" show-on-truncate-only>
<span class="">
{{
selectedWorkspaces.length > 0
? selectedWorkspaces.map((w) => w.title).join(', ')
: '-select workspaces to invite to-'
}}
</span>
<template #title>
{{
selectedWorkspaces.length > 0
? selectedWorkspaces.map((w) => w.title).join(', ')
: '-select workspaces to invite to-'
}}
</template>
</NcTooltip>
<component :is="iconMap.chevronDown" />
</div>
</NcBadge>
</div>
</NcButton>
<template #overlay>
<div class="py-2">
<div class="mx-2">
<a-input
v-model:value="searchQuery"
:class="{
'!border-brand-500': searchQuery.length > 0,
}"
class="!rounded-lg !h-8 !ring-0 !placeholder:text-gray-500 !border-gray-200 !px-4"
data-testid="nc-ws-search"
placeholder="Search workspace"
>
<template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
</div>
<div class="flex flex-col max-h-64 overflow-y-auto nc-scrollbar-md mt-2">
<div
v-for="ws in workSpaceSelectList"
:key="ws.id"
class="px-4 cursor-pointer hover:bg-gray-100 rounded-lg h-9.5 py-2 w-full flex gap-2"
@click="checked[ws.id!] = !checked[ws.id!]"
>
<div class="flex gap-2 capitalize items-center">
<GeneralWorkspaceIcon :hide-label="true" :workspace="ws" size="small" />
{{ ws.title }}
</div>
<div class="flex-1" />
<NcCheckbox v-model:checked="checked[ws.id!]" size="large" />
</div>
</div>
</div>
</template>
/>
</NcDropdown>
</template>
</div>
</div>
@ -385,7 +439,8 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
<div class="flex gap-2">
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton>
<NcButton
:disabled="isInviteButtonDisabled || emailValidation.isError"
:disabled="isInviteButtonDisabled || emailValidation.isError || isLoading"
:loading="isLoading"
size="medium"
type="primary"
class="nc-invite-btn"

2
packages/nc-gui/components/dlg/KeyboardShortcuts.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { isMac } from '#imports'
const { modelValue } = defineProps<{
modelValue: boolean
}>()

1
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import tinycolor from 'tinycolor2'
import type { BaseType } from 'nocodb-sdk'
import { isEeUI, useVModel } from '#imports'
const props = defineProps<{
modelValue: boolean

32
packages/nc-gui/components/dlg/QuickImport.vue

@ -3,38 +3,6 @@ import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam, UploadFile } from 'ant-design-vue'
import { Upload } from 'ant-design-vue'
import { toRaw, unref } from '@vue/runtime-core'
import type { ImportWorkerPayload, importFileList, streamImportFileList } from '#imports'
import {
BASE_FALLBACK_URL,
CSVTemplateAdapter,
ExcelTemplateAdapter,
ExcelUrlTemplateAdapter,
Form,
ImportSource,
ImportType,
ImportWorkerOperations,
ImportWorkerResponse,
JSONTemplateAdapter,
JSONUrlTemplateAdapter,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
iconMap,
importCsvUrlValidator,
importExcelUrlValidator,
importUrlValidator,
initWorker,
message,
reactive,
ref,
storeToRefs,
useBase,
useGlobal,
useI18n,
useNuxtApp,
useVModel,
} from '#imports'
// import worker script according to the doc of Vite
import importWorkerUrl from '~/workers/importWorker?worker&url'

1
packages/nc-gui/components/dlg/SharedBaseDuplicate.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { ProjectTypes } from 'nocodb-sdk'
import { isEeUI, useApi, useVModel, useWorkspace } from '#imports'
const props = defineProps<{
modelValue: boolean

15
packages/nc-gui/components/dlg/TableCreate.vue

@ -1,19 +1,4 @@
<script setup lang="ts">
import {
Form,
TabType,
computed,
nextTick,
onMounted,
ref,
useBase,
useTableNew,
useTablesStore,
useTabs,
useVModel,
validateTableName,
} from '#imports'
const props = defineProps<{
modelValue: boolean
sourceId: string

1
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { type LinkToAnotherRecordType, type TableType, UITypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useVModel } from '#imports'
import type { TabType } from '#imports'
const props = defineProps<{

19
packages/nc-gui/components/dlg/TableRename.vue

@ -1,25 +1,6 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import type { ComponentPublicInstance } from '@vue/runtime-core'
import {
Form,
computed,
extractSdkResponseErrorMsg,
message,
nextTick,
reactive,
storeToRefs,
useBase,
useCommandPalette,
useMetas,
useNuxtApp,
useTablesStore,
useTabs,
useUndoRedo,
useVModel,
validateTableName,
watchEffect,
} from '#imports'
interface Props {
modelValue?: boolean

65
packages/nc-gui/components/dlg/ViewCreate.vue

@ -4,7 +4,6 @@ import { capitalize } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import type { CalendarType, FormType, GalleryType, GridType, KanbanType, MapType, TableType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isSystemColumn } from 'nocodb-sdk'
import { computed, message, nextTick, onBeforeMount, reactive, ref, useApi, useI18n, useVModel, watch } from '#imports'
interface Props {
modelValue: boolean
@ -138,7 +137,6 @@ function init() {
if (repeatCount) {
form.title = `${form.title}-${repeatCount}`
}
if (selectedViewId.value) {
form.copy_from_id = selectedViewId?.value
}
@ -321,11 +319,11 @@ onMounted(async () => {
<template>
<NcModal
v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'small' : 'small'"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'"
>
<template #header>
<div class="flex w-full flex-row justify-between items-center">
<div class="flex gap-x-1.5 items-center">
<div class="flex font-bold text-base gap-x-3 items-center">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
@ -378,7 +376,7 @@ onMounted(async () => {
</div>
<a
v-if="!form.copy_from_id"
class="text-sm !text-gray-600 !hover:text-gray-600"
class="text-sm !text-gray-600 !font-default !hover:text-gray-600"
:href="`https://docs.nocodb.com/views/view-types/${typeAlias}`"
target="_blank"
>
@ -435,7 +433,12 @@ onMounted(async () => {
<span>
{{ $t('labels.organiseBy') }}
</span>
<NcSelect v-model:value="range.fk_from_column_id" :disabled="isMetaLoading" :loading="isMetaLoading">
<NcSelect
v-model:value="range.fk_from_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
class="nc-from-select"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions!].filter((f) => {
// If the fk_from_column_id of first range is Date, then all the other ranges should be Date
@ -445,14 +448,24 @@ onMounted(async () => {
return firstRange?.uidt === f.uidt
})"
:key="id"
class="w-40"
:value="option.value"
>
<div class="flex items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
<div class="flex w-full gap-2 justify-between items-center">
<div class="flex gap-2 items-center">
<SmartsheetHeaderIcon :column="option" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
</NcTooltip>
</div>
<div class="flex-1" />
<component
:is="iconMap.check"
v-if="option.value === range.fk_from_column_id"
id="nc-selected-item-icon"
class="text-primary min-w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
@ -474,8 +487,8 @@ onMounted(async () => {
v-model:value="range.fk_to_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
:placeholder="$t('placeholder.notSelected')"
class="!rounded-r-none nc-to-select"
:placeholder="$t('placenc-to-seleholder.notSelected')"
class="!rounded-r-none ct"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions].filter((f) => {
@ -524,10 +537,12 @@ onMounted(async () => {
</template>
</a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<GeneralIcon class="!text-5xl text-orange-500" icon="warning" />
<div class="text-gray-500">
<h2 class="font-semibold text-sm text-gray-800">Suitable fields not present</h2>
{{ errorMessages[form.type] }}
<div class="text-gray-500 flex gap-4">
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="warning" />
<div class="flex flex-col gap-1">
<h2 class="font-semibold text-sm mb-0 text-gray-800">Suitable fields not present</h2>
<span class="text-gray-500 font-default"> {{ errorMessages[form.type] }}</span>
</div>
</div>
</div>
@ -551,7 +566,7 @@ onMounted(async () => {
</NcModal>
</template>
<style lang="scss">
<style lang="scss" scoped>
.ant-form-item-required {
@apply !text-gray-800 font-medium;
&:before {
@ -559,7 +574,19 @@ onMounted(async () => {
}
}
.nc-from-select .ant-select-selector {
@apply !mr-2;
}
.nc-to-select .ant-select-selector {
@apply !rounded-r-none;
}
.ant-input {
@apply border-gray-200;
}
.ant-form-item {
@apply !mb-6;
}
</style>

4
packages/nc-gui/components/dlg/ViewDelete.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import { extractSdkResponseErrorMsg, message, useApi, useNuxtApp, useVModel } from '#imports'
interface Props {
modelValue: boolean
view?: Record<string, any>
@ -49,7 +47,7 @@ async function onDelete() {
<GeneralDeleteModal v-model:visible="vModel" :entity-name="$t('objects.view')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralViewIcon :meta="props.view" class="nc-view-icon"></GeneralViewIcon>
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 min-h-4"></GeneralViewIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save