Browse Source

Merge pull request #8180 from nocodb/develop

pull/8181/head 0.205.1
github-actions[bot] 8 months ago committed by GitHub
parent
commit
d826bf16f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 14
      packages/nc-gui/components/cell/Url.vue
  2. 2
      packages/nc-gui/components/smartsheet/Topbar.vue
  3. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  4. 6
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  5. 6
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  6. 7
      packages/nc-gui/composables/useLTARStore.ts
  7. 41
      packages/nc-gui/composables/useSharedFormViewStore.ts
  8. 2
      packages/nc-gui/lang/pl.json
  9. 56
      packages/nc-gui/lang/ru.json
  10. 36
      packages/noco-docs/docs/090.views/040.view-types/030.form.md
  11. 4
      packages/nocodb/src/helpers/initAdminFromEnv.ts
  12. 2
      packages/nocodb/src/meta/meta.service.ts
  13. 4
      packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts
  14. 6
      packages/nocodb/src/meta/migrations/v2/nc_011.ts
  15. 8
      packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts
  16. 27
      packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts
  17. 7
      packages/nocodb/src/models/CalendarViewColumn.ts
  18. 85
      packages/nocodb/src/models/Column.ts
  19. 4
      packages/nocodb/src/models/FormViewColumn.ts
  20. 7
      packages/nocodb/src/models/GalleryViewColumn.ts
  21. 2
      packages/nocodb/src/models/GridViewColumn.ts
  22. 4
      packages/nocodb/src/models/KanbanViewColumn.ts
  23. 4
      packages/nocodb/src/models/MapViewColumn.ts
  24. 20
      packages/nocodb/src/models/Model.ts
  25. 22
      packages/nocodb/src/models/User.ts
  26. 122
      packages/nocodb/src/models/UserRefreshToken.ts
  27. 3
      packages/nocodb/src/models/View.ts
  28. 1
      packages/nocodb/src/models/index.ts
  29. 4
      packages/nocodb/src/services/public-metas.service.ts
  30. 36
      packages/nocodb/src/services/users/users.service.ts
  31. 4
      packages/nocodb/src/services/views.service.ts
  32. 7
      packages/nocodb/src/utils/globals.ts
  33. 1
      packages/nocodb/src/utils/sanitiseUserObj.ts
  34. 6
      pnpm-lock.yaml
  35. 110
      scripts/docs/fr/120.collaboration/fr-030.share-base.md
  36. 2
      tests/playwright/tests/db/views/viewCalendar.spec.ts

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

@ -47,6 +47,8 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))!
const isForm = inject(IsFormInj)! const isForm = inject(IsFormInj)!
const trim = (val: string) => val?.trim?.()
// Used in the logic of when to display error since we are not storing the url if it's not valid // Used in the logic of when to display error since we are not storing the url if it's not valid
const localState = ref(value) const localState = ref(value)
@ -54,21 +56,21 @@ const vModel = computed({
get: () => value, get: () => value,
set: (val) => { set: (val) => {
localState.value = val localState.value = val
if (!parseProp(column.value.meta)?.validate || (val && isValidURL(val)) || !val || isForm.value) { if (!parseProp(column.value.meta)?.validate || (val && isValidURL(trim(val))) || !val || isForm.value) {
emit('update:modelValue', val) emit('update:modelValue', val)
} }
}, },
}) })
const isValid = computed(() => value && isValidURL(value)) const isValid = computed(() => value && isValidURL(trim(value)))
const url = computed(() => { const url = computed(() => {
if (!value || !isValidURL(value)) return '' if (!value || !isValidURL(trim(value))) return ''
/** add url scheme if missing */ /** add url scheme if missing */
if (/^https?:\/\//.test(value)) return value if (/^https?:\/\//.test(trim(value))) return trim(value)
return `https://${value}` return `https://${trim(value)}`
}) })
const { cellUrlOptions } = useCellUrlConfig(url) const { cellUrlOptions } = useCellUrlConfig(url)
@ -84,7 +86,7 @@ watch(
parseProp(column.value.meta)?.validate && parseProp(column.value.meta)?.validate &&
!editEnabled.value && !editEnabled.value &&
localState.value && localState.value &&
!isValidURL(localState.value) !isValidURL(trim(localState.value))
) { ) {
message.error(t('msg.error.invalidURL')) message.error(t('msg.error.invalidURL'))
localState.value = undefined localState.value = undefined

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

@ -29,7 +29,7 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<GeneralOpenLeftSidebarBtn /> <GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" /> <LazySmartsheetToolbarViewInfo v-if="!isPublic" />
<div v-if="!isSharedBase && !isMobileMode" class="w-47.5"> <div v-if="!isSharedBase && !isMobileMode">
<SmartsheetTopbarSelectMode /> <SmartsheetTopbarSelectMode />
</div> </div>
<div class="flex-1" /> <div class="flex-1" />

2
packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue

@ -81,7 +81,7 @@ const {
activeView, activeView,
parentId?.value, parentId?.value,
computed(() => autoSave.value), computed(() => autoSave.value),
() => reloadDataHook.trigger({ shouldShowLoading: showLoading.value }), () => reloadDataHook.trigger({ shouldShowLoading: showLoading.value, offset: 0 }),
modelValue.value || nestedFilters.value, modelValue.value || nestedFilters.value,
!modelValue.value, !modelValue.value,
webHook.value, webHook.value,

6
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -62,9 +62,9 @@ const onClickDetails = () => {
} }
.tab .tab-title { .tab .tab-title {
@apply min-w-0; @apply min-w-0;
word-break: 'keep-all'; word-break: keep-all;
white-space: 'nowrap'; white-space: nowrap;
display: 'inline'; display: inline;
line-height: 0.95; line-height: 0.95;
} }

6
packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue

@ -51,7 +51,7 @@ const {
unlink, unlink,
row, row,
headerDisplayValue, headerDisplayValue,
resetChildrenExcludedOffsetCount resetChildrenExcludedOffsetCount,
} = useLTARStoreOrThrow() } = useLTARStoreOrThrow()
const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow() const { addLTARRef, isNew, removeLTARRef, state: rowState } = useSmartsheetRowStoreOrThrow()
@ -102,7 +102,7 @@ watch(
} }
loadChildrenExcludedList(rowState.value) loadChildrenExcludedList(rowState.value)
} }
if(!nextVal){ if (!nextVal) {
resetChildrenExcludedOffsetCount() resetChildrenExcludedOffsetCount()
} }
}, },
@ -262,7 +262,7 @@ onUnmounted(() => {
}) })
const onFilterChange = () => { const onFilterChange = () => {
childrenExcludedListPagination.page = 1; childrenExcludedListPagination.page = 1
resetChildrenExcludedOffsetCount() resetChildrenExcludedOffsetCount()
} }
</script> </script>

7
packages/nc-gui/composables/useLTARStore.ts

