Browse Source

Merge branch 'develop' into feat/attachments

pull/4931/head
Raju Udava 2 years ago committed by GitHub
parent
commit
d564dba568
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      .github/workflows/release-nightly-dev.yml
  2. 7
      packages/nc-gui/components/account/License.vue
  3. 210
      packages/nc-gui/components/smartsheet/Grid.vue
  4. 2
      packages/nc-gui/components/tabs/auth/UserManagement.vue
  5. 36
      packages/nc-gui/components/tabs/auth/user-management/FeedbackForm.vue
  6. 11
      packages/nc-gui/composables/useGlobal/actions.ts
  7. 5
      packages/nc-gui/composables/useGlobal/state.ts
  8. 9
      packages/nc-gui/composables/useGlobal/types.ts
  9. 7
      packages/nc-gui/composables/useMultiSelect/index.ts
  10. 2
      packages/nc-gui/pages/account/index.vue
  11. 41
      packages/nc-gui/plugins/feedbackForm.ts
  12. 16
      packages/nc-gui/utils/filterUtils.ts
  13. 2
      packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts
  14. 2
      packages/nc-gui/utils/urlUtils.ts
  15. 3
      packages/noco-docs/content/en/FAQs.md
  16. 15
      packages/noco-docs/content/en/engineering/builds-and-releases.md
  17. 138
      packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts
  18. 73
      packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts
  19. 75
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts
  20. 7
      packages/nocodb/src/lib/meta/api/utilApis.ts
  21. 51
      packages/nocodb/src/lib/models/Base.ts
  22. 93
      tests/playwright/tests/columnSingleSelect.spec.ts

14
.github/workflows/release-nightly-dev.yml

@ -54,13 +54,13 @@ jobs:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
# Build executables and publish to GitHub
# release-executables:
# needs: [set-tag, release-npm]
# uses: ./.github/workflows/release-timely-executables.yml
# with:
# tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
# secrets:
# NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
release-executables:
needs: [set-tag, release-npm]
uses: ./.github/workflows/release-timely-executables.yml
with:
tag: ${{ needs.set-tag.outputs.current_version }}-${{ needs.set-tag.outputs.nightly_build_tag }}
secrets:
NC_GITHUB_TOKEN: "${{ secrets.NC_GITHUB_TOKEN }}"
# Build docker image and push to docker hub
release-docker:

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

