diff --git a/packages/nc-gui/components/cell/ClampedText.vue b/packages/nc-gui/components/cell/ClampedText.vue index 4fe2b84733..000e61b4da 100644 --- a/packages/nc-gui/components/cell/ClampedText.vue +++ b/packages/nc-gui/components/cell/ClampedText.vue @@ -7,7 +7,7 @@ const props = defineProps<{ diff --git a/packages/nc-gui/components/monaco/Editor.vue b/packages/nc-gui/components/monaco/Editor.vue index 151193c3fb..ca9d62444d 100644 --- a/packages/nc-gui/components/monaco/Editor.vue +++ b/packages/nc-gui/components/monaco/Editor.vue @@ -51,25 +51,6 @@ const vModel = computed({ const isValid = ref(true) -/** - * Adding monaco editor to Vite - * - * @ts-expect-error */ -self.MonacoEnvironment = window.MonacoEnvironment = { - async getWorker(_: any, label: string) { - switch (label) { - case 'json': { - const workerBlob = new Blob([JsonWorker], { type: 'text/javascript' }) - return await initWorker(URL.createObjectURL(workerBlob)) - } - default: { - const workerBlob = new Blob([EditorWorker], { type: 'text/javascript' }) - return await initWorker(URL.createObjectURL(workerBlob)) - } - } - }, -} - const root = ref() let editor: MonacoEditor.IStandaloneCodeEditor @@ -98,16 +79,23 @@ onMounted(async () => { editor = monacoEditor.create(root.value, { model, + contextmenu: false, theme: 'vs', foldingStrategy: 'indentation', selectOnLineNumbers: true, + language: props.lang, scrollbar: { verticalScrollbarSize: 1, horizontalScrollbarSize: 1, }, + lineNumbers: 'off', tabSize: 2, automaticLayout: true, readOnly, + bracketPairColorization: { + enabled: true, + independentColorPoolPerBracketType: true, + }, minimap: { enabled: !hideMinimap, }, @@ -163,3 +151,5 @@ watch( + + diff --git a/packages/nc-gui/components/monaco/formula.ts b/packages/nc-gui/components/monaco/formula.ts new file mode 100644 index 0000000000..f82cb7eae9 --- /dev/null +++ b/packages/nc-gui/components/monaco/formula.ts @@ -0,0 +1,106 @@ +import type { Thenable, editor, languages } from 'monaco-editor' + +import { formulas } from 'nocodb-sdk' + +const formulaKeyWords = Object.keys(formulas) + +const theme: editor.IStandaloneThemeData = { + base: 'vs', + inherit: true, + rules: [ + { token: 'string', foreground: '#007b77', fontStyle: 'bold' }, + { token: 'keyword', foreground: '#00921d', fontStyle: 'bold' }, + { token: 'number', foreground: '#9c6200', fontStyle: 'bold' }, + { token: 'operator', foreground: '#000000' }, + { token: 'identifier', foreground: '#8541f9', fontStyle: 'bold' }, + { token: 'delimiter.parenthesis', foreground: '#333333', fontStyle: 'bold' }, + { token: 'delimiter.brace', foreground: '#8541f9', fontStyle: 'bold' }, + { token: 'invalid', foreground: '#000000' }, + ], + + colors: { + 'editor.foreground': '#000000', + 'editor.background': '#FFFFFF', + 'editorCursor.foreground': '#3366FF', + 'editor.selectionBackground': '#3366FF50', + 'focusBorder': '#ffffff', + }, +} + +const generateLanguageDefinition = (identifiers: string[]) => { + identifiers = identifiers.map((identifier) => `{${identifier}}`) + + const languageDefinition: languages.IMonarchLanguage | Thenable = { + defaultToken: 'invalid', + keywords: formulaKeyWords, + identifiers, + brackets: [ + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + { open: '{', close: '}', token: 'delimiter.brace' }, + ], + tokenizer: { + root: [ + [/"/, { token: 'string.quote', bracket: '@open', next: '@dblString' }], + [/'/, { token: 'string.quote', bracket: '@open', next: '@sglString' }], + [ + new RegExp(`\\{(${identifiers.join('|').replace(/[{}]/g, '')})\\}`), + { + cases: { + '@identifiers': 'identifier', + '@default': 'invalid', + }, + }, + ], + [ + /[a-zA-Z_]\w*/, + { + cases: { + '@keywords': 'keyword', + '@default': 'invalid', + }, + }, + ], + [/\d+/, 'number'], + [/[-+/*=<>!]+/, 'operator'], + [/[{}()\[\]]/, '@brackets'], + [/[ \t\r\n]+/, 'white'], + ], + + dblString: [ + [/[^\\"]+/, 'string'], + [/\\./, 'string.escape'], + [/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }], + ], + + sglString: [ + [/[^\\']+/, 'string'], + [/\\./, 'string.escape'], + [/'/, { token: 'string.quote', bracket: '@close', next: '@pop' }], + ], + }, + } + + return languageDefinition +} + +const languageConfiguration: languages.LanguageConfiguration = { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ], + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" }, + ], +} + +export default { + name: 'formula', + theme, + generateLanguageDefinition, + languageConfiguration, +} diff --git a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue index 21155fd778..254644e3d7 100644 --- a/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue +++ b/packages/nc-gui/components/smartsheet/column/EditOrAdd.vue @@ -356,12 +356,11 @@ const isFullUpdateAllowed = computed(() => { + - + @@ -675,6 +991,10 @@ watch(parsedTree, (value, oldValue) => { @apply !pl-0; } +:deep(.ant-form-item-control-input) { + @apply h-full; +} + :deep(.ant-tabs-content-holder) { @apply mt-4; } @@ -690,4 +1010,26 @@ watch(parsedTree, (value, oldValue) => { :deep(.ant-tabs-tab-btn) { @apply !mb-1; } + +.formula-monaco { + @apply rounded-md nc-scrollbar-md border-gray-200 border-1 overflow-y-auto overflow-x-hidden resize-y; + min-height: 100px; + height: 120px; + max-height: 250px; + + &:focus-within:not(.formula-error) { + box-shadow: 0 0 0 2px var(--ant-primary-color-outline); + } + + &:focus-within:not(.formula-success) { + box-shadow: 0 0 0 2px var(--ant-error-color-outline); + } + .view-line { + width: auto !important; + } +} + +.mono-font { + font-family: 'JetBrainsMono', monospace; +} diff --git a/packages/nc-gui/components/virtual-cell/Formula.vue b/packages/nc-gui/components/virtual-cell/Formula.vue index c5507b9598..35b541d969 100644 --- a/packages/nc-gui/components/virtual-cell/Formula.vue +++ b/packages/nc-gui/components/virtual-cell/Formula.vue @@ -21,6 +21,8 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ const isNumber = computed(() => (column.value.colOptions as any)?.parsed_tree?.dataType === FormulaDataTypes.NUMERIC) +const rowHeight = inject(RowHeightInj, ref(undefined)) + const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false)) const isGrid = inject(IsGridInj, ref(false)) @@ -47,7 +49,7 @@ const isGrid = inject(IsGridInj, ref(false))
-
{{ result }}
+
{{ $t('msg.info.computedFieldEditWarning') }} diff --git a/packages/nc-gui/package.json b/packages/nc-gui/package.json index bf9026a3d2..b7fe21a906 100644 --- a/packages/nc-gui/package.json +++ b/packages/nc-gui/package.json @@ -66,6 +66,7 @@ "emoji-mart-vue-fast": "^15.0.2", "file-saver": "^2.0.5", "fuse.js": "^6.6.2", + "html-entities": "^2.5.2", "httpsnippet": "^2.0.0", "inflection": "^1.13.4", "jsbarcode": "^3.11.6", @@ -75,7 +76,7 @@ "leaflet.markercluster": "^1.5.3", "locale-codes": "^1.3.1", "marked": "^4.3.0", - "monaco-editor": "^0.45.0", + "monaco-editor": "^0.50.0", "monaco-sql-languages": "^0.11.0", "nocodb-sdk": "workspace:^", "papaparse": "^5.4.1", diff --git a/packages/nc-gui/plugins/monaco.ts b/packages/nc-gui/plugins/monaco.ts new file mode 100644 index 0000000000..073dc1a67b --- /dev/null +++ b/packages/nc-gui/plugins/monaco.ts @@ -0,0 +1,23 @@ +import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker&inline' +import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker&inline' + +export default defineNuxtPlugin(() => { + /** + * Adding monaco editor to Vite + * + * @ts-expect-error */ + self.MonacoEnvironment = window.MonacoEnvironment = { + async getWorker(_: any, label: string) { + switch (label) { + case 'json': { + const workerBlob = new Blob([JsonWorker], { type: 'text/javascript' }) + return await initWorker(URL.createObjectURL(workerBlob)) + } + default: { + const workerBlob = new Blob([EditorWorker], { type: 'text/javascript' }) + return await initWorker(URL.createObjectURL(workerBlob)) + } + } + }, + } +}) diff --git a/packages/nc-gui/utils/urlUtils.ts b/packages/nc-gui/utils/urlUtils.ts index 9d13496b1c..0607a8cc17 100644 --- a/packages/nc-gui/utils/urlUtils.ts +++ b/packages/nc-gui/utils/urlUtils.ts @@ -1,35 +1,36 @@ import isURL from 'validator/lib/isURL' +import { decode } from 'html-entities' + export const replaceUrlsWithLink = (text: string): boolean | string => { if (!text) { return false } const rawText = text.toString() - // Create a temporary element to sanitize the string by encoding any HTML code - const tempEl = document.createElement('div') - tempEl.textContent = rawText - const sanitisedText = tempEl.innerHTML - const protocolRegex = /^(https?|ftp|mailto|file):\/\// + let isUrl - const out = sanitisedText.replace(/URI::\(([^)]*)\)(?: LABEL::\(([^)]*)\))?/g, (_, url, label) => { + const out = rawText.replace(/URI::\(([^)]*)\)(?: LABEL::\(([^)]*)\))?/g, (_, url, label) => { if (!url.trim() && !label) { return ' ' } const fullUrl = protocolRegex.test(url) ? url : url.trim() ? `http://${url}` : '' + + isUrl = isURL(fullUrl) + const anchorLabel = label || url || '' const a = document.createElement('a') a.textContent = anchorLabel - a.setAttribute('href', fullUrl) + a.setAttribute('href', decode(fullUrl)) a.setAttribute('class', ' nc-cell-field-link') a.setAttribute('target', '_blank') a.setAttribute('rel', 'noopener,noreferrer') return a.outerHTML }) - return out + return isUrl ? out : false } export const isValidURL = (str: string, extraProps?) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16f1da21e7..3d82101f35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,9 @@ importers: fuse.js: specifier: ^6.6.2 version: 6.6.2 + html-entities: + specifier: ^2.5.2 + version: 2.5.2 httpsnippet: specifier: ^2.0.0 version: 2.0.0(mkdirp@2.1.6) @@ -155,8 +158,8 @@ importers: specifier: ^4.3.0 version: 4.3.0 monaco-editor: - specifier: ^0.45.0 - version: 0.45.0 + specifier: ^0.50.0 + version: 0.50.0 monaco-sql-languages: specifier: ^0.11.0 version: 0.11.0 @@ -430,7 +433,7 @@ importers: version: 0.26.0(vue@3.4.34) vite-plugin-monaco-editor: specifier: ^1.1.0 - version: 1.1.0(monaco-editor@0.45.0) + version: 1.1.0(monaco-editor@0.50.0) vite-plugin-purge-icons: specifier: ^0.10.0 version: 0.10.0(vite@4.5.3) @@ -18136,6 +18139,10 @@ packages: resolution: {integrity: sha512-Cc/RSOGlojr7NDw1oXamUQenYBB0f/SISO8QWtRdZkDOmlO/hvbGZMjgyl+6+mh2PKPRrGXUKH4JhCU18LNS2g==} dev: false + /html-entities@2.5.2: + resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} + dev: false + /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true @@ -21362,8 +21369,8 @@ packages: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} dev: false - /monaco-editor@0.45.0: - resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==} + /monaco-editor@0.50.0: + resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==} /monaco-sql-languages@0.11.0: resolution: {integrity: sha512-OoTQVLT6ldBXPEbQPw43hOy+u16dO+HkY/rCADXPM5NfRBQ1m9+04NdaQ94WcF2R6MPeansxW8AveIdjEhxqfw==} @@ -25925,9 +25932,6 @@ packages: /sqlite3@5.1.6: resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} requiresBuild: true - peerDependenciesMeta: - node-gyp: - optional: true dependencies: '@mapbox/node-pre-gyp': 1.0.11 node-addon-api: 4.3.0 @@ -25942,9 +25946,6 @@ packages: /sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} requiresBuild: true - peerDependenciesMeta: - node-gyp: - optional: true dependencies: bindings: 1.5.0 node-addon-api: 7.0.0 @@ -28205,12 +28206,12 @@ packages: - supports-color dev: true - /vite-plugin-monaco-editor@1.1.0(monaco-editor@0.45.0): + /vite-plugin-monaco-editor@1.1.0(monaco-editor@0.50.0): resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==} peerDependencies: monaco-editor: '>=0.33.0' dependencies: - monaco-editor: 0.45.0 + monaco-editor: 0.50.0 dev: true /vite-plugin-purge-icons@0.10.0(vite@4.5.3): diff --git a/tests/playwright/pages/Dashboard/Grid/Column/index.ts b/tests/playwright/pages/Dashboard/Grid/Column/index.ts index 02eb2e1b84..2d3b1595e5 100644 --- a/tests/playwright/pages/Dashboard/Grid/Column/index.ts +++ b/tests/playwright/pages/Dashboard/Grid/Column/index.ts @@ -143,7 +143,7 @@ export class ColumnPageObject extends BasePage { await this.rootPage.locator('.ant-select-item').locator(`text="${timeFormat}"`).click(); break; case 'Formula': - await this.get().locator('.nc-formula-input').fill(formula); + await this.get().locator('.inputarea').fill(formula); break; case 'QrCode': await this.get().locator('.ant-select-single').nth(1).click(); @@ -222,7 +222,6 @@ export class ColumnPageObject extends BasePage { await this.ltarOption.addFilters(ltarFilters); } - if (custom) { // enable advance options await this.get().locator('.nc-ltar-relation-type >> .ant-radio').nth(1).dblclick(); @@ -387,9 +386,18 @@ export class ColumnPageObject extends BasePage { await this.defaultValueBtn().click(); switch (type) { - case 'Formula': - await this.get().locator('.nc-formula-input').fill(formula); + case 'Formula': { + const element = this.get().locator('.inputarea'); + await element.focus(); + + await this.rootPage.keyboard.press('Control+A'); + await this.rootPage.waitForTimeout(200); + + await this.rootPage.keyboard.press('Backspace'); + await this.rootPage.waitForTimeout(200); + await element.fill(formula); break; + } case 'Duration': await this.get().locator('.ant-select-single').nth(1).click(); await this.rootPage.locator(`.ant-select-item`).getByTestId(format).click(); diff --git a/tests/playwright/tests/db/columns/columnFormula.spec.ts b/tests/playwright/tests/db/columns/columnFormula.spec.ts index 29053a016b..4d5876f4d0 100644 --- a/tests/playwright/tests/db/columns/columnFormula.spec.ts +++ b/tests/playwright/tests/db/columns/columnFormula.spec.ts @@ -145,13 +145,7 @@ const formulaDataByDbType = (context: NcContext, index: number) => { }, { formula: 'URLENCODE({City})', - result: [ - 'A%20Corua%20(La%20Corua)', - 'Abha', - 'Abu%20Dhabi', - 'Acua', - 'Adana', - ], + result: ['A%20Corua%20(La%20Corua)', 'Abha', 'Abu%20Dhabi', 'Acua', 'Adana'], }, ]; else