Browse Source

Merge branch 'develop' into fix/expanded-record-ux

pull/7298/head
Ramesh Mane 11 months ago committed by GitHub
parent
commit
530978586e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/nc-gui/components/smartsheet/Toolbar.vue
  2. 16
      packages/nc-gui/components/virtual-cell/Formula.vue
  3. 2
      packages/nc-gui/composables/useViewData.ts
  4. 2
      packages/noco-docs/docs/065.table-details/_category_.json
  5. 137
      packages/noco-docs/docs/070.fields/040.field-types/010.text-based/025.rich-text.md
  6. 30
      packages/noco-docs/docs/070.fields/040.field-types/080.user-based/010.user.md
  7. 5
      packages/noco-docs/docs/070.fields/040.field-types/080.user-based/_category_.json
  8. BIN
      packages/noco-docs/static/img/v2-unannotated/fields/types/richtext.png
  9. BIN
      packages/noco-docs/static/img/v2-unannotated/fields/types/user-field.png
  10. BIN
      packages/noco-docs/static/img/v2/fields/types/richtext-heading.png
  11. BIN
      packages/noco-docs/static/img/v2/fields/types/richtext-links.png
  12. BIN
      packages/noco-docs/static/img/v2/fields/types/richtext.png
  13. BIN
      packages/noco-docs/static/img/v2/fields/types/user-field-cell.png
  14. BIN
      packages/noco-docs/static/img/v2/fields/types/user-field.png
  15. 10
      packages/nocodb/src/db/BaseModelSqlv2.ts
  16. 37
      packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts
  17. 80
      packages/nocodb/src/db/functionMappings/commonFns.ts
  18. 35
      packages/nocodb/src/helpers/formulaHelpers.ts
  19. 24
      packages/nocodb/src/models/Column.ts
  20. 4
      packages/nocodb/src/models/FormulaColumn.ts

2
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -52,7 +52,7 @@ const { allowCSVDownload } = useSharedView()
</template> </template>
<LazySmartsheetToolbarSearchData <LazySmartsheetToolbarSearchData
v-if="(isGrid || isGallery || isKanban) && !isPublic" v-if="isGrid || isGallery || isKanban"
:class="{ :class="{
'shrink': !isMobileMode, 'shrink': !isMobileMode,
'w-full': isMobileMode, 'w-full': isMobileMode,

16
packages/nc-gui/components/virtual-cell/Formula.vue

@ -2,16 +2,7 @@
import { handleTZ } from 'nocodb-sdk' import { handleTZ } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { import { CellValueInj, ColumnInj, IsExpandedFormOpenInj, computed, inject, renderValue, replaceUrlsWithLink, useBase, useGlobal } from '#imports'
CellValueInj,
ColumnInj,
IsExpandedFormOpenInj,
computed,
inject,
renderValue,
replaceUrlsWithLink,
useBase,
} from '#imports'
// todo: column type doesn't have required property `error` - throws in typecheck // todo: column type doesn't have required property `error` - throws in typecheck
const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }> const column = inject(ColumnInj) as Ref<ColumnType & { colOptions: { error: any } }>
@ -22,6 +13,8 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const { isPg } = useBase() const { isPg } = useBase()
const { showNull } = useGlobal()
const result = computed(() => const result = computed(() =>
isPg(column.value.source_id) ? renderValue(handleTZ(cellValue?.value)) : renderValue(cellValue?.value), isPg(column.value.source_id) ? renderValue(handleTZ(cellValue?.value)) : renderValue(cellValue?.value),
) )
@ -40,7 +33,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
</template> </template>
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<span v-else-if="cellValue === null && showNull" class="nc-null uppercase">{{ $t('general.null') }}</span>
<div <div
v-else v-else
class="py-1" class="py-1"
@ -49,6 +42,7 @@ const { showEditNonEditableFieldWarning, showClearNonEditableFieldWarning, activ
}" }"
@dblclick="activateShowEditNonEditableFieldWarning" @dblclick="activateShowEditNonEditableFieldWarning"
> >
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <div v-else>{{ result }}</div>

2
packages/nc-gui/composables/useViewData.ts