@ -191,7 +191,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
const loadChildrenExcludedList = async (activeState?: any) => { const loadChildrenExcludedList = async (activeState?: any) => {
if (activeState) newRowState.state = activeState if (activeState) newRowState.state = activeState
try { try {
let offset = childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value let offset =
childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value
if (offset < 0) { if (offset < 0) {
offset = 0 offset = 0
@ -550,8 +551,8 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}) })
}) })
const resetChildrenExcludedOffsetCount = () =>{ const resetChildrenExcludedOffsetCount = () => {
childrenExcludedOffsetCount.value = 0; childrenExcludedOffsetCount.value = 0
} }
const resetChildrenListOffsetCount = () => { const resetChildrenListOffsetCount = () => {

41
packages/nc-gui/composables/useSharedFormViewStore.ts

@ -132,25 +132,27 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
{} as Record<string, FormColumnType>, {} as Record<string, FormColumnType>,
) )
columns.value = viewMeta.model?.columns?.map((c) => { columns.value = (viewMeta.model?.columns || [])
if ( .filter((c) => fieldById[c.id])
!isSystemColumn(c) && .map((c) => {
!isVirtualCol(c) && if (
!isAttachment(c) && !isSystemColumn(c) &&
c.uidt !== UITypes.SpecificDBType && !isVirtualCol(c) &&
c?.title && !isAttachment(c) &&
c?.cdf && c.uidt !== UITypes.SpecificDBType &&
!/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf) c?.title &&
) { c?.cdf &&
formState.value[c.title] = typeof c.cdf === 'string' ? c.cdf.replace(/^'|'$/g, '') : c.cdf !/^\w+\(\)|CURRENT_TIMESTAMP$/.test(c.cdf)
} ) {
formState.value[c.title] = typeof c.cdf === 'string' ? c.cdf.replace(/^'|'$/g, '') : c.cdf
}
return { return {
...c, ...c,
meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) }, meta: { ...parseProp(fieldById[c.id].meta), ...parseProp(c.meta) },
description: fieldById[c.id].description, description: fieldById[c.id].description,
} }
}) })
const _sharedViewMeta = (viewMeta as any).meta const _sharedViewMeta = (viewMeta as any).meta
sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta sharedViewMeta.value = isString(_sharedViewMeta) ? JSON.parse(_sharedViewMeta) : _sharedViewMeta
@ -188,6 +190,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
if (password.value && password.value !== '') { if (password.value && password.value !== '') {
passwordError.value = error.message passwordError.value = error.message
} }
} else if (error.error === NcErrorType.UNKNOWN_ERROR) {
console.error('Error occurred while loading shared form view', e)
message.error('Error occurred while loading shared form view')
} }
} }
} }

2
packages/nc-gui/lang/pl.json

@ -701,7 +701,7 @@
"hideNocodbBranding": "Ukryj branding NocoDB", "hideNocodbBranding": "Ukryj branding NocoDB",
"showOnConditions": "Pokaż na warunkach", "showOnConditions": "Pokaż na warunkach",
"showFieldOnConditionsMet": "Pokazuje pole tylko, gdy spełnione są warunki", "showFieldOnConditionsMet": "Pokazuje pole tylko, gdy spełnione są warunki",
"limitOptions": "Limit options", "limitOptions": "Ogranicz opcje",
"limitOptionsSubtext": "Ogranicz opcje widoczne dla użytkowników, wybierając dostępne opcje", "limitOptionsSubtext": "Ogranicz opcje widoczne dla użytkowników, wybierając dostępne opcje",
"clearSelection": "Wyczyść wybór" "clearSelection": "Wyczyść wybór"
}, },

56
packages/nc-gui/lang/ru.json

@ -157,8 +157,8 @@
"groupingField": "Поле группировки", "groupingField": "Поле группировки",
"insertAfter": "Вставить после", "insertAfter": "Вставить после",
"insertBefore": "Вставить перед", "insertBefore": "Вставить перед",
"insertAbove": "Insert above", "insertAbove": "Вставить выше",
"insertBelow": "Insert below", "insertBelow": "Вставить ниже",
"hideField": "Скрыть поле", "hideField": "Скрыть поле",
"sortAsc": "По Возрастанию", "sortAsc": "По Возрастанию",
"sortDesc": "По убыванию", "sortDesc": "По убыванию",
@ -192,21 +192,21 @@
"enter": "Вход", "enter": "Вход",
"seconds": "Секунды", "seconds": "Секунды",
"paste": "Вставить", "paste": "Вставить",
"restore": "Restore", "restore": "Восстановить",
"replace": "Replace", "replace": "Заменить",
"banner": "Banner", "banner": "Баннер",
"logo": "Logo", "logo": "Логотип",
"dropdown": "Dropdown", "dropdown": "Выпадающий список",
"list": "List", "list": "Список",
"apply": "Apply", "apply": "Применить",
"text": "Text", "text": "Текст",
"appearance": "Appearance" "appearance": "Внешний вид"
}, },
"objects": { "objects": {
"day": "Day", "day": "День",
"week": "Week", "week": "Неделя",
"month": "Month", "month": "Месяц",
"year": "Year", "year": "Год",
"workspace": "Рабочее пространство", "workspace": "Рабочее пространство",
"workspaces": "Рабочие пространства", "workspaces": "Рабочие пространства",
"project": "Проект", "project": "Проект",
@ -313,7 +313,7 @@
"isNotNull": "не равно Null" "isNotNull": "не равно Null"
}, },
"title": { "title": {
"sso": "Authentication (SSO)", "sso": "Аутентификация (SSO)",
"docs": "Документация", "docs": "Документация",
"forum": "Форум", "forum": "Форум",
"parameter": "Параметр", "parameter": "Параметр",
@ -341,7 +341,7 @@
"removeFile": "Удалить файл", "removeFile": "Удалить файл",
"hasMany": "Имеет много", "hasMany": "Имеет много",
"manyToMany": "Многие ко многим", "manyToMany": "Многие ко многим",
"oneToOne": "One to One", "oneToOne": "Один к одному",
"virtualRelation": "Виртуальные отношения", "virtualRelation": "Виртуальные отношения",
"linkMore": "Ссылка Подробнее", "linkMore": "Ссылка Подробнее",
"linkMoreRecords": "Связать больше записей", "linkMoreRecords": "Связать больше записей",
@ -436,9 +436,9 @@
"surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?" "surveyFormSubmitConfirmMsg": "Are you sure you want to submit this form?"
}, },
"labels": { "labels": {
"selectYear": "Select Year", "selectYear": "Выберите год",
"save": "Save", "save": "Сохранить",
"cancel": "Cancel", "cancel": "Отмена",
"metadataUrl": "Metadata URL", "metadataUrl": "Metadata URL",
"audience-entityId": "Audience/ Entity ID", "audience-entityId": "Audience/ Entity ID",
"redirectUrl": "Redirect URL", "redirectUrl": "Redirect URL",
@ -446,12 +446,12 @@
"saml": "Security Assertion Markup Language (SAML)", "saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider", "newProvider": "New Provider",
"generalSettings": "General Settings", "generalSettings": "General Settings",
"ssoSettings": "SSO Settings", "ssoSettings": "Настройки SSO",
"organizeBy": "Organize by", "organizeBy": "Organize by",
"previous": "Previous", "previous": "Предыдущий",
"nextMonth": "Next Month", "nextMonth": "Следующий месяц",
"previousMonth": "Previous Month", "previousMonth": "Предыдущий месяц",
"next": "Next", "next": "Следующий",
"organiseBy": "Organise by", "organiseBy": "Organise by",
"heading1": "Заголовок 1", "heading1": "Заголовок 1",
"heading2": "Заголовок 2", "heading2": "Заголовок 2",
@ -573,7 +573,7 @@
"where": "Где", "where": "Где",
"cache": "Кэш", "cache": "Кэш",
"chat": "Чат", "chat": "Чат",
"showOrHide": "Show or Hide", "showOrHide": "Показать / Скрыть",
"airtable": "Airtable", "airtable": "Airtable",
"csv": "CSV", "csv": "CSV",
"csvFile": "Файл CSV", "csvFile": "Файл CSV",
@ -589,7 +589,7 @@
"created": "Созданный", "created": "Созданный",
"sqlOutput": "Вывод SQL", "sqlOutput": "Вывод SQL",
"addOption": "Добавить настройку", "addOption": "Добавить настройку",
"interfaceColor": "Interface Color", "interfaceColor": "Цвет интерфейса",
"qrCodeValueColumn": "Столбец с QR-кодом", "qrCodeValueColumn": "Столбец с QR-кодом",
"barcodeValueColumn": "Колонка со значением штрих-кода", "barcodeValueColumn": "Колонка со значением штрих-кода",
"barcodeFormat": "Формат штрих-кода", "barcodeFormat": "Формат штрих-кода",
@ -618,7 +618,7 @@
"joinCommunity": "Сообщество NocoDB", "joinCommunity": "Сообщество NocoDB",
"joinReddit": "Присоединиться /r/NocoDB", "joinReddit": "Присоединиться /r/NocoDB",
"followNocodb": "Следите за NocoDB", "followNocodb": "Следите за NocoDB",
"communityTranslated": "(Community Translated)" "communityTranslated": "(Перевод сообщества)"
}, },
"twitter": "Twitter", "twitter": "Twitter",
"docReference": "Ссылка на документ", "docReference": "Ссылка на документ",