@ -1,11 +1,13 @@
<script lang="ts" setup>
import { useNuxtApp } from '#app'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, useApi } from '#imports'
import { extractSdkResponseErrorMsg, useApi,useGlobal } from '#imports'
const { api, isLoading } = useApi()
const {$e} = useNuxtApp()
const { $e } = useNuxtApp()
const { loadAppInfo } = useGlobal()
let key = $ref('')
@ -22,6 +24,7 @@ const setLicense = async () => {
try {
await api.orgLicense.set({ key: key })
message.success('License key updated')
await loadAppInfo();
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}

210
packages/nc-gui/components/smartsheet/Grid.vue

@ -173,128 +173,138 @@ const getContainerScrollForElement = (
return scroll
}
const { isCellSelected, activeCell, handleMouseDown, handleMouseOver, handleCellClick, clearSelectedRange, copyValue } =
useMultiSelect(
meta,
fields,
data,
$$(editEnabled),
isPkAvail,
clearCell,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (activePickerOrDropdownEl) {
const {
isCellSelected,
activeCell,
handleMouseDown,
handleMouseOver,
handleCellClick,
clearSelectedRange,
copyValue,
isCellActive,
} = useMultiSelect(
meta,
fields,
data,
$$(editEnabled),
isPkAvail,
clearCell,
makeEditable,
scrollToCell,
(e: KeyboardEvent) => {
// ignore navigating if picker(Date, Time, DateTime, Year)
// or single/multi select options is open
const activePickerOrDropdownEl = document.querySelector(
'.nc-picker-datetime.active,.nc-dropdown-single-select-cell.active,.nc-dropdown-multi-select-cell.active,.nc-picker-date.active,.nc-picker-year.active,.nc-picker-time.active',
)
if (activePickerOrDropdownEl) {
e.preventDefault()
return true
}
// skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) {
return true
}
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (isCellActive.value && !editEnabled && hasEditPermission) {
e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
return true
}
// skip keyboard event handling if there is a drawer / modal
if (isDrawerOrModalExist()) {
} else if (e.key === 'Escape') {
if (editEnabled) {
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (e.shiftKey) {
// add a line break for types like LongText / JSON
return true
}
if (editEnabled) {
editEnabled = false
return true
}
}
if (cmdOrCtrl) {
if (!isCellActive.value) return
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey
const altOrOptionKey = e.altKey
if (e.key === ' ') {
if (activeCell.row != null && !editEnabled && hasEditPermission) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
clearSelectedRange()
const row = data.value[activeCell.row]
expandForm(row)
activeCell.row = 0
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
}
} else if (e.key === 'Escape') {
if (editEnabled) {
case 'ArrowDown':
e.preventDefault()
clearSelectedRange()
activeCell.row = data.value.length - 1
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
}
} else if (e.key === 'Enter') {
if (e.shiftKey) {
// add a line break for types like LongText / JSON
case 'ArrowRight':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
}
if (editEnabled) {
case 'ArrowLeft':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = 0
scrollToCell?.()
editEnabled = false
return true
}
}
if (cmdOrCtrl) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
clearSelectedRange()
activeCell.row = 0
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowDown':
e.preventDefault()
clearSelectedRange()
activeCell.row = data.value.length - 1
activeCell.col = activeCell.col ?? 0
scrollToCell?.()
editEnabled = false
return true
case 'ArrowRight':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = fields.value?.length - 1
scrollToCell?.()
editEnabled = false
return true
case 'ArrowLeft':
e.preventDefault()
clearSelectedRange()
activeCell.row = activeCell.row ?? 0
activeCell.col = 0
scrollToCell?.()
editEnabled = false
return true
}
}
}
if (altOrOptionKey) {
switch (e.keyCode) {
case 82: {
// ALT + R
if (isAddingEmptyRowAllowed) {
$e('c:shortcut', { key: 'ALT + R' })
addEmptyRow()
}
break
if (altOrOptionKey) {
switch (e.keyCode) {
case 82: {
// ALT + R
if (isAddingEmptyRowAllowed) {
$e('c:shortcut', { key: 'ALT + R' })
addEmptyRow()
}
case 67: {
// ALT + C
if (isAddingColumnAllowed) {
$e('c:shortcut', { key: 'ALT + C' })
addColumnDropdown.value = true
}
break
break
}
case 67: {
// ALT + C
if (isAddingColumnAllowed) {
$e('c:shortcut', { key: 'ALT + C' })
addColumnDropdown.value = true
}
break
}
}
},
async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => {
const rowObj = data.value[ctx.row]
const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null
}
},
async (ctx: { row: number; col?: number; updatedColumnTitle?: string }) => {
const rowObj = data.value[ctx.row]
const columnObj = ctx.col !== undefined ? fields.value[ctx.col] : null
if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return
}
if (!ctx.updatedColumnTitle && isVirtualCol(columnObj)) {
return
}
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
)
// update/save cell value
await updateOrSaveRow(rowObj, ctx.updatedColumnTitle || columnObj.title)
},
)
function scrollToCell(row?: number | null, col?: number | null) {
row = row ?? activeCell.row

2
packages/nc-gui/components/tabs/auth/UserManagement.vue

@ -360,8 +360,6 @@ const isSuperAdmin = (user: { main_roles?: string }) => {
show-less-items
@change="loadUsers"
/>
<LazyTabsAuthUserManagementFeedbackForm />
</div>
</div>
</template>

36
packages/nc-gui/components/tabs/auth/user-management/FeedbackForm.vue

@ -1,36 +0,0 @@
<script setup lang="ts">
import { ref, useGlobal } from '#imports'
const { feedbackForm } = useGlobal()
const showForm = ref(false)
// todo: why this timeout?
setTimeout(() => (showForm.value = true), 60000)
</script>
<template>
<div v-if="showForm && feedbackForm && !feedbackForm.isHidden" class="nc-feedback-form-wrapper mt-6">
<MaterialSymbolsCloseRounded class="nc-close-icon" @click="feedbackForm.isHidden = true" />
<iframe :src="feedbackForm.url" width="100%" height="500" frameborder="0" marginheight="0" marginwidth="0">Loading </iframe>
</div>
<div v-else />
</template>
<style scoped lang="scss">
.nc-feedback-form-wrapper {
width: 100%;
position: relative;
iframe {
margin: 0 auto;
}
.nc-close-icon {
position: absolute;
top: 5px;
right: 10px;
}
}
</style>

11
packages/nc-gui/composables/useGlobal/actions.ts

@ -43,5 +43,14 @@ export function useGlobalActions(state: State): Actions {
})
}
return { signIn, signOut, refreshToken }
const loadAppInfo = async () => {
try {
const nuxtApp = useNuxtApp()
state.appInfo.value = await nuxtApp.$api.utils.appInfo()
} catch (e) {
console.error(e)
}
}
return { signIn, signOut, refreshToken, loadAppInfo }
}

5
packages/nc-gui/composables/useGlobal/state.ts

@ -59,11 +59,6 @@ export function useGlobalState(storageKey = 'nocodb-gui-v2'): State {
token: null,
lang: preferredLanguage,
darkMode: prefersDarkMode,
feedbackForm: {
url: 'https://docs.google.com/forms/d/e/1FAIpQLSeTlAfZjszgr53lArz3NvUEnJGOT9JtG9NAU5d0oQwunDS2Pw/viewform?embedded=true',
createdAt: new Date('2020-01-01T00:00:00.000Z').toISOString(),
isHidden: false,
},
filterAutoSave: true,
previewAs: null,
includeM2M: false,

9
packages/nc-gui/composables/useGlobal/types.ts

@ -4,13 +4,6 @@ import type { JwtPayload } from 'jwt-decode'
import type { Language, ProjectRole, User } from '~/lib'
import type { useCounter } from '#imports'
export interface FeedbackForm {
url: string
createdAt: string
isHidden: boolean
lastFormPollDate?: string
}
export interface AppInfo {
ncSiteUrl: string
authType: 'jwt' | 'none'
@ -34,7 +27,6 @@ export interface StoredState {
token: string | null
lang: keyof typeof Language
darkMode: boolean
feedbackForm: FeedbackForm
filterAutoSave: boolean
previewAs: ProjectRole | null
includeM2M: boolean
@ -63,6 +55,7 @@ export interface Actions {
signOut: () => void
signIn: (token: string) => void
refreshToken: () => void
loadAppInfo: () => void
}
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>

7
packages/nc-gui/composables/useMultiSelect/index.ts

@ -61,6 +61,10 @@ export function useMultiSelect(
const columnLength = $computed(() => unref(fields)?.length)
const isCellActive = computed(
() => !(activeCell.row === null || activeCell.col === null || isNaN(activeCell.row) || isNaN(activeCell.col)),
)
function makeActive(row: number, col: number) {
if (activeCell.row === row && activeCell.col === col) {
return
@ -171,7 +175,7 @@ export function useMultiSelect(
return true
}
if (activeCell.row === null || activeCell.col === null) {
if (!isCellActive.value) {
return
}
@ -367,6 +371,7 @@ export function useMultiSelect(
useEventListener(document, 'mouseup', handleMouseUp)
return {
isCellActive,
handleMouseDown,
handleMouseOver,
clearSelectedRange,

2
packages/nc-gui/pages/account/index.vue

@ -81,7 +81,7 @@ const openKeys = ref([/^\/account\/users/.test($route.fullPath) && 'users'])
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('license')"
key="apps"
key="license"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
@click="navigateTo('/account/license')"
>

41
packages/nc-gui/plugins/feedbackForm.ts

@ -1,41 +0,0 @@
import dayjs from 'dayjs'
import { defineNuxtPlugin, useGlobal, useNuxtApp } from '#imports'
const handleFeedbackForm = async () => {
let { feedbackForm: currentFeedbackForm } = $(useGlobal())
if (!currentFeedbackForm) return
const { $api } = useNuxtApp()
const isFirstTimePolling = !currentFeedbackForm.lastFormPollDate
const now = dayjs()
const lastFormPolledDate = dayjs(currentFeedbackForm.lastFormPollDate)
if (isFirstTimePolling || dayjs.duration(now.diff(lastFormPolledDate)).days() > 0) {
$api.instance
.get('/api/v1/feedback_form')
.then((response) => {
try {
const { data: feedbackForm } = response
if (!feedbackForm.error) {
const isFetchedFormDuplicate = currentFeedbackForm.url === feedbackForm.url
currentFeedbackForm = {
url: feedbackForm.url,
lastFormPollDate: now.toISOString(),
createdAt: feedbackForm.created_at,
isHidden: isFetchedFormDuplicate ? currentFeedbackForm.isHidden : false,
}
}
} catch (e) {}
})
.catch(() => {})
}
}
export default defineNuxtPlugin(() => {
handleFeedbackForm()
})

16
packages/nc-gui/utils/filterUtils.ts

@ -41,13 +41,13 @@ export const comparisonOpList: {
text: 'is empty',
value: 'empty',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Rating],
excludedTypes: [UITypes.Checkbox, UITypes.Rating, UITypes.Number, UITypes.Decimal, UITypes.Percent, UITypes.Currency],
},
{
text: 'is not empty',
value: 'notempty',
ignoreVal: true,
excludedTypes: [UITypes.Checkbox, UITypes.Rating],
excludedTypes: [UITypes.Checkbox, UITypes.Rating, UITypes.Number, UITypes.Decimal, UITypes.Percent, UITypes.Currency],
},
{
text: 'is null',
@ -67,7 +67,7 @@ export const comparisonOpList: {
{
text: 'contains any of',
value: 'anyof',
includedTypes: [UITypes.MultiSelect],
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: 'does not contain all of',
@ -77,26 +77,26 @@ export const comparisonOpList: {
{
text: 'does not contain any of',
value: 'nanyof',
includedTypes: [UITypes.MultiSelect],
includedTypes: [UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: '>',
value: 'gt',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: '<',
value: 'lt',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: '>=',
value: 'gte',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
},
{
text: '<=',
value: 'lte',
excludedTypes: [UITypes.Checkbox],
excludedTypes: [UITypes.Checkbox, UITypes.MultiSelect, UITypes.SingleSelect],
},
]

2
packages/nc-gui/utils/parsers/ExcelTemplateAdapter.ts

@ -257,6 +257,8 @@ export default class ExcelTemplateAdapter extends TemplateGenerator {
})
const cellObj = ws[cellId]
rowData[table.columns[i].column_name] = (cellObj && cellObj.w) || row[i]
} else if (table.columns[i].uidt === UITypes.SingleLineText || table.columns[i].uidt === UITypes.LongText) {
rowData[table.columns[i].column_name] = row[i] === null || row[i] === undefined ? null : `${row[i]}`
} else {
// TODO: do parsing if necessary based on type
rowData[table.columns[i].column_name] = row[i]

2
packages/nc-gui/utils/urlUtils.ts

@ -20,7 +20,7 @@ export const replaceUrlsWithLink = (text: string): boolean | string => {
}
export const isValidURL = (str: string) => {
return isURL(str)
return isURL(`${str}`)
}
export const openLink = (path: string, baseURL?: string, target = '_blank') => {

3
packages/noco-docs/content/en/FAQs.md

@ -45,7 +45,7 @@ PackageVersion: **0.97.0**
```
## What is available in free version ?
- [Detailed comparison of NocoDB's generous CE compared to others is here](https://github.com/orgs/nocodb/projects/13).
- NocoDB has just one version that is free & open source.
- In it you will notice advanced features are all available for free.
- ACL
@ -66,7 +66,6 @@ Auth Token is a JWT Token generated based on the logged-in user. By default, the
API Token is a Nano ID with a length of 40. If you are passing API Token, make sure that the header is called `xc-token`.
## Do you plan to have Enterprise Edition ?
For features that make sense for enterprises like below - yes
- SSO, SLA, Organisation wide reports and analytics,
- Advanced Audit or ACL,

15
packages/noco-docs/content/en/engineering/builds-and-releases.md

@ -11,7 +11,7 @@ There are 3 kinds of docker builds in NocoDB
- Release builds [nocodb/nocodb](https://hub.docker.com/r/nocodb/nocodb) : built during NocoDB release.
- Daily builds [nocodb/nocodb-daily](https://hub.docker.com/r/nocodb/nocodb-daily) : built every 6 hours from Develop branch.
- Daily builds [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely): built for every PR.
- Timely builds [nocodb/nocodb-timely](https://hub.docker.com/r/nocodb/nocodb-timely): built for every PR and manually triggered PRs.
Below is an overview of how to make these builds and what happens behind the scenes.
@ -118,7 +118,15 @@ Once the deployment is finished, there would be some new changes being pushed to
## Timely builds
### What are timely builds ?
NocoDB creates docker and binaries for each PR!
NocoDB has github actions which creates docker and binaries for each PR! And these can be found as a **comment on the last commit** of the PR.
Example shown below
- Go to a PR and click on the comment.
<img width="1111" alt="Screenshot 2023-01-23 at 15 46 36" src="https://user-images.githubusercontent.com/5435402/214083736-80062398-3712-430f-9865-86b110090c91.png">
- Click on the link to copy the docker image and run it locally.
<img width="1231" alt="Screenshot 2023-01-23 at 15 46 55" src="https://user-images.githubusercontent.com/5435402/214083755-945d9485-2b9e-4739-8408-068bdf4a84b7.png">
This is to
- reduce pull request cycle time
@ -135,7 +143,7 @@ The docker images will be built and pushed to Docker Hub (See [nocodb/nocodb-tim
![image](https://user-images.githubusercontent.com/35857179/175012097-240dab05-da93-4c4e-87c1-1c36fb1350bd.png)
## Executables or Binariess
## Executables or Binaries
Similarly, we provide a timely build for executables for non-docker users. The source code will be built, packaged as binary files, and pushed to Github (See [nocodb/nocodb-timely](https://github.com/nocodb/nocodb-timely/releases) for the full list).
@ -157,4 +165,3 @@ NocoDB creates Docker and Binaries for each PR.
This is to
- reduce pull request cycle time
- allow issue reporters / reviewers to verify the fix without setting up their local machines

138
packages/nocodb-sdk/src/lib/sqlUi/MssqlUi.ts

@ -163,122 +163,6 @@ export class MssqlUi {
};
}
// static getDefaultLengthForDatatype(type) {
// switch (type) {
// case "int":
// return 11;
// break;
// case "tinyint":
// return 1;
// break;
// case "smallint":
// return 5;
// break;
//
// case "mediumint":
// return 9;
// break;
// case "bigint":
// return 20;
// break;
// case "bit":
// return 64;
// break;
// case "boolean":
// return '';
// break;
// case "float":
// return 12;
// break;
// case "decimal":
// return 10;
// break;
// case "double":
// return 22;
// break;
// case "serial":
// return 20;
// break;
// case "date":
// return '';
// break;
// case "datetime":
// case "timestamp":
// return 6;
// break;
// case "time":
// return '';
// break;
// case "year":
// return '';
// break;
// case "char":
// return 255;
// break;
// case "varchar":
// return 45;
// break;
// case "nchar":
// return 255;
// break;
// case "text":
// return '';
// break;
// case "tinytext":
// return '';
// break;
// case "mediumtext":
// return '';
// break;
// case "longtext":
// return ''
// break;
// case "binary":
// return 255;
// break;
// case "varbinary":
// return 65500;
// break;
// case "blob":
// return '';
// break;
// case "tinyblob":
// return '';
// break;
// case "mediumblob":
// return '';
// break;
// case "longblob":
// return '';
// break;
// case "enum":
// return '\'a\',\'b\'';
// break;
// case "set":
// return '\'a\',\'b\'';
// break;
// case "geometry":
// return '';
// case "point":
// return '';
// case "linestring":
// return '';
// case "polygon":
// return '';
// case "multipoint":
// return '';
// case "multilinestring":
// return '';
// case "multipolygon":
// return '';
// case "json":
// return ''
// break;
//
// }
//
// }
static getDefaultLengthForDatatype(type) {
switch (type) {
case 'bigint':
@ -306,7 +190,7 @@ export class MssqlUi {
return '';
case 'decimal':
return '';
return 10;
case 'float':
return '';
@ -394,8 +278,12 @@ export class MssqlUi {
static getDefaultLengthIsDisabled(type) {
switch (type) {
case 'nvarchar':
case 'numeric':
case 'decimal':
return false;
case 'tinyint':
case 'float':
case 'int':
case 'bigint':
case 'binary':
case 'bit':
@ -404,17 +292,13 @@ export class MssqlUi {
case 'datetime':
case 'datetime2':
case 'datetimeoffset':
case 'decimal':
case 'float':
case 'geography':
case 'geometry':
case 'heirarchyid':
case 'image':
case 'int':
case 'money':
case 'nchar':
case 'ntext':
case 'numeric':
case 'real':
case 'json':
case 'smalldatetime':
@ -425,7 +309,6 @@ export class MssqlUi {
case 'text':
case 'time':
case 'timestamp':
case 'tinyint':
case 'uniqueidentifier':
case 'varbinary':
case 'xml':
@ -578,7 +461,7 @@ export class MssqlUi {
return '';
case 'decimal':
return '';
return '2';
case 'float':
return '';
@ -608,7 +491,7 @@ export class MssqlUi {
return '';
case 'numeric':
return '';
return '2';
case 'nvarchar':
return '';
@ -714,8 +597,8 @@ export class MssqlUi {
// }
}
static showScale(_columnObj) {
return false;
static showScale(columnObj) {
return columnObj.dt === 'decimal' || columnObj.dt === 'numeric';
}
static removeUnsigned(columns) {
@ -1261,6 +1144,7 @@ export class MssqlUi {
'real',
'smallint',
'tinyint',
'money',
];
case 'Percent':

73
packages/nocodb/src/lib/db/sql-client/lib/mssql/MssqlClient.ts

@ -2580,11 +2580,15 @@ class MssqlClient extends KnexClient {
const defaultValue = getDefaultValue(n);
const shouldSanitize = true;
const scaleAndPrecision =
!getDefaultLengthIsDisabled(n.dt) && n.dtxp
? `(${n.dtxp}${n.dtxs ? `,${n.dtxs}` : ''})`
: '';
if (change === 0) {
query = existingQuery ? ',' : '';
query += this.genQuery(`?? ${n.dt}`, [n.cn], shouldSanitize);
query += !getDefaultLengthIsDisabled(n.dt) && n.dtxp ? `(${n.dtxp})` : '';
query += scaleAndPrecision;
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' IDENTITY(1,1)' : ' ';
query += defaultValue
@ -2599,7 +2603,7 @@ class MssqlClient extends KnexClient {
}
} else if (change === 1) {
query += this.genQuery(` ADD ?? ${n.dt}`, [n.cn], shouldSanitize);
query += !getDefaultLengthIsDisabled(n.dt) && n.dtxp ? `(${n.dtxp})` : '';
query += scaleAndPrecision;
query += n.rqd ? ' NOT NULL' : ' NULL';
query += n.ai ? ' IDENTITY(1,1)' : ' ';
query += defaultValue
@ -2629,43 +2633,17 @@ class MssqlClient extends KnexClient {
);
}
if (n.dtxp !== o.dtxp && !['text'].includes(n.dt)) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? ${n.dt}(${n.dtxp});\n`,
[this.getTnPath(t), n.cn],
shouldSanitize
);
} else if (n.dt !== o.dt) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? TYPE ${n.dt};\n`,
[this.getTnPath(t), n.cn],
shouldSanitize
);
}
if (n.rqd !== o.rqd) {
if (
n.dtxp !== o.dtxp ||
n.dtxs !== o.dtxs ||
n.dt !== o.dt ||
n.rqd !== o.rqd
) {
query += this.genQuery(
`\nALTER TABLE ?? ALTER COLUMN ?? ${n.dt}`,
`\nALTER TABLE ?? ALTER COLUMN ?? ${n.dt}${scaleAndPrecision}`,
[this.getTnPath(t), n.cn],
shouldSanitize
);
if (
![
'int',
'bigint',
'bit',
'real',
'float',
'decimal',
'money',
'smallint',
'tinyint',
'geometry',
'datetime',
'text',
].includes(n.dt)
)
query += n.dtxp && n.dtxp != -1 ? `(${n.dtxp})` : '';
query += n.rqd ? ` NOT NULL;\n` : ` NULL;\n`;
}
@ -2787,43 +2765,32 @@ function getDefaultValue(n) {
function getDefaultLengthIsDisabled(type) {
switch (type) {
// case 'binary':
// case 'char':
// case 'sql_variant':
// case 'nvarchar':
// case 'nchar':
// case 'ntext':
// case 'varbinary':
// case 'sysname':
case 'bigint':
case 'bit':
case 'date':
case 'datetime':
case 'datetime2':
case 'datetimeoffset':
case 'decimal':
case 'float':
case 'geography':
case 'geometry':
case 'heirarchyid':
case 'image':
case 'int':
case 'money':
case 'numeric':
case 'real':
case 'json':
case 'smalldatetime':
case 'smallint':
case 'smallmoney':
case 'text':
case 'time':
case 'timestamp':
case 'int':
case 'tinyint':
case 'bigint':
case 'bit':
case 'smallint':
case 'float':
case 'uniqueidentifier':
case 'xml':
return true;
break;
default:
case 'decimal':
case 'numeric':
case 'varchar':
return false;
break;

75
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/conditionV2.ts

@ -303,50 +303,55 @@ const parseConditionV2 = async (
} else {
val = val.startsWith('%') || val.endsWith('%') ? val : `%${val}%`;
}
if (qb?.client?.config?.client === 'pg') {
qb = qb.whereRaw('??::text not ilike ?', [field, val]);
} else {
qb = qb.whereNot(field, 'like', val);
}
qb.where((nestedQb) => {
if (qb?.client?.config?.client === 'pg') {
nestedQb.whereRaw('??::text not ilike ?', [field, val]);
} else {
nestedQb.whereNot(field, 'like', val);
}
nestedQb.orWhereNull(field);
});
break;
case 'allof':
case 'anyof':
case 'nallof':
case 'nanyof':
// Condition for filter, without negation
const condition = (builder: Knex.QueryBuilder) => {
const items = val.split(',').map((item) => item.trim());
for (let i = 0; i < items.length; i++) {
let sql;
const bindings = [field, `%,${items[i]},%`];
if (qb?.client?.config?.client === 'pg') {
sql = "(',' || ??::text || ',') ilike ?";
} else if (qb?.client?.config?.client === 'sqlite3') {
sql = "(',' || ?? || ',') like ?";
} else {
sql = "CONCAT(',', ??, ',') like ?";
}
if (i === 0) {
builder = builder.whereRaw(sql, bindings);
} else {
if (
filter.comparison_op === 'allof' ||
filter.comparison_op === 'nallof'
) {
builder = builder.andWhereRaw(sql, bindings);
{
// Condition for filter, without negation
const condition = (builder: Knex.QueryBuilder) => {
const items = val.split(',').map((item) => item.trim());
for (let i = 0; i < items.length; i++) {
let sql;
const bindings = [field, `%,${items[i]},%`];
if (qb?.client?.config?.client === 'pg') {
sql = "(',' || ??::text || ',') ilike ?";
} else if (qb?.client?.config?.client === 'sqlite3') {
sql = "(',' || ?? || ',') like ?";
} else {
sql = "CONCAT(',', ??, ',') like ?";
}
if (i === 0) {
builder = builder.whereRaw(sql, bindings);
} else {
builder = builder.orWhereRaw(sql, bindings);
if (
filter.comparison_op === 'allof' ||
filter.comparison_op === 'nallof'
) {
builder = builder.andWhereRaw(sql, bindings);
} else {
builder = builder.orWhereRaw(sql, bindings);
}
}
}
};
if (
filter.comparison_op === 'allof' ||
filter.comparison_op === 'anyof'
) {
qb = qb.where(condition);
} else {
qb = qb.whereNot(condition).orWhereNull(field);
}
};
if (
filter.comparison_op === 'allof' ||
filter.comparison_op === 'anyof'
) {
qb = qb.where(condition);
} else {
qb = qb.whereNot(condition).orWhereNull(field);
}
break;
case 'gt':

7
packages/nocodb/src/lib/meta/api/utilApis.ts

@ -97,12 +97,6 @@ export async function versionInfo(_req: Request, res: Response) {
res.json(response);
}
export function feedbackFormGet(_req: Request, res: Response) {
feedbackForm()
.then((form) => res.json(form))
.catch((e) => res.json({ error: e.message }));
}
export async function appHealth(_: Request, res: Response) {
res.json({
message: 'OK',
@ -382,7 +376,6 @@ export default (router) => {
router.post('/api/v1/db/meta/axiosRequestMake', catchError(axiosRequestMake));
router.get('/api/v1/version', catchError(versionInfo));
router.get('/api/v1/health', catchError(appHealth));
router.get('/api/v1/feedback_form', catchError(feedbackFormGet));
router.post('/api/v1/url_to_config', catchError(urlToDbConfig));
router.get(
'/api/v1/aggregated-meta-info',

51
packages/nocodb/src/lib/models/Base.ts

@ -7,7 +7,7 @@ import {
MetaTable,
} from '../utils/globals';
import Model from './Model';
import { BaseType } from 'nocodb-sdk';
import { BaseType, UITypes } from 'nocodb-sdk';
import NocoCache from '../cache/NocoCache';
import CryptoJS from 'crypto-js';
import { extractProps } from '../meta/helpers/extractProps';
@ -277,6 +277,55 @@ export default class Base implements BaseType {
},
ncMeta
);
const relColumns = [];
const relRank = {
[UITypes.Lookup]: 1,
[UITypes.Rollup]: 2,
[UITypes.ForeignKey]: 3,
[UITypes.LinkToAnotherRecord]: 4,
}
for (const model of models) {
for (const col of await model.getColumns(ncMeta)) {
let colOptionTableName = null;
let cacheScopeName = null;
switch (col.uidt) {
case UITypes.Rollup:
colOptionTableName = MetaTable.COL_ROLLUP;
cacheScopeName = CacheScope.COL_ROLLUP;
break;
case UITypes.Lookup:
colOptionTableName = MetaTable.COL_LOOKUP;
cacheScopeName = CacheScope.COL_LOOKUP;
break;
case UITypes.ForeignKey:
case UITypes.LinkToAnotherRecord:
colOptionTableName = MetaTable.COL_RELATIONS;
cacheScopeName = CacheScope.COL_RELATION;
break;
}
if (colOptionTableName && cacheScopeName) {
relColumns.push({ col, colOptionTableName, cacheScopeName });
}
}
}
relColumns.sort((a, b) => {
return relRank[a.col.uidt] - relRank[b.col.uidt];
});
for (const relCol of relColumns) {
await ncMeta.metaDelete(null, null, relCol.colOptionTableName, {
fk_column_id: relCol.col.id,
});
await NocoCache.deepDel(
relCol.cacheScopeName,
`${relCol.cacheScopeName}:${relCol.col.id}`,
CacheDelDirection.CHILD_TO_PARENT
);
}
for (const model of models) {
await model.delete(ncMeta, true);
}

93
tests/playwright/tests/columnSingleSelect.spec.ts

@ -2,6 +2,7 @@ import { test } from '@playwright/test';
import { DashboardPage } from '../pages/Dashboard';
import { GridPage } from '../pages/Dashboard/Grid';
import setup from '../setup';
import { ToolbarPage } from '../pages/Dashboard/common/Toolbar';
test.describe('Single select', () => {
let dashboard: DashboardPage, grid: GridPage;
@ -93,3 +94,95 @@ test.describe('Single select', () => {
await grid.column.delete({ title: 'SingleSelect' });
});
});
test.describe('Single select - filter & sort', () => {
// Row values
// no values (row ❶)
// only Foo (row ❷)
// only Bar (row ❸)
// only Baz (row ❹)
// Example filters:
//
// where tags contains any of [Foo, Bar]
// result: rows 2,3
// where tags does not contain any of [Foo, Bar]
// result: rows 1,4
let dashboard: DashboardPage, grid: GridPage, toolbar: ToolbarPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page });
dashboard = new DashboardPage(page, context.project);
toolbar = dashboard.grid.toolbar;
grid = dashboard.grid;
await dashboard.treeView.createTable({ title: 'sheet1' });
await grid.column.create({ title: 'SingleSelect', type: 'SingleSelect' });
await grid.column.selectOption.addOptions({
columnTitle: 'SingleSelect',
options: ['foo', 'bar', 'baz'],
});
await grid.addNewRow({ index: 0, value: '1' });
await grid.addNewRow({ index: 1, value: '2' });
await grid.addNewRow({ index: 2, value: '3' });
await grid.addNewRow({ index: 3, value: '4' });
await grid.cell.selectOption.select({ index: 1, columnHeader: 'SingleSelect', option: 'foo', multiSelect: false });
await grid.cell.selectOption.select({ index: 2, columnHeader: 'SingleSelect', option: 'bar', multiSelect: false });
await grid.cell.selectOption.select({ index: 3, columnHeader: 'SingleSelect', option: 'baz', multiSelect: false });
});
// define validateRowArray function
async function validateRowArray(value: string[]) {
const length = value.length;
for (let i = 0; i < length; i++) {
await dashboard.grid.cell.verify({
index: i,
columnHeader: 'Title',
value: value[i],
});
}
}
async function verifyFilter(param: { opType: string; value?: string; result: string[] }) {
await toolbar.clickFilter();
await toolbar.filter.add({
columnTitle: 'SingleSelect',
opType: param.opType,
value: param.value,
isLocallySaved: false,
});
await toolbar.clickFilter();
// verify filtered rows
await validateRowArray(param.result);
// Reset filter
await toolbar.filter.reset();
}
test('Select and clear options and rename options', async () => {
await verifyFilter({ opType: 'contains any of', value: 'foo,bar', result: ['2', '3'] });
await verifyFilter({ opType: 'does not contain any of', value: 'foo,bar', result: ['1', '4'] });
// Sort column
await toolbar.sort.add({
columnTitle: 'SingleSelect',
isAscending: true,
isLocallySaved: false,
});
await validateRowArray(['1', '3', '4', '2']);
await toolbar.sort.reset();
// sort descending & validate
await toolbar.sort.add({
columnTitle: 'SingleSelect',
isAscending: false,
isLocallySaved: false,
});
await validateRowArray(['2', '4', '3', '1']);
await toolbar.sort.reset();
});
});

Loading…
Cancel
Save