@ -205,7 +205,7 @@ export function useViewData(
} as any, } as any,
{ cancelToken: controller.value.token }, { cancelToken: controller.value.token },
) )
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, where: where?.value })
} catch (error) { } catch (error) {
// if the request is canceled, then do nothing // if the request is canceled, then do nothing
if (error.code === 'ERR_CANCELED') { if (error.code === 'ERR_CANCELED') {

2
packages/noco-docs/docs/065.table-details/_category_.json

@ -1,5 +1,5 @@
{ {
"label": "Table Details", "label": "Table details",
"collapsible": true, "collapsible": true,
"collapsed": true "collapsed": true
} }

137
packages/noco-docs/docs/070.fields/040.field-types/010.text-based/025.rich-text.md

@ -0,0 +1,137 @@
---
title: 'Rich text'
description: 'This article explains how to create & work with a Rich text field.'
tags: ['Fields', 'Field types', 'Text based types', 'Rich text']
keywords: ['Fields', 'Field types', 'Text based types', 'Rich text', 'Create rich text field']
---
`Rich Text` field is text based field & is extension of `Long text` that allows you to add formatting to the text. You can add text formatting like bold, italic, underline, strikethrough, horizontal rule, ordered list, unordered list, code, quote, etc.
## Create a `Rich Text` field
1. Click on `+` icon to the right of `Fields header`
2. On the dropdown modal, enter the field name (Optional).
3. Select the field type as `Long text` from the dropdown.
4. Enable `Rich Text` toggle field.
5. Set default value for the field (Optional).
6. Click on `Save Field` button.
![image](/img/v2/fields/types/richtext.png)
:::note
- Specify default value without quotes.
- Use `Enter` key to add new line.
:::
### Cell display
`Rich Text` field is displayed as a single line text field in the table view. Click on the expand icon in the cell to view the full text.
![image](/img/v2/fields/long-text-expand.png)
![image](/img/v2/fields/long-text-expand-2.png)
## Formatting options
NocoDB supports markdown syntax for formatting the text. Following are the supported formatting options.
### Heading
To create a heading, prefix `#` symbol preceding your heading text. The number of # symbols employed will dictate the heading's hierarchy level and typeface size. Three levels of headings are supported.
```
# Heading 1
## Heading 2
### Heading 3
```
![image](/img/v2/fields/types/richtext-heading.png)
### Text formatting
You can emphasise text with bold, italic, strikethrough or underline formatting options. Table below shows syntax, keyboard shortcut, example & output for each formatting option.
| Style | Syntax | Keyboard shortcut | Example | Output |
| --- | --- | --- | --- | --- |
| Bold | `**bold text**` | `Ctrl/Cmd + B` | `**This is bold text**` | **This is bold text** |
| Italic | `*italicized text*` | `Ctrl/Cmd + I` | `*This is italicized text*` | *This is italicized text* |
| Strikethrough | `~~strikethrough text~~` | `Ctrl/Cmd + Shift + X` | `~~This is strikethrough text~~` | ~~This is strikethrough text~~ |
| Underline | | `Ctrl/Cmd + U` | `This is underlined text` | <u>This is underlined text</u> |
### Quote block
You can quote text with a `>`
```
normal text
> quoted text
```
normal text
> quoted text
### Code block
Code block can be created by using (3 backticks) before & after the code.
````
```
This is a code block
```
````
```
This is a code block
```
### Link
You can create an inline link by using `Link` menu option in the rich text toolbar
![image](/img/v2/fields/types/richtext-links.png)
### Bullet List
You can create unordered list by using `Bulleted list` menu option in the rich text toolbar or by preceding the text with `-` `+` or `*` symbol.
```
- Item 1
- Item 2
+ Item 1
+ Item 2
* Item 1
* Item 2
```
- Item 1
- Item 2
+ Item 1
+ Item 2
* Item 1
* Item 2
:::note
You can create nested lists by using `tab` key & `shift + tab` key to indent & outdent the list items.
:::
### Numbered List
You can create ordered list by using `Numbered list` menu option in the rich text toolbar or by preceding the text with `1.` symbol.
```
1. Item 1
2. Item 2
```
1. Item 1
2. Item 2
### Task list
You can create task lists by using `Task list` menu option in the rich text toolbar or by preceding the text with `[ ]` symbol. You can mark the task as completed by using `[x]` symbol.
```
[ ] Item 1
[x] Item 2
```
- [ ] Item 1
- [x] Item 2
## Similar text based fields
Following are the other text based fields available in NocoDB, custom-built for specific use cases.
- [Single line text](010.single-line-text.md)
- [URL](050.url.md)
- [Email](030.email.md)
- [Phone](040.phonenumber.md)

30
packages/noco-docs/docs/070.fields/040.field-types/080.user-based/010.user.md

@ -0,0 +1,30 @@
---
title: 'User'
description: 'This article explains how to create & work with a User field.'
tags: ['Fields', 'Field types', 'User']
keywords: ['Fields', 'Field types', 'User', 'Create User field']
---
`User` field type allows you to assign a user from your current workspace to a record. For example, you can create a `Task` table with a `User` field type to assign a task to a user. You can also configure the field to allow assigning multiple users to a record.
## Create a User field
1. Click on `+` icon to the right of `Fields header`
2. On the dropdown modal, enter the field name (Optional).
3. Select the field type as `User` from the dropdown.
4. Configure `Allow adding multiple users` toggle field (Optional).
5. Configure default value (Optional)
6. Click on `Save Field` button.
![image](/img/v2/fields/types/user-field.png)
### Cell display
`User` field display is quite identical to `Select` field. It is displayed as a dropdown in the table view. Click on the dropdown to select a user. If `Allow adding multiple users` is enabled, you can select multiple users from the dropdown.
![image](/img/v2/fields/types/user-field-cell.png)
:::note
- If a user is removed from workspace, the user will be removed from the dropdown list. If such user was assigned to a record already, the user will be displayed as is.
- To remove a user from a record, click on the `x` icon next to the user name.
- If display name is not set for a user, the user's email address will be displayed.
:::

5
packages/noco-docs/docs/070.fields/040.field-types/080.user-based/_category_.json

@ -0,0 +1,5 @@
{
"label": "User based",
"collapsible": true,
"collapsed": true
}

BIN
packages/noco-docs/static/img/v2-unannotated/fields/types/richtext.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

BIN
packages/noco-docs/static/img/v2-unannotated/fields/types/user-field.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
packages/noco-docs/static/img/v2/fields/types/richtext-heading.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
packages/noco-docs/static/img/v2/fields/types/richtext-links.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
packages/noco-docs/static/img/v2/fields/types/richtext.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

BIN
packages/noco-docs/static/img/v2/fields/types/user-field-cell.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
packages/noco-docs/static/img/v2/fields/types/user-field.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

10
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -3846,9 +3846,6 @@ class BaseModelSqlv2 {
return; return;
} }
const options = await column
.getColOptions<{ options: SelectOption[] }>()
.then(({ options }) => options.map((opt) => opt.title));
const columnTitle = column.title; const columnTitle = column.title;
const columnName = column.column_name; const columnName = column.column_name;
const columnValue = const columnValue =
@ -3857,6 +3854,13 @@ class BaseModelSqlv2 {
return; return;
} }
const options = await column
.getColOptions<{ options: SelectOption[] }>()
.then(
(selectOptionsMeta) =>
selectOptionsMeta?.options?.map((opt) => opt.title) || [],
);
// if multi select, then split the values // if multi select, then split the values
const columnValueArr = const columnValueArr =
column.uidt === UITypes.MultiSelect column.uidt === UITypes.MultiSelect

37
packages/nocodb/src/db/formulav2/formulaQueryBuilderv2.ts

@ -873,6 +873,43 @@ async function _formulaQueryBuilder(
); );
} }
// if operator is == or !=, then handle comparison with BLANK which should accept NULL and empty string
if (pt.operator === '==' || pt.operator === '!=') {
if (pt.left.callee?.name !== pt.right.callee?.name) {
// if left/right is BLANK, accept both NULL and empty string
for (const operand of ['left', 'right']) {
if (
pt[operand].type === 'CallExpression' &&
pt[operand].callee.name === 'BLANK'
) {
const isString =
pt[operand === 'left' ? 'right' : 'left'].dataType ===
FormulaDataTypes.STRING;
let calleeName;
if (pt.operator === '==') {
calleeName = isString ? 'ISBLANK' : 'ISNULL';
} else {
calleeName = isString ? 'ISNOTBLANK' : 'ISNOTNULL';
}
return fn(
{
type: 'CallExpression',
arguments: [operand === 'left' ? pt.right : pt.left],
callee: {
type: 'Identifier',
name: calleeName,
},
},
alias,
prevBinaryOp,
);
}
}
}
}
if (pt.operator === '==') { if (pt.operator === '==') {
pt.operator = '='; pt.operator = '=';
// if left/right is of different type, convert to string and compare // if left/right is of different type, convert to string and compare

80
packages/nocodb/src/db/functionMappings/commonFns.ts

@ -33,7 +33,6 @@ async function treatArgAsConditionalExp(
} }
export default { export default {
// todo: handle default case
SWITCH: async (args: MapFnArgs) => { SWITCH: async (args: MapFnArgs) => {
const count = Math.floor((args.pt.arguments.length - 1) / 2); const count = Math.floor((args.pt.arguments.length - 1) / 2);
let query = ''; let query = '';
@ -55,6 +54,9 @@ export default {
const switchVal = (await args.fn(args.pt.arguments[0])).builder.toQuery(); const switchVal = (await args.fn(args.pt.arguments[0])).builder.toQuery();
// used it for null value check
let elseValPrefix = '';
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
let val; let val;
// cast to string if the return value types are different // cast to string if the return value types are different
@ -73,13 +75,34 @@ export default {
val = (await args.fn(args.pt.arguments[i * 2 + 2])).builder.toQuery(); val = (await args.fn(args.pt.arguments[i * 2 + 2])).builder.toQuery();
} }
query += args.knex if (
.raw( args.pt.arguments[i * 2 + 1].type === 'CallExpression' &&
`\n\tWHEN ${( args.pt.arguments[i * 2 + 1].callee?.name === 'BLANK'
await args.fn(args.pt.arguments[i * 2 + 1]) ) {
).builder.toQuery()} THEN ${val}`, elseValPrefix += args.knex
) .raw(
.toQuery(); `\n\tWHEN ${switchVal} IS NULL ${
args.pt.arguments[i * 2 + 1].dataType === FormulaDataTypes.STRING
? `OR ${switchVal} = ''`
: ''
} THEN ${val}`,
)
.toQuery();
} else if (
args.pt.arguments[i * 2 + 1].dataType === FormulaDataTypes.NULL
) {
elseValPrefix += args.knex
.raw(`\n\tWHEN ${switchVal} IS NULL THEN ${val}`)
.toQuery();
} else {
query += args.knex
.raw(
`\n\tWHEN ${(
await args.fn(args.pt.arguments[i * 2 + 1])
).builder.toQuery()} THEN ${val}`,
)
.toQuery();
}
} }
if (args.pt.arguments.length % 2 === 0) { if (args.pt.arguments.length % 2 === 0) {
let val; let val;
@ -100,8 +123,13 @@ export default {
await args.fn(args.pt.arguments[args.pt.arguments.length - 1]) await args.fn(args.pt.arguments[args.pt.arguments.length - 1])
).builder.toQuery(); ).builder.toQuery();
} }
if (elseValPrefix) {
query += `\n\tELSE ${val}`; query += `\n\tELSE (CASE ${elseValPrefix} ELSE ${val} END)`;
} else {
query += `\n\tELSE ${val}`;
}
} else if (elseValPrefix) {
query += `\n\tELSE (CASE ${elseValPrefix} END)`;
} }
return { return {
builder: args.knex.raw( builder: args.knex.raw(
@ -321,4 +349,36 @@ export default {
), ),
}; };
}, },
ISBLANK: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const { builder: valueBuilder } = await fn(pt.arguments[0]);
return {
builder: knex.raw(
`(${valueBuilder} IS NULL OR ${valueBuilder} = '')${colAlias}`,
),
};
},
ISNULL: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const { builder: valueBuilder } = await fn(pt.arguments[0]);
return {
builder: knex.raw(`(${valueBuilder} IS NULL)${colAlias}`),
};
},
ISNOTBLANK: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const { builder: valueBuilder } = await fn(pt.arguments[0]);
return {
builder: knex.raw(
`(${valueBuilder} IS NOT NULL AND ${valueBuilder} != '')${colAlias}`,
),
};
},
ISNOTNULL: async ({ fn, knex, pt, colAlias }: MapFnArgs) => {
const { builder: valueBuilder } = await fn(pt.arguments[0]);
return {
builder: knex.raw(`(${valueBuilder} IS NOT NULL)${colAlias}`),
};
},
}; };