36
packages/noco-docs/docs/090.views/040.view-types/030.form.md

@ -32,7 +32,7 @@ Form view builder layout can be divided into 4 sections:
In the **Form View** area, click on in input boxes provided for **Title** & **Description** to add/update title & description to the form. In the **Form View** area, click on in input boxes provided for **Title** & **Description** to add/update title & description to the form.
:::info :::info
Formatting options are supported for the description field. You can also use markdown to format the text. Formatting options are supported for the description field. You can also use Markdown to format the text.
::: :::
![Form Title & Description](/img/v2/views/form-view/title-description.png) ![Form Title & Description](/img/v2/views/form-view/title-description.png)
@ -80,19 +80,19 @@ NocoDB allows you to configure the form view to perform various actions after a
![Form View Settings](/img/v2/views/form-view/post-submit-settings.png) ![Form View Settings](/img/v2/views/form-view/post-submit-settings.png)
:::info :::info
Formatting options are supported for the `After Submit Message` field. You can also use markdown to format the text. Formatting options are supported for the `After Submit Message` field. You can also use Markdown to format the text.
::: :::
## Field configuration ## Field configuration
To change the field label displayed on the form & add help-text, click on the required field in the **Form Area** and on the right side configuration panel, configure To change the field label displayed on the form & add help-text, click on the required field in the **Form Area** and on the right side configuration panel, configure
1. **Label** `Opitonal` : Defaults to the field name. This doesn't affect the field name in the table. 1. **Label** `Optional` : Defaults to the field name. This doesn't affect the field name in the table.
2. **Help Text** `Optional` 2. **Help Text** `Optional`
3. **Required** : Toggle to mark the field as required 3. **Required** : Toggle to mark the field as required
![Field Label & Help Text](/img/v2/views/form-view/field-config.png) ![Field Label & Help Text](/img/v2/views/form-view/field-config.png)
:::info :::info
Formatting options are supported for the `Help Text` field. You can also use markdown to format the text. Formatting options are supported for the `Help Text` field. You can also use Markdown to format the text.
::: :::
### Field Type Specific Settings ### Field Type Specific Settings
@ -111,14 +111,22 @@ For select based field types, you can configure the options layout to be display
![Options Layout](/img/v2/views/form-view/options-layout.png) ![Options Layout](/img/v2/views/form-view/options-layout.png)
## Prefill Form Fields ## Prefill Form Fields
Prefilling form fields is a way to pre-populate form fields with default values. This can be useful when you want to save time for users by prefilling some fields with default values. The prefilled fields and their values are visible in the URL of the form view & can be manually constructed by ensuring URL parameters are appropriately encoded. Here's a more professional rephrasing of the given content:
## Pre-Filling Form Fields
NocoDB offers a convenient feature that allows pre-filling form fields with specific values by setting URL parameters. This functionality enables the creation of custom URLs with desired field values, streamlining data entry and enhancing user experience.
To construct a pre-filled form URL manually, ensure that the URL parameters are appropriately encoded in the following format: `?key1=value1&key2=value2`.
For instance, the URL `https://wh8s5w.noco.to/#/nc/form/66da06-f074-47af-ace7-fde46df55?Status=Qualification&Priority=Very+high` pre-fills the `Status` field with `Qualification` and the `Priority` field with `Very high`.
NocoDB provides an intuitive alternative approach to generate pre-filled URLs through the form builder.
1. Open the form builder and pre-fill the required form fields with the desired values.
2. Click on the `Share` button located in the top right corner.
3. Toggle the `Enable Public Viewing` button to enable sharing.
4. Toggle the `Enable Prefill` button to enable pre-filling.
5. Click on the `Copy Link` button to copy the pre-filled URL.
NocoDB provides an easier approach to construct prefilled URLs. One can use the form builder to prefill form fields with default values & auto-generate encoded prefilled URL. Follow the below steps to prefill form fields & generate a prefilled URL -
1. Open the form builder, prefill the required form fields with default values.
2. Click on the `Share` button in the top right corner.
3. Toggle `Enable Public Viewing` button to enable share.
4. Toggle `Enable Prefill` button to enable prefill.
5. Click on the `Copy Link` button to copy the link.
![Prefill](/img/v2/views/form-view/prefill.png) ![Prefill](/img/v2/views/form-view/prefill.png)
![Prefill share](/img/v2/views/form-view/prefill-share.png) ![Prefill share](/img/v2/views/form-view/prefill-share.png)
@ -129,15 +137,15 @@ NocoDB provides an easier approach to construct prefilled URLs. One can use the
::: :::
### Prefill modes ### Prefill modes
1. **Default**: Standard mode. This mode will prefill the form fields with the default values set in the form builder. Users can edit the prefilled fields. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`. 1. **Default**: Standard mode. This mode will prefill the form fields with the values set in the shared form URL. Users can edit the prefilled fields in the form. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill default](/img/v2/views/form-view/prefill-default.png) ![Prefill default](/img/v2/views/form-view/prefill-default.png)
2. **Hide prefilled fields**: This mode will prefill the form fields with the default values set in the form builder but will hide the prefilled fields from the user. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`. 2. **Hide prefilled fields**: This mode will prefill the form fields with the values set in the shared form URL but will hide the prefilled fields in the form from the user. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill hide](/img/v2/views/form-view/prefill-hide.png) ![Prefill hide](/img/v2/views/form-view/prefill-hide.png)
3. **Lock prefilled fields as read-only**: This mode will prefill the form fields with the default values set in the form builder and will lock the prefilled fields as read-only. When shared, the prefilled fields will be visible in the URL. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`. 3. **Lock prefilled fields as read-only**: This mode will prefill the form fields with the values set in the shared form URL and will lock the prefilled fields as read-only. In the image below, the `Number` field is prefilled with the value `1234`, `Currency` field is prefilled with the value `1000` and `Year` field is prefilled with value `2023`.
![Prefill lock](/img/v2/views/form-view/prefill-lock.png) ![Prefill lock](/img/v2/views/form-view/prefill-lock.png)

4
packages/nocodb/src/helpers/initAdminFromEnv.ts

@ -185,7 +185,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -199,7 +198,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -220,7 +218,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
}, },
ncMeta, ncMeta,
); );
@ -248,7 +245,6 @@ export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) {
password, password,
email_verification_token, email_verification_token,
token_version: randomTokenString(), token_version: randomTokenString(),
refresh_token: null,
roles, roles,
}, },
ncMeta, ncMeta,

2
packages/nocodb/src/meta/meta.service.ts

@ -228,7 +228,7 @@ export class MetaService {
case MetaTable.USERS: case MetaTable.USERS:
prefix = 'us'; prefix = 'us';
break; break;
case MetaTable.ORGS: case MetaTable.ORGS_OLD:
prefix = 'org'; prefix = 'org';
break; break;
case MetaTable.TEAMS: case MetaTable.TEAMS:

4
packages/nocodb/src/meta/migrations/XcMigrationSourcev2.ts

@ -29,6 +29,7 @@ import * as nc_039_sqlite_alter_column_types from '~/meta/migrations/v2/nc_039_s
import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types'; import * as nc_040_form_view_alter_column_types from '~/meta/migrations/v2/nc_040_form_view_alter_column_types';
import * as nc_041_calendar_view from '~/meta/migrations/v2/nc_041_calendar_view'; import * as nc_041_calendar_view from '~/meta/migrations/v2/nc_041_calendar_view';
import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block'; import * as nc_042_user_block from '~/meta/migrations/v2/nc_042_user_block';
import * as nc_043_user_refresh_token from '~/meta/migrations/v2/nc_043_user_refresh_token';
// Create a custom migration source class // Create a custom migration source class
export default class XcMigrationSourcev2 { export default class XcMigrationSourcev2 {
@ -69,6 +70,7 @@ export default class XcMigrationSourcev2 {
'nc_040_form_view_alter_column_types', 'nc_040_form_view_alter_column_types',
'nc_041_calendar_view', 'nc_041_calendar_view',
'nc_042_user_block', 'nc_042_user_block',
'nc_043_user_refresh_token',
]); ]);
} }
@ -140,6 +142,8 @@ export default class XcMigrationSourcev2 {
return nc_041_calendar_view; return nc_041_calendar_view;
case 'nc_042_user_block': case 'nc_042_user_block':
return nc_042_user_block; return nc_042_user_block;
case 'nc_043_user_refresh_token':
return nc_043_user_refresh_token;
} }
} }
} }

6
packages/nocodb/src/meta/migrations/v2/nc_011.ts

@ -700,7 +700,7 @@ const up = async (knex) => {
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.ORGS, (table) => { await knex.schema.createTable(MetaTable.ORGS_OLD, (table) => {
table.string('id', 20).primary().notNullable(); table.string('id', 20).primary().notNullable();
table.string('title'); table.string('title');
@ -712,13 +712,13 @@ const up = async (knex) => {
table.string('title'); table.string('title');
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => {
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.string('user_id', 20); table.string('user_id', 20);
table.foreign('user_id').references(`${MetaTable.USERS}.id`); table.foreign('user_id').references(`${MetaTable.USERS}.id`);
table.timestamps(true, true); table.timestamps(true, true);

8
packages/nocodb/src/meta/migrations/v2/nc_032_cleanup.ts

@ -7,11 +7,11 @@ const up = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.TEAMS); await knex.schema.dropTable(MetaTable.TEAMS);
await knex.schema.dropTable(MetaTable.ORGS); await knex.schema.dropTable(MetaTable.ORGS_OLD);
}; };
const down = async (knex: Knex) => { const down = async (knex: Knex) => {
await knex.schema.createTable(MetaTable.ORGS, (table) => { await knex.schema.createTable(MetaTable.ORGS_OLD, (table) => {
table.string('id', 20).primary().notNullable(); table.string('id', 20).primary().notNullable();
table.string('title'); table.string('title');
@ -23,13 +23,13 @@ const down = async (knex: Knex) => {
table.string('title'); table.string('title');
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.timestamps(true, true); table.timestamps(true, true);
}); });
await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => { await knex.schema.createTable(MetaTable.TEAM_USERS, (table) => {
table.string('org_id', 20); table.string('org_id', 20);
table.foreign('org_id').references(`${MetaTable.ORGS}.id`); table.foreign('org_id').references(`${MetaTable.ORGS_OLD}.id`);
table.string('user_id', 20); table.string('user_id', 20);
table.foreign('user_id').references(`${MetaTable.USERS}.id`); table.foreign('user_id').references(`${MetaTable.USERS}.id`);
table.timestamps(true, true); table.timestamps(true, true);

27
packages/nocodb/src/meta/migrations/v2/nc_043_user_refresh_token.ts

@ -0,0 +1,27 @@
import type { Knex } from 'knex';
import { MetaTable } from '~/utils/globals';
const up = async (knex: Knex) => {
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.dropColumn('refresh_token');
});
await knex.schema.createTable(MetaTable.USER_REFRESH_TOKENS, (table) => {
table.string('fk_user_id', 20).index();
table.string('token', 255).index();
table.text('meta');
table.timestamp('expires_at').index();
table.timestamps(true, true);
});
};
const down = async (knex: Knex) => {
await knex.schema.dropTable(MetaTable.USER_REFRESH_TOKENS);
await knex.schema.alterTable(MetaTable.USERS, (table) => {
table.string('refresh_token', 255);
});
};
export { up, down };

7
packages/nocodb/src/models/CalendarViewColumn.ts

@ -79,18 +79,13 @@ export default class CalendarViewColumn {
insertObj.source_id = viewRef.source_id; insertObj.source_id = viewRef.source_id;
} }
const { id, fk_column_id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.CALENDAR_VIEW_COLUMNS, MetaTable.CALENDAR_VIEW_COLUMNS,
insertObj, insertObj,
); );
await NocoCache.set(
`${CacheScope.CALENDAR_VIEW_COLUMN}:${fk_column_id}`,
id,
);
{ {
const view = await View.get(column.fk_view_id, ncMeta); const view = await View.get(column.fk_view_id, ncMeta);
await View.clearSingleQueryCache(view.fk_model_id, [view], ncMeta); await View.clearSingleQueryCache(view.fk_model_id, [view], ncMeta);

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

@ -823,64 +823,33 @@ export default class Column<T = any> implements ColumnType {
); );
} }
// Grid View Columns // Delete from all view columns
await ncMeta.metaDelete(null, null, MetaTable.GRID_VIEW_COLUMNS, { const viewColumnTables = [
fk_column_id: col.id, MetaTable.GRID_VIEW_COLUMNS,
}); MetaTable.FORM_VIEW_COLUMNS,
const gridViewColumnId = await NocoCache.get( MetaTable.KANBAN_VIEW_COLUMNS,
`${CacheScope.GRID_VIEW_COLUMN}:${col.id}`, MetaTable.GALLERY_VIEW_COLUMNS,
CacheGetType.TYPE_STRING, ];
); const viewColumnCacheScope = [
if (gridViewColumnId) { CacheScope.GRID_VIEW_COLUMN,
await NocoCache.deepDel( CacheScope.FORM_VIEW_COLUMN,
`${CacheScope.GRID_VIEW_COLUMN}:${gridViewColumnId}`, CacheScope.KANBAN_VIEW_COLUMN,
CacheDelDirection.CHILD_TO_PARENT, CacheScope.GALLERY_VIEW_COLUMN,
); ];
}
for (let i = 0; i < viewColumnTables.length; i++) {
// Form View Columns const table = viewColumnTables[i];
await ncMeta.metaDelete(null, null, MetaTable.FORM_VIEW_COLUMNS, { const cacheScope = viewColumnCacheScope[i];
fk_column_id: col.id, const viewColumns = await ncMeta.metaList2(null, null, table, {
}); condition: { fk_column_id: id },
const formViewColumnId = await NocoCache.get( });
`${CacheScope.FORM_VIEW_COLUMN}:${col.id}`, await ncMeta.metaDelete(null, null, table, { fk_column_id: id });
CacheGetType.TYPE_STRING, for (const viewColumn of viewColumns) {
); await NocoCache.deepDel(
if (formViewColumnId) { `${cacheScope}:${viewColumn.id}`,
await NocoCache.deepDel( CacheDelDirection.CHILD_TO_PARENT,
`${CacheScope.FORM_VIEW_COLUMN}:${formViewColumnId}`, );
CacheDelDirection.CHILD_TO_PARENT, }
);
}
// Kanban View Columns
await ncMeta.metaDelete(null, null, MetaTable.KANBAN_VIEW_COLUMNS, {
fk_column_id: col.id,
});
const kanbanViewColumnId = await NocoCache.get(
`${CacheScope.KANBAN_VIEW_COLUMN}:${col.id}`,
CacheGetType.TYPE_STRING,
);
if (kanbanViewColumnId) {
await NocoCache.deepDel(
`${CacheScope.KANBAN_VIEW_COLUMN}:${kanbanViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
}
// Gallery View Column
await ncMeta.metaDelete(null, null, MetaTable.GALLERY_VIEW_COLUMNS, {
fk_column_id: col.id,
});
const galleryViewColumnId = await NocoCache.get(
`${CacheScope.GALLERY_VIEW_COLUMN}:${col.id}`,
CacheGetType.TYPE_STRING,
);
if (galleryViewColumnId) {
await NocoCache.deepDel(
`${CacheScope.GALLERY_VIEW_COLUMN}:${galleryViewColumnId}`,
CacheDelDirection.CHILD_TO_PARENT,
);
} }
// Get LTAR columns in which current column is referenced as foreign key // Get LTAR columns in which current column is referenced as foreign key

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