35
packages/nocodb/src/helpers/formulaHelpers.ts

@ -0,0 +1,35 @@
import jsep from 'jsep';
import { UITypes } from 'nocodb-sdk';
import type FormulaColumn from '../models/FormulaColumn';
import type { Column } from '~/models';
export async function getFormulasReferredTheColumn({
column,
columns,
}: {
column: Column;
columns: Column[];
}): Promise<Column[]> {
const fn = (pt) => {
if (pt.type === 'CallExpression') {
return pt.arguments.some((arg) => fn(arg));
} else if (pt.type === 'Literal') {
} else if (pt.type === 'Identifier') {
return [column.id, column.title].includes(pt.name);
} else if (pt.type === 'BinaryExpression') {
return fn(pt.left) || fn(pt.right);
}
};
return columns.reduce(async (columnsPromise, c) => {
const columns = await columnsPromise;
if (c.uidt !== UITypes.Formula) return columns;
const formula = await c.getColOptions<FormulaColumn>();
if (fn(jsep(formula.formula))) {
columns.push(c);
}
return columns;
}, Promise.resolve([]));
}

24
packages/nocodb/src/models/Column.ts

@ -3,6 +3,7 @@ import {
isLinksOrLTAR, isLinksOrLTAR,
UITypes, UITypes,
} from 'nocodb-sdk'; } from 'nocodb-sdk';
import { Logger } from '@nestjs/common';
import type { ColumnReqType, ColumnType } from 'nocodb-sdk'; import type { ColumnReqType, ColumnType } from 'nocodb-sdk';
import FormulaColumn from '~/models/FormulaColumn'; import FormulaColumn from '~/models/FormulaColumn';
import LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn'; import LinkToAnotherRecordColumn from '~/models/LinkToAnotherRecordColumn';
@ -28,6 +29,7 @@ import {
} from '~/utils/globals'; } from '~/utils/globals';
import NocoCache from '~/cache/NocoCache'; import NocoCache from '~/cache/NocoCache';
import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils'; import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils';
import { getFormulasReferredTheColumn } from '~/helpers/formulaHelpers';
const selectColors = [ const selectColors = [
'#cfdffe', '#cfdffe',
@ -42,6 +44,8 @@ const selectColors = [
'#eeeeee', '#eeeeee',
]; ];
const logger = new Logger('Column');
export default class Column<T = any> implements ColumnType { export default class Column<T = any> implements ColumnType {
public fk_model_id: string; public fk_model_id: string;
public base_id: string; public base_id: string;
@ -1159,6 +1163,26 @@ export default class Column<T = any> implements ColumnType {
// on column update, delete any optimised single query cache // on column update, delete any optimised single query cache
await NocoCache.delAll(CacheScope.SINGLE_QUERY, `${oldCol.fk_model_id}:*`); await NocoCache.delAll(CacheScope.SINGLE_QUERY, `${oldCol.fk_model_id}:*`);
const updatedColumn = await Column.get({ colId });
// invalidate formula parsed-tree in which current column is used
// whenever a new request comes for that formula, it will be populated again
getFormulasReferredTheColumn({
column: updatedColumn,
columns: await Column.list({ fk_model_id: column.fk_model_id }),
})
.then(async (formulas) => {
for (const formula of formulas) {
await FormulaColumn.update(formula.id, {
parsed_tree: null,
});
}
})
// ignore the error and continue, if formula is no longer valid it will be captured in the next run
.catch((err) => {
logger.error(err);
});
} }
static async updateAlias( static async updateAlias(

4
packages/nocodb/src/models/FormulaColumn.ts

@ -74,8 +74,6 @@ export default class FormulaColumn {
'parsed_tree', 'parsed_tree',
]); ]);
updateObj.parsed_tree = stringifyMetaProp(updateObj, 'parsed_tree');
// get existing cache // get existing cache
const key = `${CacheScope.COL_FORMULA}:${id}`; const key = `${CacheScope.COL_FORMULA}:${id}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
@ -84,6 +82,8 @@ export default class FormulaColumn {
// set cache // set cache
await NocoCache.set(key, o); await NocoCache.set(key, o);
} }
if ('parsed_tree' in updateObj)
updateObj.parsed_tree = stringifyMetaProp(updateObj, 'parsed_tree');
// set meta // set meta
await ncMeta.metaUpdate(null, null, MetaTable.COL_FORMULA, updateObj, id); await ncMeta.metaUpdate(null, null, MetaTable.COL_FORMULA, updateObj, id);
} }

Loading…
Cancel
Save