@ -92,15 +92,13 @@ export default class FormViewColumn implements FormColumnType {
insertObj.source_id = viewRef.source_id; insertObj.source_id = viewRef.source_id;
} }
const { id, fk_column_id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.FORM_VIEW_COLUMNS, MetaTable.FORM_VIEW_COLUMNS,
insertObj, insertObj,
); );
await NocoCache.set(`${CacheScope.FORM_VIEW_COLUMN}:${fk_column_id}`, id);
return this.get(id, ncMeta).then(async (viewColumn) => { return this.get(id, ncMeta).then(async (viewColumn) => {
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.FORM_VIEW_COLUMN, CacheScope.FORM_VIEW_COLUMN,

7
packages/nocodb/src/models/GalleryViewColumn.ts

@ -66,18 +66,13 @@ export default class GalleryViewColumn {
insertObj.source_id = viewRef.source_id; insertObj.source_id = viewRef.source_id;
} }
const { id, fk_column_id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.GALLERY_VIEW_COLUMNS, MetaTable.GALLERY_VIEW_COLUMNS,
insertObj, insertObj,
); );
await NocoCache.set(
`${CacheScope.GALLERY_VIEW_COLUMN}:${fk_column_id}`,
id,
);
// on new view column, delete any optimised single query cache // on new view column, delete any optimised single query cache
{ {
const view = await View.get(column.fk_view_id, ncMeta); const view = await View.get(column.fk_view_id, ncMeta);

2
packages/nocodb/src/models/GridViewColumn.ts

@ -107,8 +107,6 @@ export default class GridViewColumn implements GridColumnType {
insertObj, insertObj,
); );
await NocoCache.set(`${CacheScope.GRID_VIEW_COLUMN}:${fk_column_id}`, id);
await View.fixPVColumnForView(column.fk_view_id, ncMeta); await View.fixPVColumnForView(column.fk_view_id, ncMeta);
// on new view column, delete any optimised single query cache // on new view column, delete any optimised single query cache

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

@ -63,15 +63,13 @@ export default class KanbanViewColumn implements KanbanColumnType {
insertObj.source_id = viewRef.source_id; insertObj.source_id = viewRef.source_id;
} }
const { id, fk_column_id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.KANBAN_VIEW_COLUMNS, MetaTable.KANBAN_VIEW_COLUMNS,
insertObj, insertObj,
); );
await NocoCache.set(`${CacheScope.KANBAN_VIEW_COLUMN}:${fk_column_id}`, id);
return this.get(id, ncMeta).then(async (kanbanViewColumn) => { return this.get(id, ncMeta).then(async (kanbanViewColumn) => {
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.KANBAN_VIEW_COLUMN, CacheScope.KANBAN_VIEW_COLUMN,

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

@ -57,15 +57,13 @@ export default class MapViewColumn {
insertObj.source_id = viewRef.source_id; insertObj.source_id = viewRef.source_id;
} }
const { id, fk_column_id } = await ncMeta.metaInsert2( const { id } = await ncMeta.metaInsert2(
null, null,
null, null,
MetaTable.MAP_VIEW_COLUMNS, MetaTable.MAP_VIEW_COLUMNS,
insertObj, insertObj,
); );
await NocoCache.set(`${CacheScope.MAP_VIEW_COLUMN}:${fk_column_id}`, id);
return this.get(id, ncMeta).then(async (viewCol) => { return this.get(id, ncMeta).then(async (viewCol) => {
await NocoCache.appendToList( await NocoCache.appendToList(
CacheScope.MAP_VIEW_COLUMN, CacheScope.MAP_VIEW_COLUMN,

20
packages/nocodb/src/models/Model.ts

@ -9,7 +9,7 @@ import dayjs from 'dayjs';
import type { BoolType, TableReqType, TableType } from 'nocodb-sdk'; import type { BoolType, TableReqType, TableType } from 'nocodb-sdk';
import type { XKnex } from '~/db/CustomKnex'; import type { XKnex } from '~/db/CustomKnex';
import type { LinkToAnotherRecordColumn } from '~/models/index'; import type { LinksColumn, LinkToAnotherRecordColumn } from '~/models/index';
import Hook from '~/models/Hook'; import Hook from '~/models/Hook';
import Audit from '~/models/Audit'; import Audit from '~/models/Audit';
import View from '~/models/View'; import View from '~/models/View';
@ -817,6 +817,24 @@ export default class Model implements TableType {
} }
} }
// use set to avoid duplicate
const relatedModelIds = new Set<string>();
// clear all single query cache of related views
for (const col of model.columns) {
if (!isLinksOrLTAR(col)) continue;
const colOptions = await col.getColOptions<
LinkToAnotherRecordColumn | LinksColumn
>();
relatedModelIds.add(colOptions?.fk_related_model_id);
}
await Promise.all(
Array.from(relatedModelIds).map(async (modelId: string) => {
await View.clearSingleQueryCache(modelId, null, ncMeta);
}),
);
return true; return true;
} }

22
packages/nocodb/src/models/User.ts

@ -9,7 +9,7 @@ import {
CacheScope, CacheScope,
MetaTable, MetaTable,
} from '~/utils/globals'; } from '~/utils/globals';
import { Base, BaseUser } from '~/models'; import { Base, BaseUser, UserRefreshToken } from '~/models';
import { sanitiseUserObj } from '~/utils'; import { sanitiseUserObj } from '~/utils';
export default class User implements UserType { export default class User implements UserType {
@ -20,7 +20,6 @@ export default class User implements UserType {
password?: string; password?: string;
salt?: string; salt?: string;
refresh_token?: string;
invite_token?: string; invite_token?: string;
invite_token_expires?: number | Date; invite_token_expires?: number | Date;
reset_password_expires?: number | Date; reset_password_expires?: number | Date;
@ -50,7 +49,6 @@ export default class User implements UserType {
'email', 'email',
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',
@ -91,7 +89,6 @@ export default class User implements UserType {
'email', 'email',
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',
@ -184,9 +181,21 @@ export default class User implements UserType {
} }
static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) { static async getByRefreshToken(refresh_token, ncMeta = Noco.ncMeta) {
return await ncMeta.metaGet2(null, null, MetaTable.USERS, { const userRefreshToken = await UserRefreshToken.getByToken(
refresh_token, refresh_token,
}); ncMeta,
);
if (!userRefreshToken) {
return null;
}
return await ncMeta.metaGet2(
null,
null,
MetaTable.USERS,
userRefreshToken.fk_user_id,
);
} }
public static async list( public static async list(
@ -262,6 +271,7 @@ export default class User implements UserType {
args: { args: {
user?: User; user?: User;
baseId?: string; baseId?: string;
orgId?: string;
}, },
ncMeta = Noco.ncMeta, ncMeta = Noco.ncMeta,
) { ) {

122
packages/nocodb/src/models/UserRefreshToken.ts

@ -0,0 +1,122 @@
import process from 'process';
import dayjs from 'dayjs';
import Noco from '~/Noco';
import { extractProps } from '~/helpers/extractProps';
import { MetaTable } from '~/utils/globals';
import { parseMetaProp, stringifyMetaProp } from '~/utils/modelUtils';
const NC_REFRESH_TOKEN_EXP_IN_DAYS =
parseInt(process.env.NC_REFRESH_TOKEN_EXP_IN_DAYS, 10) || 90;
// throw error if user provided invalid value
if (NC_REFRESH_TOKEN_EXP_IN_DAYS <= 0) {
throw new Error('NC_REFRESH_TOKEN_EXP_IN_DAYS must be a positive number');
}
export default class UserRefreshToken {
fk_user_id: string;
token: string;
expires_at: any;
meta?: any;
created_at?: any;
updated_at?: any;
public static async insert(
userRefreshToken: Partial<UserRefreshToken>,
ncMeta = Noco.ncMeta,
) {
// clear old invalid tokens before inserting new one
// todo: verify the populated sql query
await ncMeta.metaDelete(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
fk_user_id: userRefreshToken.fk_user_id,
},
{
expires_at: {
lt: dayjs().toDate(),
},
},
);
const insertObj = extractProps(userRefreshToken, [
'fk_user_id',
'token',
'expires_at',
'meta',
]);
// set expiry based on the env or default value
if (!('expires_at' in insertObj)) {
insertObj.expires_at = dayjs()
.add(NC_REFRESH_TOKEN_EXP_IN_DAYS, 'day')
.toDate();
}
if ('meta' in insertObj) {
insertObj.meta = stringifyMetaProp(insertObj);
}
await ncMeta.metaInsert2(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
insertObj,
true,
);
return insertObj;
}
static async updateOldToken(
oldToken: string,
newToken: string,
ncMeta = Noco.ncMeta,
) {
return await ncMeta.metaUpdate(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
token: newToken,
expires_at: dayjs().add(NC_REFRESH_TOKEN_EXP_IN_DAYS, 'day').toDate(),
},
{
token: oldToken,
},
);
}
static async deleteToken(token: string, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(null, null, MetaTable.USER_REFRESH_TOKENS, {
token,
});
}
static async deleteAllUserToken(userId: string, ncMeta = Noco.ncMeta) {
return await ncMeta.metaDelete(null, null, MetaTable.USER_REFRESH_TOKENS, {
fk_user_id: userId,
});
}
static async getByToken(
token: string,
ncMeta = Noco.ncMeta,
): Promise<UserRefreshToken> {
const userToken = await ncMeta.metaGet2(
null,
null,
MetaTable.USER_REFRESH_TOKENS,
{
token,
},
);
if (!userToken) return null;
userToken.meta = parseMetaProp(userToken);
return userToken;
}
}

3
packages/nocodb/src/models/View.ts

@ -1898,6 +1898,9 @@ export default class View implements ViewType {
'fk_parent_id', 'fk_parent_id',
'is_group', 'is_group',
'logical_op', 'logical_op',
'base_id',
'source_id',
'order',
]), ]),
fk_view_id: view_id, fk_view_id: view_id,
id: generatedId, id: generatedId,

1
packages/nocodb/src/models/index.ts

@ -40,3 +40,4 @@ export { default as View } from './View';
export { default as LinksColumn } from './LinksColumn'; export { default as LinksColumn } from './LinksColumn';
export { default as Notification } from './Notification'; export { default as Notification } from './Notification';
export { default as PresignedUrl } from './PresignedUrl'; export { default as PresignedUrl } from './PresignedUrl';
export { default as UserRefreshToken } from './UserRefreshToken';

4
packages/nocodb/src/services/public-metas.service.ts

@ -41,6 +41,10 @@ export class PublicMetasService {
view.model.columns = view.columns view.model.columns = view.columns
.filter((c) => { .filter((c) => {
const column = view.model.columnsById[c.fk_column_id]; const column = view.model.columnsById[c.fk_column_id];
// Check if column exists to prevent processing non-existent columns
if (!column) return false;
return ( return (
c.show || c.show ||
(column.rqd && !column.cdf && !column.ai) || (column.rqd && !column.cdf && !column.ai) ||

36
packages/nocodb/src/services/users/users.service.ts

@ -21,7 +21,7 @@ import { validatePayload } from '~/helpers';
import { MetaService } from '~/meta/meta.service'; import { MetaService } from '~/meta/meta.service';
import { MetaTable } from '~/utils/globals'; import { MetaTable } from '~/utils/globals';
import Noco from '~/Noco'; import Noco from '~/Noco';
import { Store, User } from '~/models'; import { Store, User, UserRefreshToken } from '~/models';
import { randomTokenString } from '~/helpers/stringHelpers'; import { randomTokenString } from '~/helpers/stringHelpers';
import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2'; import NcPluginMgrv2 from '~/helpers/NcPluginMgrv2';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
@ -370,9 +370,9 @@ export class UsersService {
NcError.badRequest(`Missing refresh token`); NcError.badRequest(`Missing refresh token`);
} }
const user = await User.getByRefreshToken( const oldRefreshToken = param.req.cookies.refresh_token;
param.req.cookies.refresh_token,
); const user = await User.getByRefreshToken(oldRefreshToken);
if (!user) { if (!user) {
NcError.badRequest(`Invalid refresh token`); NcError.badRequest(`Invalid refresh token`);
@ -380,10 +380,12 @@ export class UsersService {
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { try {
email: user.email, await UserRefreshToken.updateOldToken(oldRefreshToken, refreshToken);
refresh_token: refreshToken, } catch (error) {
}); console.error('Failed to update old refresh token:', error);
NcError.internalServerError('Failed to update refresh token');
}
setTokenCookie(param.res, refreshToken); setTokenCookie(param.res, refreshToken);
@ -495,9 +497,9 @@ export class UsersService {
const refreshToken = randomTokenString(); const refreshToken = randomTokenString();
await User.update(user.id, { await UserRefreshToken.insert({
refresh_token: refreshToken, token: refreshToken,
email: user.email, fk_user_id: user.id,
}); });
setTokenCookie(param.res, refreshToken); setTokenCookie(param.res, refreshToken);
@ -532,9 +534,10 @@ export class UsersService {
const user = (param.req as any).user; const user = (param.req as any).user;
if (user?.id) { if (user?.id) {
await User.update(user.id, { await User.update(user.id, {
refresh_token: null,
token_version: randomTokenString(), token_version: randomTokenString(),
}); });
// todo: clear only token present in cookie to avoid invalidating all refresh token
await UserRefreshToken.deleteAllUserToken(user.id);
} }
return { msg: 'Signed out successfully' }; return { msg: 'Signed out successfully' };
} catch (e) { } catch (e) {
@ -572,10 +575,15 @@ export class UsersService {
} }
await User.update(user.id, { await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user['token_version'], token_version: user['token_version'],
}); });
await UserRefreshToken.insert({
token: refreshToken,
fk_user_id: user.id,
meta: req.user?.extra,
});
setTokenCookie(res, refreshToken); setTokenCookie(res, refreshToken);
} }
} }

4
packages/nocodb/src/services/views.service.ts

@ -76,6 +76,10 @@ export class ViewsService {
}) { }) {
const model = await Model.get(param.tableId); const model = await Model.get(param.tableId);
if (!model) {
NcError.tableNotFound(param.tableId);
}
const viewList = await xcVisibilityMetaGet({ const viewList = await xcVisibilityMetaGet({
baseId: model.base_id, baseId: model.base_id,
models: [model], models: [model],

7
packages/nocodb/src/utils/globals.ts

@ -29,7 +29,7 @@ export enum MetaTable {
KANBAN_VIEW = 'nc_kanban_view_v2', KANBAN_VIEW = 'nc_kanban_view_v2',
KANBAN_VIEW_COLUMNS = 'nc_kanban_view_columns_v2', KANBAN_VIEW_COLUMNS = 'nc_kanban_view_columns_v2',
USERS = 'nc_users_v2', USERS = 'nc_users_v2',
ORGS = 'nc_orgs_v2', ORGS_OLD = 'nc_orgs_v2',
TEAMS = 'nc_teams_v2', TEAMS = 'nc_teams_v2',
TEAM_USERS = 'nc_team_users_v2', TEAM_USERS = 'nc_team_users_v2',
VIEWS = 'nc_views_v2', VIEWS = 'nc_views_v2',
@ -46,6 +46,7 @@ export enum MetaTable {
MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2', MAP_VIEW_COLUMNS = 'nc_map_view_columns_v2',
STORE = 'nc_store', STORE = 'nc_store',
NOTIFICATION = 'notification', NOTIFICATION = 'notification',
USER_REFRESH_TOKENS = 'nc_user_refresh_tokens',
} }
export enum MetaTableOldV2 { export enum MetaTableOldV2 {
@ -60,7 +61,7 @@ export const orderedMetaTables = [
MetaTable.AUDIT, MetaTable.AUDIT,
MetaTable.TEAM_USERS, MetaTable.TEAM_USERS,
MetaTable.TEAMS, MetaTable.TEAMS,
MetaTable.ORGS, MetaTable.ORGS_OLD,
MetaTable.PROJECT_USERS, MetaTable.PROJECT_USERS,
MetaTable.USERS, MetaTable.USERS,
MetaTable.MAP_VIEW, MetaTable.MAP_VIEW,
@ -151,7 +152,7 @@ export enum CacheScope {
MAP_VIEW_COLUMN = 'mapViewColumn', MAP_VIEW_COLUMN = 'mapViewColumn',
KANBAN_VIEW_COLUMN = 'kanbanViewColumn', KANBAN_VIEW_COLUMN = 'kanbanViewColumn',
USER = 'user', USER = 'user',
ORGS = 'orgs', ORGS_OLD = 'orgs',
TEAM = 'team', TEAM = 'team',
TEAM_USER = 'teamUser', TEAM_USER = 'teamUser',
VIEW = 'view', VIEW = 'view',

1
packages/nocodb/src/utils/sanitiseUserObj.ts

@ -1,7 +1,6 @@
const ignoreKeys = new Set([ const ignoreKeys = new Set([
'password', 'password',
'salt', 'salt',
'refresh_token',
'invite_token', 'invite_token',
'invite_token_expires', 'invite_token_expires',
'reset_password_expires', 'reset_password_expires',

6
pnpm-lock.yaml

@ -23909,6 +23909,9 @@ packages:
/sqlite3@5.1.6: /sqlite3@5.1.6:
resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==}
requiresBuild: true requiresBuild: true
peerDependenciesMeta:
node-gyp:
optional: true
dependencies: dependencies:
'@mapbox/node-pre-gyp': 1.0.11 '@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 4.3.0 node-addon-api: 4.3.0
@ -23923,6 +23926,9 @@ packages:
/sqlite3@5.1.7: /sqlite3@5.1.7:
resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==}
requiresBuild: true requiresBuild: true
peerDependenciesMeta:
node-gyp:
optional: true
dependencies: dependencies:
bindings: 1.5.0 bindings: 1.5.0
node-addon-api: 7.0.0 node-addon-api: 7.0.0

110
scripts/docs/fr/120.collaboration/fr-030.share-base.md

@ -0,0 +1,110 @@
***
titre : "Partager la base"
description: "Procédures pour partager publiquement une base et générer une iframe intégrée"
balises : \["Collaboration", "Bases", "Partage"]
mots-clés : \["Base NocoDB", "base de partage", "collaboration de base", "actions de base", "paramètres de base"]
-----------------------------------------------------------------------------------------------------------------
Pour partager une base, suivez les étapes ci-dessous :
1. Accédez au coin supérieur droit de la barre de navigation supérieure et cliquez sur le`Share`bouton.
2. In the `Shared base` section, toggle the switch to `Enable public access`afin d'activer la fonctionnalité de base partagée.
3. Le lien généré pour la base partagée sera affiché ci-dessus et pourra être utilisé pour partager ce projet avec d'autres. Pour copier l'URL, cliquez simplement sur le`Copy Link` option.
![Share base](/img/v2/base/share-base-1.png)
![Share base](/img/v2/base/share-base-2.png)
## Copy base
Le`Copy base`La fonctionnalité permet aux utilisateurs de créer une copie de la base (base d'importation) dans leur propre espace de travail. Cette fonctionnalité est également utile pour les utilisateurs qui souhaitent utiliser une base comme modèle pour de futurs projets. Pour copier une base, suivez les étapes ci-dessous :
1. Access shared base URL that you wish to copy.
2. Clique sur le`Copy base` button located in the top right corner of the toolbar.
3. Un modal apparaîtra, vous invitant à sélectionner l'espace de travail dans lequel vous souhaitez copier la base. Sélectionnez l'espace de travail souhaité
4. Configurez si vous souhaitez copier la base avec ou sans données/vues.
5. Clique sur le`Copy base`bouton pour terminer le processus.
![Copy base](/img/v2/base/share-base-copy-base.png)![Copy base](/img/v2/base/share-base-copy-base-2.png)
## Modifier la base de partage
Modifier le`Share base`Le paramètre rendra le généré précédemment`Share base`lien invalide et générer un nouveau lien à sa place.
Voici les étapes pour le modifier :
1. Clique sur le`Share`bouton situé dans le coin supérieur droit de la barre d’outils.
2. Activez l'option intitulée`Enable public access`pour désactiver le partage de base.
3. Basculez la même option,`Enable public access,`pour réactiver le partage de base, générant ensuite un nouveau lien.
![Enable public access](/img/v2/base/share-base-enable-public-access.png)
## Désactiver la base de partage
Désactivation`Share base`rendra le généré précédemment`Share base`lien invalide
Voici les étapes pour le désactiver :
1. Cliquez sur le bouton « Partager » situé dans le coin supérieur droit de la barre d'outils.
2. Activez l'option intitulée`Enable public access` to deactivate the base share.
![Enable public access](/img/v2/base/share-base-enable-public-access.png)
## Share base Access Permissions
La « Base partagée » peut être configurée selon deux modes :
1. **Téléspectateur**- Les utilisateurs disposant du lien fourni auront**lecture seulement** access to the base data.
2. **Éditeur**- Les utilisateurs disposant du lien fourni auront**read and write** access to the base data.
:::note
* L'autorisation d'accès par défaut est définie sur`Viewer`
* Base partagée avec`Editor`l'autorisation d'accès n'est actuellement disponible que dans la version auto-hébergée
:::
Basculer`Enable Editor Access`bouton pour configurer les autorisations comme vous le souhaitez![Share base edit access](/img/v2/base/share-base-edit-access.png)
## Cadre intégrable
L'interface NocoDB peut être intégrée de manière transparente aux applications existantes grâce à l'utilisation du[IFRAME HTML](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)attribut. Cette fonctionnalité permet aux utilisateurs d'intégrer l'interface NocoDB dans leurs applications, permettant une expérience utilisateur unifiée. Pour générer le code HTML intégrable, procédez comme suit :
**Pour générer du code HTML intégrable :**
1. Click the `Share`bouton situé dans le coin supérieur droit de la barre d’outils.
2. Au sein du`Shared base link`onglet, sélectionnez le bouton pour copier le`Embeddable HTML code`dans votre presse-papiers.
![Share base iFrame](/img/v2/base/share-base-iframe.png)
Exemple:
```html
<iframe
class="nc-embed"
src="https://nocodb-nocodb-rsyir.ondigitalocean.app/dashboard/#/nc/base/e3bba9df-4fc1-4d11-b7ce-41c4a3ad6810?embed"
frameBorder="0"
width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"
>
</iframe>
```
### Intégrer dans le corps HTML de l'application
Exemple de code avec iframe intégré généré ci-dessus
```html
<!DOCTYPE html>
<html>
<body>
<iframe
class="nc-embed"
src="http://localhost:3000/#/nc/base/7d4b551c-b5e0-41c9-a87b-f3984c21d2c7?embed"
frameBorder="0"
width="100%"
height="700"
style="background: transparent; "
></iframe>
</body>
</html>
```

2
tests/playwright/tests/db/views/viewCalendar.spec.ts

@ -288,7 +288,7 @@ test.describe('Calendar View', () => {
await dashboard.viewSidebar.deleteView({ title: 'Calendar' }); await dashboard.viewSidebar.deleteView({ title: 'Calendar' });
}); });
test('Calendar Drag and Drop & Undo Redo Operations', async () => { test.skip('Calendar Drag and Drop & Undo Redo Operations', async () => {
test.slow(); test.slow();
await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` }); await dashboard.treeView.openBase({ title: `xcdb${context.workerId}` });

Loading…
Cancel
Save