Browse Source

Merge branch 'develop' into 8333-ops-auto-upstall-script-enhancements

pull/8373/head
Rohit 2 months ago committed by GitHub
parent
commit
97beba6be6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      docker-compose/setup-script/noco.sh
  2. 4
      packages/nc-gui/assets/nc-icons/arrow-up-right.svg
  3. 5
      packages/nc-gui/assets/nc-icons/control-panel.svg
  4. 4
      packages/nc-gui/assets/nc-icons/home.svg
  5. 10
      packages/nc-gui/assets/nc-icons/office.svg
  6. 11
      packages/nc-gui/assets/nc-icons/slash.svg
  7. 4
      packages/nc-gui/assets/nc-icons/workspace.svg
  8. 1
      packages/nc-gui/components.d.ts
  9. 2
      packages/nc-gui/components/cell/Email.vue
  10. 4
      packages/nc-gui/components/cell/Url.vue
  11. 3
      packages/nc-gui/components/dashboard/Sidebar/EEMenuOption.vue
  12. 6
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  13. 4
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  14. 3
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  15. 208
      packages/nc-gui/components/dlg/InviteDlg.vue
  16. 3
      packages/nc-gui/components/general/BaseIconColorPicker.vue
  17. 26
      packages/nc-gui/components/general/CopyButton.vue
  18. 2
      packages/nc-gui/components/general/WorkspaceIcon.vue
  19. 7
      packages/nc-gui/components/nc/Badge.vue
  20. 2
      packages/nc-gui/components/nc/ErrorBoundary.vue
  21. 2
      packages/nc-gui/components/nc/Select.vue
  22. 128
      packages/nc-gui/components/project/AccessSettings.vue
  23. 48
      packages/nc-gui/components/project/View.vue
  24. 8
      packages/nc-gui/components/roles/Badge.vue
  25. 8
      packages/nc-gui/components/roles/Selector.vue
  26. 2
      packages/nc-gui/components/shared-view/Calendar.vue
  27. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  28. 5
      packages/nc-gui/components/shared-view/Grid.vue
  29. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  30. 2
      packages/nc-gui/components/shared-view/Map.vue
  31. 136
      packages/nc-gui/components/smartsheet/Cell.vue
  32. 1
      packages/nc-gui/components/smartsheet/Form.vue
  33. 17
      packages/nc-gui/components/smartsheet/Row.vue
  34. 5
      packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue
  35. 4
      packages/nc-gui/components/smartsheet/TableDataCell.vue
  36. 61
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  37. 23
      packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue
  38. 215
      packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue
  39. 36
      packages/nc-gui/components/smartsheet/calendar/MonthView.vue
  40. 41
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue
  41. 601
      packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue
  42. 4
      packages/nc-gui/components/smartsheet/column/DefaultValue.vue
  43. 4
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  44. 21
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  45. 62
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  46. 1
      packages/nc-gui/components/smartsheet/grid/GroupByTable.vue
  47. 943
      packages/nc-gui/components/smartsheet/grid/Table.vue
  48. 4
      packages/nc-gui/components/smartsheet/grid/index.vue
  49. 30
      packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts
  50. 2
      packages/nc-gui/components/smartsheet/header/Menu.vue
  51. 4
      packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue
  52. 4
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  53. 2
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  54. 10
      packages/nc-gui/components/tabs/Smartsheet.vue
  55. 99
      packages/nc-gui/components/virtual-cell/BelongsTo.vue
  56. 127
      packages/nc-gui/components/virtual-cell/HasMany.vue
  57. 136
      packages/nc-gui/components/virtual-cell/Links.vue
  58. 127
      packages/nc-gui/components/virtual-cell/ManyToMany.vue
  59. 93
      packages/nc-gui/components/virtual-cell/OneToOne.vue
  60. 2
      packages/nc-gui/components/virtual-cell/QrCode.vue
  61. 117
      packages/nc-gui/components/virtual-cell/components/Header.vue
  62. 85
      packages/nc-gui/components/virtual-cell/components/LinkRecordDropdown.vue
  63. 416
      packages/nc-gui/components/virtual-cell/components/LinkedItems.vue
  64. 291
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  65. 341
      packages/nc-gui/components/virtual-cell/components/UnLinkedItems.vue
  66. 184
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  67. 40
      packages/nc-gui/components/workspace/Settings.vue
  68. 91
      packages/nc-gui/components/workspace/View.vue
  69. 3
      packages/nc-gui/composables/useCalendarViewStore.ts
  70. 66
      packages/nc-gui/composables/useData.ts
  71. 2
      packages/nc-gui/composables/useExpandedFormDetached/index.ts
  72. 6
      packages/nc-gui/composables/useExpandedFormStore.ts
  73. 28
      packages/nc-gui/composables/useLTARStore.ts
  74. 4
      packages/nc-gui/composables/useMetas.ts
  75. 55
      packages/nc-gui/composables/useMultiSelect/index.ts
  76. 23
      packages/nc-gui/composables/useOrganization.ts
  77. 6
      packages/nc-gui/composables/useSharedFormViewStore.ts
  78. 249
      packages/nc-gui/composables/useSmartsheetLtarHelpers.ts
  79. 266
      packages/nc-gui/composables/useSmartsheetRowStore.ts
  80. 2
      packages/nc-gui/composables/useSmartsheetStore.ts
  81. 11
      packages/nc-gui/composables/useUserSorts.ts
  82. 8
      packages/nc-gui/composables/useViewColumns.ts
  83. 897
      packages/nc-gui/composables/useViewGroupBy.ts
  84. 2
      packages/nc-gui/context/index.ts
  85. 70
      packages/nc-gui/lang/ar.json
  86. 70
      packages/nc-gui/lang/bn_IN.json
  87. 70
      packages/nc-gui/lang/cs.json
  88. 70
      packages/nc-gui/lang/da.json
  89. 70
      packages/nc-gui/lang/de.json
  90. 70
      packages/nc-gui/lang/en.json
  91. 70
      packages/nc-gui/lang/es.json
  92. 70
      packages/nc-gui/lang/eu.json
  93. 70
      packages/nc-gui/lang/fa.json
  94. 70
      packages/nc-gui/lang/fi.json
  95. 70
      packages/nc-gui/lang/fr.json
  96. 70
      packages/nc-gui/lang/he.json
  97. 70
      packages/nc-gui/lang/hi.json
  98. 70
      packages/nc-gui/lang/hr.json
  99. 70
      packages/nc-gui/lang/id.json
  100. 70
      packages/nc-gui/lang/it.json
  101. Some files were not shown because too many files have changed in this diff Show More

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

@ -167,8 +167,8 @@ stop_service() {
show_logs_sub_menu() {
clear
echo "Select a replica for $1:"
for i in $(seq 1 "$2"); do
echo "$i. $1 replica $i"
for i in $(seq 1 $2); do
echo "$i. \"$1\" replica $i"
done
echo "A. All"
echo "0. Back to Logs Menu"

4
packages/nc-gui/assets/nc-icons/arrow-up-right.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.66666 11.3334L11.3333 4.66675" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.66666 4.66675H11.3333V11.3334" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

5
packages/nc-gui/assets/nc-icons/control-panel.svg

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 14V6" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 6H14" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

4
packages/nc-gui/assets/nc-icons/home.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5.99992L8 1.33325L14 5.99992V13.3333C14 13.6869 13.8595 14.026 13.6095 14.2761C13.3594 14.5261 13.0203 14.6666 12.6667 14.6666H3.33333C2.97971 14.6666 2.64057 14.5261 2.39052 14.2761C2.14048 14.026 2 13.6869 2 13.3333V5.99992Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 14.6667V8H10V14.6667" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

10
packages/nc-gui/assets/nc-icons/office.svg

@ -0,0 +1,10 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_130_18151)">
<path d="M31 31.3606H0.999999C0.800999 31.3606 0.639999 31.1996 0.639999 31.0006V1.00063C0.639999 0.801625 0.800999 0.640625 0.999999 0.640625H19C19.199 0.640625 19.36 0.801625 19.36 1.00063V3.64062H25C25.199 3.64062 25.36 3.80163 25.36 4.00063V7.64062H31C31.199 7.64062 31.36 7.80163 31.36 8.00063V31.0006C31.36 31.1996 31.199 31.3606 31 31.3606ZM19.36 30.6406H30.64V8.36062H19.36V30.6406ZM12.36 30.6406H18.64V1.36063H1.36V30.6406H7.64V23.0006C7.64 22.8016 7.801 22.6406 8 22.6406H12C12.199 22.6406 12.36 22.8016 12.36 23.0006V30.6406ZM8.36 30.6406H11.64V23.3606H8.36V30.6406ZM19.36 7.64062H24.639V4.36063H19.36V7.64062ZM27.36 25.0006H26.64V23.0006H27.361L27.36 25.0006ZM23.36 25.0006H22.64V23.0006H23.361L23.36 25.0006ZM16.36 25.0006H15.64V23.0006H16.36V25.0006ZM4.36 25.0006H3.64V23.0006H4.36V25.0006ZM27.36 19.0006H26.64V17.0006H27.361L27.36 19.0006ZM23.36 19.0006H22.64V17.0006H23.361L23.36 19.0006ZM16.36 19.0006H15.64V17.0006H16.36V19.0006ZM12.36 19.0006H11.64V17.0006H12.36V19.0006ZM8.36 19.0006H7.64V17.0006H8.36V19.0006ZM4.36 19.0006H3.64V17.0006H4.36V19.0006ZM27.36 13.0006H26.64V11.0006H27.361L27.36 13.0006ZM23.36 13.0006H22.64V11.0006H23.361L23.36 13.0006ZM16.36 13.0006H15.64V11.0006H16.36V13.0006ZM12.36 13.0006H11.64V11.0006H12.36V13.0006ZM8.36 13.0006H7.64V11.0006H8.36V13.0006ZM4.36 13.0006H3.64V11.0006H4.36V13.0006ZM16.36 7.00063H15.64V5.00063H16.36V7.00063ZM12.36 7.00063H11.64V5.00063H12.36V7.00063ZM8.36 7.00063H7.64V5.00063H8.36V7.00063ZM4.36 7.00063H3.64V5.00063H4.36V7.00063Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_130_18151">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

11
packages/nc-gui/assets/nc-icons/slash.svg

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="slash" clip-path="url(#clip0_311_1327)">
<path id="Vector" d="M7.99998 14.6668C11.6819 14.6668 14.6666 11.6821 14.6666 8.00016C14.6666 4.31826 11.6819 1.3335 7.99998 1.3335C4.31808 1.3335 1.33331 4.31826 1.33331 8.00016C1.33331 11.6821 4.31808 14.6668 7.99998 14.6668Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M3.28662 3.28662L12.7133 12.7133" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_311_1327">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 732 B

4
packages/nc-gui/assets/nc-icons/workspace.svg

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" fill="none"/>
<rect x="0.5" y="0.5" width="15" height="15" rx="3.5" stroke="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

1
packages/nc-gui/components.d.ts vendored

@ -52,6 +52,7 @@ declare module 'vue' {
ARadio: typeof import('ant-design-vue/es')['Radio']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARate: typeof import('ant-design-vue/es')['Rate']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']

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

@ -109,7 +109,7 @@ watch(
<nuxt-link
v-else-if="validEmail"
no-ref
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link"
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link max-w-full"
:href="`mailto:${vModel}`"
target="_blank"
:tabindex="readOnly ? -1 : 0"

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

@ -121,7 +121,7 @@ watch(
v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch
no-rel
class="py-1 z-3 underline hover:opacity-75 nc-cell-field-link"
class="py-1 z-3 underline hover:opacity-75 nc-cell-field-link max-w-full"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0"
@ -133,7 +133,7 @@ watch(
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
no-prefetch
no-rel
class="py-1 z-3 w-full h-full text-center !no-underline hover:opacity-75 nc-cell-field-link"
class="py-1 z-3 w-full h-full text-center !no-underline hover:opacity-75 nc-cell-field-link max-w-full"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0"

3
packages/nc-gui/components/dashboard/Sidebar/EEMenuOption.vue

@ -0,0 +1,3 @@
<template>
<span></span>
</template>

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

@ -20,12 +20,14 @@ const { isMobileMode } = useGlobal()
const logout = async () => {
isLoggingOut.value = true
try {
const isSsoUser = !!(user?.value as any)?.sso_client_id
await signOut(false)
// No need as all stores are cleared on signout
// await clearWorkspaces()
await navigateTo('/signin')
await navigateTo(isSsoUser ? '/sso' : '/signin')
} catch (e) {
console.error(e)
} finally {
@ -167,6 +169,8 @@ onMounted(() => {
<NcDivider />
<DashboardSidebarEEMenuOption v-if="isEeUI" />
<nuxt-link v-e="['c:user:settings']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="ncSettings" class="menu-icon" /> {{ $t('title.accountSettings') }} </NcMenuItem>
</nuxt-link>

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

@ -290,11 +290,11 @@ const isEditBaseModalOpen = computed({
<template>
<div class="flex flex-row w-full h-full nc-data-sources-view">
<div class="flex flex-col w-full overflow-auto">
<div class="flex flex-row w-full justify-end mt-6 mb-5">
<div class="flex flex-row w-full justify-end mt-6.5 mb-2">
<NcButton
v-if="dataSourcesAwakened"
size="large"
class="z-10 !rounded-lg !px-2 mr-2.5"
class="z-10 !px-2"
type="primary"
@click="vState = DataSourcesSubTab.New"
>

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

@ -86,8 +86,7 @@ const customFormState = ref<ProjectCreateForm>({
const clientTypes = computed(() => {
return _clientTypes.filter((type) => {
// return appInfo.value?.ee || type.value !== ClientType.SNOWFLAKE
return type.value !== ClientType.SNOWFLAKE
return ![ClientType.SNOWFLAKE, ClientType.DATABRICKS].includes(type.value)
})
})

208
packages/nc-gui/components/project/ShareBaseDlg.vue → packages/nc-gui/components/dlg/InviteDlg.vue

@ -1,30 +1,43 @@
<script setup lang="ts">
import type { RoleLabels } from 'nocodb-sdk'
import { OrderedProjectRoles, ProjectRoles } from 'nocodb-sdk'
<script lang="ts" setup>
import { ProjectRoles, type RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractEmail } from '~/helpers/parsers/parserHelpers'
const props = defineProps<{
modelValue: boolean
type?: 'base' | 'workspace' | 'organization'
baseId?: string
emails?: string[]
workspaceId?: string
}>()
const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const inviteData = reactive({
email: '',
roles: ProjectRoles.NO_ACCESS,
})
const { baseRoles } = useRoles()
const { baseRoles, workspaceRoles } = useRoles()
const basesStore = useBases()
const { activeProjectId } = storeToRefs(basesStore)
const workspaceStore = useWorkspace()
const { createProjectUser } = basesStore
const { inviteCollaborator: inviteWsCollaborator } = workspaceStore
const dialogShow = useVModel(props, 'modelValue', emit)
const orderedRoles = computed(() => {
return props.type === 'base' ? ProjectRoles : WorkspaceUserRoles
})
const userRoles = computed(() => {
return props.type === 'base' ? baseRoles.value : workspaceRoles.value
})
const inviteData = reactive({
email: '',
roles: orderedRoles.value.NO_ACCESS,
})
const divRef = ref<HTMLDivElement>()
const focusRef = ref<HTMLInputElement>()
@ -35,23 +48,44 @@ const emailValidation = reactive({
message: '',
})
const allowedRoles = ref<ProjectRoles[]>([])
const singleEmailValue = ref('')
onMounted(async () => {
try {
const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => baseRoles.value && Object.keys(baseRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
allowedRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1).filter((r) => r)
const emailBadges = ref<Array<string>>([])
const allowedRoles = ref<[]>([])
const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
}
watch(dialogShow, async (newVal) => {
if (newVal) {
try {
// todo: enable after discussing with anbu
// const currentRoleIndex = Object.values(orderedRoles.value).findIndex(
// (role) => userRoles.value && Object.keys(userRoles.value).includes(role),
// )
// if (currentRoleIndex !== -1) {
allowedRoles.value = Object.values(orderedRoles.value) // .slice(currentRoleIndex + 1)
// }
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
if (props.emails) {
emailBadges.value = props.emails
}
setTimeout(() => {
focusOnDiv()
}, 100)
} else {
emailBadges.value = []
inviteData.email = ''
singleEmailValue.value = ''
}
})
const singleEmailValue = ref('')
const emailBadges = ref<Array<string>>([])
const insertOrUpdateString = (str: string) => {
// Check if the string already exists in the array
@ -84,7 +118,7 @@ const emailInputValidation = (input: string, isBulkEmailCopyPaste: boolean = fal
return true
}
const isInvitButtonDiabled = computed(() => {
const isInviteButtonDisabled = computed(() => {
if (!emailBadges.value.length && !singleEmailValue.value.length) {
return true
}
@ -95,7 +129,7 @@ const isInvitButtonDiabled = computed(() => {
watch(inviteData, (newVal) => {
// when user only want to enter a single email
// we dont convert that as badge
// we don't convert that as badge
const isSingleEmailValid = validateEmail(newVal.email)
if (isSingleEmailValid && !emailBadges.value.length) {
@ -105,7 +139,7 @@ watch(inviteData, (newVal) => {
}
singleEmailValue.value = ''
// when user enters multiple emails comma sepearted or space sepearted
// when user enters multiple emails comma separated or space separated
const isNewEmail = newVal.email.charAt(newVal.email.length - 1) === ',' || newVal.email.charAt(newVal.email.length - 1) === ' '
if (isNewEmail && newVal.email.trim().length) {
const emailToAdd = newVal.email.split(',')[0].trim() || newVal.email.split(' ')[0].trim()
@ -140,12 +174,6 @@ const handleEnter = () => {
emailValidation.isError = false
emailValidation.message = ''
}
const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
}
// remove one email per backspace
onKeyStroke('Backspace', () => {
if (isDivFocused.value && inviteData.email.length < 1) {
@ -197,7 +225,9 @@ const onPaste = (e: ClipboardEvent) => {
inviteData.email = ''
}
const inviteProjectCollaborator = async () => {
const workSpaces = ref<NcWorkspace[]>([])
const inviteCollaborator = async () => {
try {
const payloadData = singleEmailValue.value || emailBadges.value.join(',')
if (!payloadData.includes(',')) {
@ -207,10 +237,19 @@ const inviteProjectCollaborator = async () => {
emailValidation.message = 'invalid email'
}
}
await createProjectUser(activeProjectId.value!, {
email: payloadData,
roles: inviteData.roles,
} as unknown as User)
if (props.type === 'base' && props.baseId) {
await createProjectUser(props.baseId!, {
email: payloadData,
roles: inviteData.roles,
} as unknown as User)
} else if (props.type === 'workspace' && props.workspaceId) {
await inviteWsCollaborator(payloadData, inviteData.roles, props.workspaceId)
} else if (props.type === 'organization') {
// TODO: Add support for Bulk Workspace Invite
for (const workspace of workSpaces.value) {
await inviteWsCollaborator(payloadData, inviteData.roles, workspace.id)
}
}
message.success('Invitation sent successfully')
inviteData.email = ''
@ -223,40 +262,70 @@ const inviteProjectCollaborator = async () => {
}
}
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles)
const organizationStore = useOrganization()
const { listWorkspaces } = organizationStore
const { workspaces } = storeToRefs(organizationStore)
const workSpaceSelectList = computed(() => {
return workspaces.value.filter((w) => !workSpaces.value.find((ws) => ws.id === w.id))
})
const addToList = (workspaceId: string) => {
workSpaces.value.push(workspaces.value.find((w) => w.id === workspaceId)!)
}
const removeWorkspace = (workspaceId: string) => {
workSpaces.value = workSpaces.value.filter((w) => w.id !== workspaceId)
}
onMounted(async () => {
if (props.type === 'organization') {
await listWorkspaces()
}
})
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles | WorkspaceUserRoles)
</script>
<template>
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
:header="$t('activity.createTable')"
:show-separator="false"
size="medium"
class="nc-invite-dlg"
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
{{ $t('activity.addMember') }}
{{
type === 'organization'
? $t('labels.addMembersToOrganization')
: type === 'base'
? $t('activity.addMember')
: $t('activity.inviteToWorkspace')
}}
</div>
</template>
<div class="flex items-center justify-between gap-3 mt-2">
<div class="flex w-full flex-col">
<div class="flex w-full gap-4 flex-col">
<div class="flex justify-between gap-3 w-full">
<div
ref="divRef"
class="flex items-center border-1 gap-1 w-full overflow-x-auto nc-scrollbar-x-md items-center h-10 rounded-lg !min-w-96"
tabindex="0"
:class="{
'border-primary/100': isDivFocused,
'p-1': emailBadges?.length > 1,
}"
@click="focusOnDiv"
class="flex items-center border-1 gap-1 w-full overflow-x-scroll nc-scrollbar-x-md items-center h-10 rounded-lg !min-w-96"
tabindex="0"
@blur="isDivFocused = false"
@click="focusOnDiv"
>
<span
v-for="(email, index) in emailBadges"
:key="email"
class="border-1 text-gray-800 bg-gray-100 rounded-md flex items-center px-2 py-1"
class="border-1 text-gray-800 first:ml-1 bg-gray-100 rounded-md flex items-center px-2 py-1"
>
{{ email }}
<component
@ -272,38 +341,65 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
:placeholder="$t('activity.enterEmail')"
class="w-full min-w-36 outline-none px-2"
data-testid="email-input"
@keyup.enter="handleEnter"
@blur="isDivFocused = false"
@keyup.enter="handleEnter"
@paste.prevent="onPaste"
/>
</div>
<RolesSelector
size="lg"
class="nc-invite-role-selector"
:description="false"
:on-role-change="onRoleChange"
:role="inviteData.roles"
:roles="allowedRoles"
:on-role-change="onRoleChange"
:description="false"
class="!min-w-[152px] nc-invite-role-selector"
size="lg"
/>
</div>
<span v-if="emailValidation.isError && emailValidation.message" class="ml-2 text-red-500 text-[10px] mt-1.5">{{
emailValidation.message
}}</span>
<template v-if="type === 'organization'">
<NcSelect :placeholder="$t('labels.selectWorkspace')" size="middle" @change="addToList">
<a-select-option v-for="workspace in workSpaceSelectList" :key="workspace.id" :value="workspace.id">
{{ workspace.title }}
</a-select-option>
</NcSelect>
<div class="flex flex-wrap gap-2">
<NcBadge v-for="workspace in workSpaces" :key="workspace.id">
<div class="px-2 flex gap-2 items-center py-1">
<GeneralWorkspaceIcon :workspace="workspace" hide-label size="small" />
<span class="text-gray-600">
{{ workspace.title }}
</span>
<component :is="iconMap.close" class="w-3 h-3" @click="removeWorkspace(workspace.id)" />
</div>
</NcBadge>
</div>
</template>
</div>
</div>
<div class="flex mt-8 justify-end">
<div class="flex gap-2">
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton>
<NcButton
type="primary"
:disabled="isInviteButtonDisabled || emailValidation.isError"
size="medium"
:disabled="isInvitButtonDiabled || emailValidation.isError"
@click="inviteProjectCollaborator"
type="primary"
class="nc-invite-btn"
@click="inviteCollaborator"
>
{{ $t('activity.inviteToBase') }}
{{ type === 'base' ? $t('activity.inviteToBase') : $t('activity.inviteToWorkspace') }}
</NcButton>
</div>
</div>
</NcModal>
</template>
<style lang="scss" scoped>
:deep(.nc-invite-role-selector .nc-role-badge) {
@apply w-full;
}
</style>

3
packages/nc-gui/components/general/BaseIconColorPicker.vue

@ -7,7 +7,7 @@ const props = withDefaults(
defineProps<{
type?: NcProjectType | string
modelValue?: string
size?: 'small' | 'medium' | 'large' | 'xlarge'
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'
readonly?: boolean
iconClass?: string
}>(),
@ -62,6 +62,7 @@ watch(
:class="{
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,
'h-5 w-5 text-base': size === 'xsmall',
'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium',
'h-10 w-10 text-2xl': size === 'large',

26
packages/nc-gui/components/general/CopyButton.vue

@ -0,0 +1,26 @@
<script setup lang="ts">
import { useCopy } from '~/composables/useCopy'
const props = defineProps<{
content?: string
timeout?: number
}>()
const { copy } = useCopy()
const copied = ref(false)
const copyContent = async () => {
await copy(props.content || '')
copied.value = true
setTimeout(() => {
copied.value = false
}, props.timeout || 2000)
}
</script>
<template>
<NcButton size="xsmall" type="text" @click="copyContent">
<MdiCheck v-if="copied" class="h-3.5" />
<component :is="iconMap.copy" v-else class="text-gray-800" />
</NcButton>
</template>

2
packages/nc-gui/components/general/WorkspaceIcon.vue

@ -6,6 +6,7 @@ const props = defineProps<{
workspace: WorkspaceType | undefined
hideLabel?: boolean
size?: 'small' | 'medium' | 'large'
isRounded?: boolean
}>()
const workspaceColor = computed(() => {
@ -24,6 +25,7 @@ const size = computed(() => props.size || 'medium')
'min-w-4 w-4 h-4 rounded': size === 'small',
'min-w-6 w-6 h-6 rounded-md': size === 'medium',
'min-w-10 w-10 h-10 rounded-lg !text-base': size === 'large',
'!rounded-[50%]': props.isRounded,
}"
:style="{ backgroundColor: workspaceColor }"
>

7
packages/nc-gui/components/nc/Badge.vue

@ -4,17 +4,18 @@ const props = withDefaults(
color?: string
border?: boolean
size?: 'sm' | 'md' | 'lg'
rounded?: 'sm' | 'md' | 'lg'
}>(),
{
border: true,
size: 'sm',
rounded: 'md',
},
)
</script>
<template>
<div
class="rounded-md px-1 flex items-center"
:class="{
'border-purple-500 bg-purple-100': props.color === 'purple',
'border-blue-500 bg-blue-100': props.color === 'blue',
@ -28,7 +29,11 @@ const props = withDefaults(
'h-6': props.size === 'sm',
'h-8': props.size === 'md',
'h-10': props.size === 'lg',
'rounded-sm': props.rounded === 'sm',
'rounded-md': props.rounded === 'md',
'rounded-lg': props.rounded === 'lg',
}"
class="px-1 flex items-center"
>
<slot />
</div>

2
packages/nc-gui/components/nc/ErrorBoundary.vue

@ -20,7 +20,7 @@ export default {
onErrorCaptured((err) => {
if (import.meta.client && (!nuxtApp.isHydrating || !nuxtApp.payload.serverRendered)) {
console.log('UI Error :', err)
console.error('UI Error :', err)
emit('error', err)
error.value = err
return false

2
packages/nc-gui/components/nc/Select.vue

@ -3,6 +3,7 @@ const props = defineProps<{
value?: string | string[]
placeholder?: string
mode?: 'multiple' | 'tags'
size?: 'small' | 'middle' | 'large'
dropdownClassName?: string
showSearch?: boolean
// filterOptions is a function
@ -44,6 +45,7 @@ const onChange = (value: string) => {
<template>
<a-select
v-model:value="vModel"
:size="size"
:allow-clear="allowClear"
:disabled="loading"
:dropdown-class-name="dropdownClassName"

128
packages/nc-gui/components/project/AccessSettings.vue

@ -1,27 +1,44 @@
<script lang="ts" setup>
import {
OrderedProjectRoles,
OrgUserRoles,
ProjectRoles,
WorkspaceRolesToProjectRoles,
extractRolesObj,
parseStringDateTime,
timeAgo,
} from 'nocodb-sdk'
import type { Roles, WorkspaceUserRoles } from 'nocodb-sdk'
import { OrderedProjectRoles, OrgUserRoles, ProjectRoles, WorkspaceRolesToProjectRoles } from 'nocodb-sdk'
import type { User } from '#imports'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
const props = defineProps<{
baseId?: string
}>()
const basesStore = useBases()
const { getBaseUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore
const { activeProjectId } = storeToRefs(basesStore)
const { activeProjectId, bases } = storeToRefs(basesStore)
const { orgRoles, baseRoles } = useRoles()
const { orgRoles, baseRoles, loadRoles } = useRoles()
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = useUserSorts('Project')
const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN])
const orgStore = useOrg()
const { orgId } = storeToRefs(orgStore)
const isAdminPanel = inject(IsAdminPanelInj, ref(false))
const { $api } = useNuxtApp()
const currentBase = computedAsync(async () => {
let base
if (props.baseId) {
await loadRoles(props.baseId)
base = bases.value.get(props.baseId)
if (!base) {
base = await $api.base.read(props.baseId!)
}
} else {
base = bases.value.get(activeProjectId.value)
}
return base
})
const isInviteModalVisible = ref(false)
interface Collaborators {
@ -56,8 +73,9 @@ const sortedCollaborators = computed(() => {
const loadCollaborators = async () => {
try {
if (!currentBase.value) return
const { users, totalRows } = await getBaseUsers({
baseId: activeProjectId.value!,
baseId: currentBase.value.id!,
...(!userSearchText.value ? {} : ({ searchText: userSearchText.value } as any)),
force: true,
})
@ -69,12 +87,11 @@ const loadCollaborators = async () => {
.map((user: any) => ({
...user,
base_roles: user.roles,
roles: extractRolesObj(user.main_roles)?.[OrgUserRoles.SUPER_ADMIN]
? OrgUserRoles.SUPER_ADMIN
: user.roles ??
(user.workspace_roles
? WorkspaceRolesToProjectRoles[user.workspace_roles as WorkspaceUserRoles] ?? ProjectRoles.NO_ACCESS
: ProjectRoles.NO_ACCESS),
roles:
user.roles ??
(user.workspace_roles
? WorkspaceRolesToProjectRoles[user.workspace_roles as WorkspaceUserRoles] ?? ProjectRoles.NO_ACCESS
: ProjectRoles.NO_ACCESS),
})),
]
} catch (e: any) {
@ -93,7 +110,7 @@ const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
WorkspaceRolesToProjectRoles[currentCollaborator.workspace_roles as WorkspaceUserRoles] === roles &&
isEeUI)
) {
await removeProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
await removeProjectUser(currentBase.value.id!, currentCollaborator as unknown as User)
if (
currentCollaborator.workspace_roles &&
WorkspaceRolesToProjectRoles[currentCollaborator.workspace_roles as WorkspaceUserRoles] === roles &&
@ -105,11 +122,11 @@ const updateCollaborator = async (collab: any, roles: ProjectRoles) => {
}
} else if (currentCollaborator.base_roles) {
currentCollaborator.roles = roles
await updateProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
await updateProjectUser(currentBase.value.id!, currentCollaborator as unknown as User)
} else {
currentCollaborator.roles = roles
currentCollaborator.base_roles = roles
await createProjectUser(activeProjectId.value!, currentCollaborator as unknown as User)
await createProjectUser(currentBase.value.id!, currentCollaborator as unknown as User)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
@ -142,24 +159,50 @@ watch(isInviteModalVisible, () => {
loadCollaborators()
}
})
watch(currentBase, () => {
loadCollaborators()
})
</script>
<template>
<div class="nc-collaborator-table-container mt-4 nc-access-settings-view h-[calc(100vh-8rem)]">
<LazyProjectShareBaseDlg v-model:model-value="isInviteModalVisible" />
<div
:class="{
'px-6 ': isAdminPanel,
}"
class="nc-collaborator-table-container mt-4 nc-access-settings-view h-[calc(100vh-8rem)]"
>
<div v-if="isAdminPanel" class="font-bold w-full !mb-5 text-2xl" data-rec="true">
<div class="flex items-center gap-3">
<!-- TODO: @DarkPhoenix2704 -->
<NuxtLink
:href="`/admin/${orgId}/bases`"
class="!hover:(text-black underline-gray-600) !text-black !underline-transparent ml-0.75 max-w-1/4"
>
{{ $t('objects.projects') }}
</NuxtLink>
<span class="text-2xl"> / </span>
<GeneralBaseIconColorPicker readonly />
<span class="text-base">
{{ currentBase?.title }}
</span>
</div>
</div>
<LazyDlgInviteDlg v-model:model-value="isInviteModalVisible" :base-id="currentBase?.id" type="base" />
<div v-if="isLoading" class="nc-collaborators-list items-center justify-center">
<GeneralLoader size="xlarge" />
</div>
<template v-else>
<div class="w-full flex flex-row justify-between items-baseline max-w-350 mt-6.5 mb-2 pr-0.25">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md" :placeholder="$t('title.searchMembers')">
<div class="w-full flex flex-row justify-between items-center max-w-350 mt-6.5 mb-2 pr-0.25">
<a-input v-model:value="userSearchText" :placeholder="$t('title.searchMembers')" class="!max-w-90 !rounded-md mr-4">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<NcButton size="small" @click="isInviteModalVisible = true">
<div class="flex gap-1">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" class="w-4 h-4" />
{{ $t('activity.addMembers') }}
</div>
@ -188,7 +231,7 @@ watch(isInviteModalVisible, () => {
<div class="text-gray-700 user-access-grid flex items-center space-x-2">
<span>
{{ $t('general.access') }}
{{ $t('general.role') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.roles" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
@ -203,17 +246,16 @@ watch(isInviteModalVisible, () => {
>
<div class="flex gap-3 items-center users-email-grid">
<GeneralUserIcon size="base" :email="collab.email" />
<NcTooltip v-if="collab.display_name">
<template #title>
<div class="flex flex-col">
<div class="flex gap-3">
<span class="text-gray-800 capitalize font-semibold">
{{ collab.display_name || collab.email.slice(0, collab.email.indexOf('@')) }}
</span>
</div>
<span class="text-xs text-gray-600">
{{ collab.email }}
</template>
<span class="truncate">
{{ collab.display_name }}
</span>
</NcTooltip>
<span v-else class="truncate">
{{ collab.email }}
</span>
</div>
</div>
<div class="user-access-grid">
<template v-if="accessibleRoles.includes(collab.roles)">
@ -230,7 +272,7 @@ watch(isInviteModalVisible, () => {
/>
</template>
<template v-else>
<RolesBadge :role="collab.roles" />
<RolesBadge :border="false" :role="collab.roles" />
</template>
</div>
<div class="date-joined-grid">
@ -252,6 +294,18 @@ watch(isInviteModalVisible, () => {
</template>
<style scoped lang="scss">
.ant-input::placeholder {
@apply text-gray-500;
}
.ant-input:placeholder-shown {
@apply text-gray-500 !text-md;
}
.ant-input-affix-wrapper {
@apply px-4 rounded-lg py-2 w-84 border-1 focus:border-brand-500 border-gray-200 !ring-0;
}
.color-band {
@apply w-6 h-6 left-0 top-2.5 rounded-full flex justify-center uppercase text-white font-weight-bold text-xs items-center;
}

48
packages/nc-gui/components/project/View.vue

@ -3,22 +3,36 @@ import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout'
import { isEeUI } from '#imports'
const props = defineProps<{
baseId: string
}>()
const basesStore = useBases()
const { openedProject, activeProjectId, basesUser } = storeToRefs(basesStore)
const { openedProject, activeProjectId, basesUser, bases } = storeToRefs(basesStore)
const { activeTables, activeTable } = storeToRefs(useTablesStore())
const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
const { navigateToProjectPage } = useBase()
const isAdminPanel = inject(IsAdminPanelInj, ref(false))
const router = useRouter()
const route = router.currentRoute
const { $e } = useNuxtApp()
const { $e, $api } = useNuxtApp()
/* const defaultBase = computed(() => {
return openedProject.value?.sources?.[0]
}) */
const currentBase = computed(async () => {
let base
if (props.baseId) {
base = bases.value.get(props.baseId)
if (!base) base = await $api.base.read(props.baseId!)
} else {
base = openedProject.value
}
return base
})
const { isUIAllowed, baseRoles } = useRoles()
@ -37,7 +51,7 @@ const userCount = computed(() =>
watch(
() => route.value.query?.page,
(newVal, oldVal) => {
if (route.value.name !== 'index-typeOrId-baseId-index-index') return
// if (route.value.name !== 'index-typeOrId-baseId-index-index') return
if (newVal && newVal !== oldVal) {
if (newVal === 'collaborator') {
projectPageTab.value = 'collaborator'
@ -46,11 +60,14 @@ watch(
} else {
projectPageTab.value = 'allTable'
}
return
}
projectPageTab.value = 'allTable'
if (isAdminPanel.value) {
projectPageTab.value = 'collaborator'
} else {
projectPageTab.value = 'allTable'
}
},
{ immediate: true },
)
@ -66,11 +83,11 @@ watch(projectPageTab, () => {
})
watch(
() => [openedProject.value?.id, openedProject.value?.title],
() => [currentBase.value?.id, currentBase.value?.title],
() => {
if (activeTable.value?.title) return
useTitle(`${openedProject.value?.title ?? activeWorkspace.value?.title ?? 'NocoDB'}`)
useTitle(`${currentBase.value?.title ?? activeWorkspace.value?.title ?? 'NocoDB'}`)
},
{
immediate: true,
@ -81,17 +98,18 @@ watch(
<template>
<div class="h-full nc-base-view">
<div
v-if="!isAdminPanel"
class="flex flex-row pl-2 pr-2 gap-1 border-b-1 border-gray-200 justify-between w-full"
:class="{ 'nc-table-toolbar-mobile': isMobileMode, 'h-[var(--topbar-height)]': !isMobileMode }"
>
<div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" :color="parseProp(openedProject?.meta).iconColor" />
<GeneralProjectIcon :color="parseProp(currentBase?.meta).iconColor" :type="currentBase?.type" />
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
<template #title> {{ openedProject?.title }}</template>
<template #title> {{ currentBase?.title }}</template>
<span class="truncate">
{{ openedProject?.title }}
{{ currentBase?.title }}
</span>
</NcTooltip>
</div>
@ -105,7 +123,7 @@ watch(
}"
>
<a-tabs v-model:activeKey="projectPageTab" class="w-full">
<a-tab-pane key="allTable">
<a-tab-pane v-if="!isAdminPanel" key="allTable">
<template #tab>
<div class="tab-title" data-testid="proj-view-tab__all-tables">
<NcLayout />
@ -143,7 +161,7 @@ watch(
</div>
</div>
</template>
<ProjectAccessSettings />
<ProjectAccessSettings :base-id="currentBase.id" />
</a-tab-pane>
<a-tab-pane v-if="isUIAllowed('sourceCreate')" key="data-source">
<template #tab>

8
packages/nc-gui/components/roles/Badge.vue

@ -8,6 +8,8 @@ const props = withDefaults(
clickable?: boolean
inherit?: boolean
border?: boolean
showIcon?: boolean
iconOnly?: boolean
size?: 'sm' | 'md' | 'lg'
}>(),
{
@ -15,6 +17,8 @@ const props = withDefaults(
inherit: false,
border: true,
size: 'sm',
iconOnly: false,
showIcon: true,
},
)
@ -60,8 +64,8 @@ const roleProperties = computed(() => {
}"
>
<div class="flex items-center gap-2">
<GeneralIcon :icon="roleProperties.icon" />
<span class="flex whitespace-nowrap">
<GeneralIcon v-if="showIcon" :icon="roleProperties.icon" />
<span v-if="!iconOnly" class="flex whitespace-nowrap">
{{ $t(`objects.roleType.${roleProperties.label}`) }}
</span>
</div>

8
packages/nc-gui/components/roles/Selector.vue

@ -1,11 +1,12 @@
<script lang="ts" setup>
import { RoleDescriptions } from 'nocodb-sdk'
import type { RoleLabels } from 'nocodb-sdk'
import { RoleDescriptions } from 'nocodb-sdk'
import type { SelectValue } from 'ant-design-vue/es/select'
import { toRef } from '#imports'
const props = withDefaults(
defineProps<{
border?: boolean
role: keyof typeof RoleLabels
roles: (keyof typeof RoleLabels)[]
description?: boolean
@ -14,6 +15,7 @@ const props = withDefaults(
size?: 'sm' | 'md' | 'lg'
}>(),
{
border: true,
description: true,
size: 'sm',
},
@ -36,7 +38,7 @@ function onChangeRole(val: SelectValue) {
<template>
<div ref="dropdownRef" size="lg" class="nc-roles-selector relative" @click="isDropdownOpen = !isDropdownOpen">
<RolesBadge data-testid="roles" :role="roleRef" :inherit="inheritRef === role" :size="sizeRef" clickable />
<RolesBadge :border="false" :inherit="inheritRef === role" :role="roleRef" :size="sizeRef" clickable data-testid="roles" />
<a-select
:value="roleRef"
:open="isDropdownOpen"
@ -54,7 +56,7 @@ function onChangeRole(val: SelectValue) {
class="flex flex-col nc-role-select-dropdown gap-1"
>
<div class="flex items-center justify-between">
<RolesBadge :class="`nc-role-select-${rl}`" :role="rl" :inherit="inheritRef === rl" :border="false" />
<RolesBadge :border="false" :class="`nc-role-select-${rl}`" :inherit="inheritRef === rl" :role="rl" />
<GeneralIcon v-if="rl === roleRef" icon="check" class="text-primary" />
</div>
<div v-if="descriptionRef" class="text-gray-500 text-xs">{{ RoleDescriptions[rl] }}</div>

2
packages/nc-gui/components/shared-view/Calendar.vue

@ -28,6 +28,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView)

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -32,6 +32,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView)

5
packages/nc-gui/components/shared-view/Grid.vue

@ -23,7 +23,7 @@ const { signedIn } = useGlobal()
const { loadProject } = useBase()
const { isLocked } = useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
const { isLocked, xWhere } = useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView)
useProvideCalendarViewStore(meta, sharedView)
@ -41,6 +41,9 @@ provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideViewGroupBy(sharedView, meta, xWhere, true)
useProvideSmartsheetLtarHelpers(meta)
if (signedIn.value) {
try {

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -27,6 +27,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideKanbanViewStore(meta, sharedView, true)

2
packages/nc-gui/components/shared-view/Map.vue

@ -27,6 +27,8 @@ provide(IsPublicInj, ref(true))
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideSmartsheetLtarHelpers(meta)
useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
useProvideMapViewStore(meta, sharedView, true)

136
packages/nc-gui/components/smartsheet/Cell.vue

@ -112,6 +112,17 @@ const syncValue = useDebounceFn(
{ maxWait: 2000 },
)
let saveTimer: number
const updateWhenEditCompleted = () => {
if (editEnabled.value) {
if (saveTimer) clearTimeout(saveTimer)
saveTimer = window.setTimeout(updateWhenEditCompleted, 500)
} else {
emit('save')
}
}
const vModel = computed({
get: () => {
return props.modelValue
@ -122,7 +133,9 @@ const vModel = computed({
} else if (val !== props.modelValue) {
currentRow.value.rowMeta.changed = true
emit('update:modelValue', val)
if (isAutoSaved(column.value)) {
if (column.value.pk) {
updateWhenEditCompleted()
} else if (isAutoSaved(column.value)) {
syncValue()
} else if (!isManualSaved(column.value)) {
emit('save')
@ -154,43 +167,10 @@ const onContextmenu = (e: MouseEvent) => {
e.stopPropagation()
}
}
// Todo: move intersection logic to a separate component or a vue directive
const intersected = ref(false)
const intersectionObserver = ref<IntersectionObserver>()
const elementToObserve = ref<Element>()
// load the cell only when it is in the viewport
function initIntersectionObserver() {
intersectionObserver.value = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// if the cell is in the viewport, load the cell and disconnect the observer
if (entry.isIntersecting) {
intersected.value = true
intersectionObserver.value?.disconnect()
intersectionObserver.value = undefined
}
})
})
}
// observe the cell when it is mounted
onMounted(() => {
initIntersectionObserver()
intersectionObserver.value?.observe(elementToObserve.value!)
})
// disconnect the observer when the cell is unmounted
onUnmounted(() => {
intersectionObserver.value?.disconnect()
})
</script>
<template>
<div
ref="elementToObserve"
:class="[
`nc-cell-${(column?.uidt || 'default').toLowerCase()}`,
{
@ -214,51 +194,49 @@ onUnmounted(() => {
@keydown.shift.enter.exact="navigate(NavigateDir.PREV, $event)"
>
<template v-if="column">
<template v-if="intersected">
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellSingleSelect
v-else-if="isSingleSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellMultiSelect
v-else-if="isMultiSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="isDateTime(column, abstractType)"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
/>
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" />
<LazyCellEmail v-else-if="isEmail(column)" v-model="vModel" />
<LazyCellUrl v-else-if="isURL(column)" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber(column)" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" />
<LazyCellUser v-else-if="isUser(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" />
<LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)"
class="nc-locked-overlay"
/>
</template>
<LazyCellTextArea v-if="isTextArea(column)" v-model="vModel" :virtual="props.virtual" />
<LazyCellGeoData v-else-if="isGeoData(column)" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean(column, abstractType)" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellSingleSelect
v-else-if="isSingleSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellMultiSelect
v-else-if="isMultiSelect(column)"
v-model="vModel"
:disable-option-creation="!!isEditColumnMenu"
:row-index="props.rowIndex"
/>
<LazyCellDatePicker v-else-if="isDate(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellYearPicker v-else-if="isYear(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellDateTimePicker
v-else-if="isDateTime(column, abstractType)"
v-model="vModel"
:is-pk="isPrimaryKey(column)"
:is-updated-from-copy-n-paste="currentRow.rowMeta.isUpdatedFromCopyNPaste"
/>
<LazyCellTimePicker v-else-if="isTime(column, abstractType)" v-model="vModel" :is-pk="isPrimaryKey(column)" />
<LazyCellRating v-else-if="isRating(column)" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration(column)" v-model="vModel" />
<LazyCellEmail v-else-if="isEmail(column)" v-model="vModel" />
<LazyCellUrl v-else-if="isURL(column)" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber(column)" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent(column)" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency(column)" v-model="vModel" @save="emit('save')" />
<LazyCellUser v-else-if="isUser(column)" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellDecimal v-else-if="isDecimal(column)" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat(column, abstractType)" v-model="vModel" />
<LazyCellText v-else-if="isString(column, abstractType)" v-model="vModel" />
<LazyCellInteger v-else-if="isInt(column, abstractType)" v-model="vModel" />
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)"
class="nc-locked-overlay"
/>
</template>
</div>
</template>

1
packages/nc-gui/components/smartsheet/Form.vue

@ -120,7 +120,6 @@ reloadEventHook.on(async () => {
const { fields, showAll, hideAll } = useViewColumnsOrThrow()
const { state, row } = useProvideSmartsheetRowStore(
meta,
ref({
row: formState.value,
oldRow: {},

17
packages/nc-gui/components/smartsheet/Row.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import type { Row } from '#imports'
import {
ReloadRowDataHookInj,
@ -10,7 +8,6 @@ import {
provide,
toRef,
useProvideSmartsheetRowStore,
useSmartsheetStoreOrThrow,
} from '#imports'
const props = defineProps<{
@ -19,12 +16,7 @@ const props = defineProps<{
const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs, clearLTARCell, addLTARRef, cleaMMCell } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
currentRow,
)
const { isNew, state } = useProvideSmartsheetRowStore(currentRow)
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)!
@ -39,13 +31,6 @@ reloadHook.on((params) => {
})
provide(ReloadRowDataHookInj, reloadHook)
defineExpose({
syncLTARRefs,
clearLTARCell,
addLTARRef,
cleaMMCell,
})
</script>
<template>

5
packages/nc-gui/components/smartsheet/SharedMapMarkerPopup.vue

@ -1,7 +1,4 @@
<script lang="ts" setup>
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { isVirtualCol } from 'nocodb-sdk'
import {
ActiveViewInj,
@ -52,7 +49,7 @@ provide(ReloadRowDataHookInj, reloadViewDataHook!)
const currentRow = toRef(props, 'row')
useProvideSmartsheetRowStore(meta as Ref<TableType>, currentRow)
useProvideSmartsheetRowStore(currentRow)
</script>
<template>

4
packages/nc-gui/components/smartsheet/TableDataCell.vue

@ -1,15 +1,13 @@
<script lang="ts" setup>
import { CellClickHookInj, CurrentCellInj, createEventHook, ref } from '#imports'
const el = ref<HTMLTableDataCellElement>()
const el = ref<HTMLElement>()
const cellClickHook = createEventHook()
provide(CellClickHookInj, cellClickHook)
provide(CurrentCellInj, el)
defineExpose({ el })
</script>
<template>

61
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -62,43 +62,10 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
if (!isForm.value) e.stopImmediatePropagation()
}
// Todo: move intersection logic to a separate component or a vue directive
const intersected = ref(false)
const intersectionObserver = ref<IntersectionObserver>()
const elementToObserve = ref<Element>()
// load the cell only when it is in the viewport
function initIntersectionObserver() {
intersectionObserver.value = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
// if the cell is in the viewport, load the cell and disconnect the observer
if (entry.isIntersecting) {
intersected.value = true
intersectionObserver.value?.disconnect()
intersectionObserver.value = undefined
}
})
})
}
// observe the cell when it is mounted
onMounted(() => {
initIntersectionObserver()
intersectionObserver.value?.observe(elementToObserve.value!)
})
// disconnect the observer when the cell is unmounted
onUnmounted(() => {
intersectionObserver.value?.disconnect()
})
</script>
<template>
<div
ref="elementToObserve"
class="nc-virtual-cell w-full flex items-center"
:class="{
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
@ -107,21 +74,19 @@ onUnmounted(() => {
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
>
<template v-if="intersected">
<LazyVirtualCellLinks v-if="isLink(column)" />
<LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellOneToOne v-else-if="isOo(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" />
<LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
</template>
<LazyVirtualCellLinks v-if="isLink(column)" />
<LazyVirtualCellHasMany v-else-if="isHm(column)" />
<LazyVirtualCellManyToMany v-else-if="isMm(column)" />
<LazyVirtualCellBelongsTo v-else-if="isBt(column)" />
<LazyVirtualCellOneToOne v-else-if="isOo(column)" />
<LazyVirtualCellRollup v-else-if="isRollup(column)" />
<LazyVirtualCellFormula v-else-if="isFormula(column)" />
<LazyVirtualCellQrCode v-else-if="isQrCode(column)" />
<LazyVirtualCellBarcode v-else-if="isBarcode(column)" />
<LazyVirtualCellCount v-else-if="isCount(column)" />
<LazyVirtualCellLookup v-else-if="isLookup(column)" />
<LazyCellReadOnlyDateTimePicker v-else-if="isCreatedOrLastModifiedTimeCol(column)" :model-value="modelValue" />
<LazyCellReadOnlyUser v-else-if="isCreatedOrLastModifiedByCol(column)" :model-value="modelValue" />
</div>
</template>

23
packages/nc-gui/components/smartsheet/calendar/DayView/DateField.vue

@ -18,16 +18,23 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => {
const fi = _fields.value?.find((f) => f.title === field.title)
const fieldStyles = computed(() => {
if (!_fields.value) return new Map()
return new Map(
_fields.value.map((field) => [
field.fk_column_id,
{
underline: field.underline,
bold: field.bold,
italic: field.italic,
},
]),
)
})
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
// We loop through all the records and calculate the position of each record based on the range
// We only need to calculate the top, of the record since there is no overlap in the day view of date Field
const recordsAcrossAllRange = computed<Row[]>(() => {

215
packages/nc-gui/components/smartsheet/calendar/DayView/DateTimeField.vue

@ -28,15 +28,22 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType) => {
if (!_fields.value) return { underline: false, bold: false, italic: false }
const fi = _fields.value.find((f) => f.title === field.title)
const fieldStyles = computed(() => {
if (!_fields.value) return new Map()
return new Map(
_fields.value.map((field) => [
field.fk_column_id,
{
underline: field.underline,
bold: field.bold,
italic: field.italic,
},
]),
)
})
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
const hours = computed(() => {
@ -49,36 +56,38 @@ const hours = computed(() => {
return hours
})
const calculateNewDates = ({
endDate,
startDate,
scheduleStart,
scheduleEnd,
}: {
endDate: dayjs.Dayjs
startDate: dayjs.Dayjs
scheduleStart: dayjs.Dayjs
scheduleEnd: dayjs.Dayjs
}) => {
// If there is no end date, we add 15 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(15, 'minutes')
}
const calculateNewDates = useMemoize(
({
endDate,
startDate,
scheduleStart,
scheduleEnd,
}: {
endDate: dayjs.Dayjs
startDate: dayjs.Dayjs
scheduleStart: dayjs.Dayjs
scheduleEnd: dayjs.Dayjs
}) => {
// If there is no end date, we add 15 minutes to the start date and use that as the end date
if (!endDate.isValid()) {
endDate = startDate.clone().add(15, 'minutes')
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isSameOrBefore(scheduleStart)) {
startDate = scheduleStart
}
// If the start date is before the opened date, we use the schedule start as the start date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (startDate.isSameOrBefore(scheduleStart)) {
startDate = scheduleStart
}
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd)) {
endDate = scheduleEnd
}
// If the end date is after the schedule end, we use the schedule end as the end date
// This is to ensure the generated style of the record is not outside the bounds of the calendar
if (endDate.isAfter(scheduleEnd)) {
endDate = scheduleEnd
}
return { endDate, startDate }
}
return { endDate, startDate }
},
)
const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
@ -133,35 +142,14 @@ const hasSlotForRecord = (
}
const getMaxOverlaps = ({
row,
gridTimeMap,
columnArray,
graph,
}: {
row: Row
gridTimeMap: Map<
number,
{
count: number
id: string[]
}
>
columnArray: Array<Array<Row>>
graph: Map<string, Set<string>>
}) => {
const visited: Set<string> = new Set()
const graph: Map<string, Set<string>> = new Map()
// Build the graph
for (const [_gridTime, { id: ids }] of gridTimeMap) {
for (const id1 of ids) {
if (!graph.has(id1)) {
graph.set(id1, new Set())
}
for (const id2 of ids) {
if (id1 !== id2) {
graph.get(id1)!.add(id2)
}
}
}
}
const dfs = (id: string): number => {
visited.add(id)
@ -169,6 +157,7 @@ const getMaxOverlaps = ({
const neighbors = graph.get(id)
if (neighbors) {
for (const neighbor of neighbors) {
if (maxOverlaps >= columnArray.length) return maxOverlaps
if (!visited.has(neighbor)) {
maxOverlaps = Math.min(Math.max(maxOverlaps, dfs(neighbor) + 1), columnArray.length)
}
@ -187,32 +176,19 @@ const getMaxOverlaps = ({
const recordsAcrossAllRange = computed<{
record: Row[]
count: {
[key: string]: {
gridTimeMap: Map<
number,
{
count: number
id: string[]
overflow: boolean
overflowCount: number
}
}
>
}>(() => {
if (!calendarRange.value || !formattedData.value) return { record: [], count: {} }
const scheduleStart = dayjs(selectedDate.value).startOf('day')
const scheduleEnd = dayjs(selectedDate.value).endOf('day')
// We use this object to keep track of the number of records that overlap at a given time, and if the number of records exceeds 4, we hide the record
// and show a button to view more records
// The key is the time in HH:mm format
// id is the id of the record generated below
const overlaps: {
[key: string]: {
id: string[]
overflow: boolean
overflowCount: number
}
} = {}
const perRecordHeight = 52
const columnArray: Array<Array<Row>> = [[]]
@ -400,11 +376,28 @@ const recordsAcrossAllRange = computed<{
record.rowMeta.overLapIteration = parseInt(columnIndex) + 1
}
}
const graph = new Map<string, Set<string>>()
// Build the graph
for (const [_gridTime, { id: ids }] of gridTimeMap) {
for (const id1 of ids) {
if (!graph.has(id1)) {
graph.set(id1, new Set())
}
for (const id2 of ids) {
if (id1 !== id2) {
graph.get(id1)!.add(id2)
}
}
}
}
for (const record of recordsByRange) {
const numberOfOverlaps = getMaxOverlaps({
row: record,
gridTimeMap,
columnArray,
graph,
})
record.rowMeta.numberOfOverlaps = numberOfOverlaps
@ -418,24 +411,6 @@ const recordsAcrossAllRange = computed<{
if (record.rowMeta.overLapIteration! - 1 > 7) {
display = 'none'
gridTimeMap.forEach((value, key) => {
if (value.id.includes(record.rowMeta.id!)) {
if (!overlaps[key]) {
overlaps[key] = {
id: value.id,
overflow: true,
overflowCount: value.id.length,
}
} else {
overlaps[key].overflow = true
value.id.forEach((id) => {
if (!overlaps[key].id.includes(id)) {
overlaps[key].id.push(id)
}
})
}
}
})
} else {
left = width * (record.rowMeta.overLapIteration! - 1)
}
@ -453,7 +428,7 @@ const recordsAcrossAllRange = computed<{
}
return {
count: overlaps,
gridTimeMap,
record: recordsByRange,
}
})
@ -477,7 +452,7 @@ const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[],
}, 500)
// When the user is dragging a record, we calculate the new start and end date based on the mouse position
const calculateNewRow = (event: MouseEvent) => {
const calculateNewRow = (event: MouseEvent, skipChangeCheck?: boolean) => {
if (!container.value || !dragRecord.value) return { newRow: null, updateProperty: [] }
const { top } = container.value.getBoundingClientRect()
@ -505,7 +480,7 @@ const calculateNewRow = (event: MouseEvent) => {
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
},
}
@ -528,11 +503,16 @@ const calculateNewRow = (event: MouseEvent) => {
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol.title!)
}
// If from and to columns of the dragRecord and the newRow are the same, we don't manipulate the formattedRecords and formattedSideBarData. This removes unwanted computation
if (dragRecord.value.row[fromCol.title!] === newRow.row[fromCol.title!] && !skipChangeCheck) {
return { newRow: null, updateProperty: [] }
}
if (!newRow) {
return { newRow: null, updateProperty: [] }
}
@ -552,6 +532,11 @@ const calculateNewRow = (event: MouseEvent) => {
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk !== newPk
})
dragRecord.value = {
...dragRecord.value,
row: newRow.row,
}
}
return { newRow, updateProperty }
}
@ -668,7 +653,7 @@ const stopDrag = (event: MouseEvent) => {
clearTimeout(dragTimeout.value!)
if (!isUIAllowed('dataEdit') || !isDragging.value || !container.value || !dragRecord.value) return
const { newRow, updateProperty } = calculateNewRow(event)
const { newRow, updateProperty } = calculateNewRow(event, true)
if (!newRow && !updateProperty) return
const allRecords = document.querySelectorAll('.draggable-record')
@ -823,32 +808,18 @@ const dropEvent = (event: DragEvent) => {
}
const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
let startOfHour = hour.startOf('hour')
const endOfHour = hour.endOf('hour')
const ids: Array<string> = []
let isOverflow = false
if (!recordsAcrossAllRange.value || !recordsAcrossAllRange.value.gridTimeMap) return { isOverflow: false, overflowCount: 0 }
const { gridTimeMap } = recordsAcrossAllRange.value
const startMinute = hour.hour() * 60 + hour.minute()
const endMinute = hour.hour() * 60 + hour.minute() + 59
let overflowCount = 0
while (startOfHour.isBefore(endOfHour, 'minute')) {
const hourKey = startOfHour.hour() * 60 + startOfHour.minute()
if (recordsAcrossAllRange.value?.count?.[hourKey]?.overflow) {
isOverflow = true
recordsAcrossAllRange.value?.count?.[hourKey]?.id.forEach((id) => {
if (!ids.includes(id)) {
ids.push(id)
overflowCount += 1
}
})
}
startOfHour = startOfHour.add(1, 'minute')
for (let minute = startMinute; minute <= endMinute; minute++) {
const recordCount = gridTimeMap.get(minute)?.count ?? 0
overflowCount = Math.max(overflowCount, recordCount)
}
overflowCount = overflowCount > 8 ? overflowCount - 8 : 0
return { isOverflow, overflowCount }
return { isOverflow: overflowCount - 8 > 0, overflowCount: overflowCount - 8 }
}
const viewMore = (hour: dayjs.Dayjs) => {

36
packages/nc-gui/components/smartsheet/calendar/MonthView.vue

@ -64,15 +64,22 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
if (!field) return { underline: false, bold: false, italic: false }
const fi = _fields.value?.find((f) => f.title === field.title)
const fieldStyles = computed(() => {
if (!_fields.value) return new Map()
return new Map(
_fields.value.map((field) => [
field.fk_column_id,
{
underline: field.underline,
bold: field.bold,
italic: field.italic,
},
]),
)
})
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
const dates = computed(() => {
@ -343,7 +350,7 @@ const recordsToDisplay = computed<{
}
})
const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean, skipChangeCheck?: boolean) => {
const { top, height, width, left } = calendarGridContainer.value.getBoundingClientRect()
const percentY = (event.clientY - top - window.scrollY) / height
@ -364,7 +371,7 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
...dragRecord.value,
row: {
...dragRecord.value?.row,
[fromCol!.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol!.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
},
}
@ -384,10 +391,15 @@ const calculateNewRow = (event: MouseEvent, updateSideBar?: boolean) => {
endDate = newStartDate.clone()
}
newRow.row[toCol!.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol!.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
updateProperty.push(toCol!.title!)
}
// If from and to columns of the dragRecord and the newRow are the same, we don't manipulate the formattedRecords and formattedSideBarData. This removes unwanted computation
if (dragRecord.value.row[fromCol.title!] === newRow.row[fromCol.title!] && !skipChangeCheck) {
return { newRow: null, updatedProperty: [] }
}
if (!newRow) return { newRow: null, updateProperty: [] }
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
@ -515,7 +527,7 @@ const stopDrag = (event: MouseEvent) => {
event.preventDefault()
dragElement.value!.style.boxShadow = 'none'
const { newRow, updateProperty } = calculateNewRow(event, false)
const { newRow, updateProperty } = calculateNewRow(event, false, true)
const allRecords = document.querySelectorAll('.draggable-record')
allRecords.forEach((el) => {

41
packages/nc-gui/components/smartsheet/calendar/WeekView/DateField.vue

@ -1,6 +1,6 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { type ColumnType } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { computed, ref, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
@ -22,14 +22,22 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
const fi = _fields.value?.find((f) => f.title === field?.title)
const fieldStyles = computed(() => {
if (!_fields.value) return new Map()
return new Map(
_fields.value.map((field) => [
field.fk_column_id,
{
underline: field.underline,
bold: field.bold,
italic: field.italic,
},
]),
)
})
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
// Calculate the dates of the week
@ -71,6 +79,18 @@ const findFirstSuitableRow = (recordsInDay: any, startDayIndex: number, spanDays
}
}
const isInRange = (date: dayjs.Dayjs) => {
return (
date &&
date.isBetween(
dayjs(selectedDateRange.value.start).startOf('day'),
dayjs(selectedDateRange.value.end).endOf('day'),
'day',
'[]',
)
)
}
const calendarData = computed(() => {
if (!formattedData.value || !calendarRange.value) return []
@ -156,9 +176,8 @@ const calendarData = computed(() => {
let position = 'none'
const isStartInRange =
ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
const isEndInRange = endDate && endDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
const isStartInRange = isInRange(ogStartDate)
const isEndInRange = isInRange(endDate)
// Calculate the position of the record in the calendar based on the start and end date
// The position can be 'none', 'leftRounded', 'rightRounded', 'rounded'

601
packages/nc-gui/components/smartsheet/calendar/WeekView/DateTimeField.vue

@ -1,8 +1,8 @@
<script lang="ts" setup>
import dayjs from 'dayjs'
import { type ColumnType } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { computed, ref, useViewColumnsOrThrow } from '#imports'
import { computed, ref, useMemoize, useViewColumnsOrThrow } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'
const emits = defineEmits(['expandRecord', 'newRecord'])
@ -14,7 +14,6 @@ const {
calendarRange,
displayField,
selectedTime,
selectedDate,
updateRowProperty,
sideBarFilterOption,
showSideMenu,
@ -34,16 +33,53 @@ const fields = inject(FieldsInj, ref())
const { fields: _fields } = useViewColumnsOrThrow()
const getFieldStyle = (field: ColumnType | undefined) => {
if (!field) return { underline: false, bold: false, italic: false }
const fi = _fields.value?.find((f) => f.title === field.title)
const fieldStyles = computed(() => {
if (!_fields.value) return new Map()
return new Map(
_fields.value.map((field) => [
field.fk_column_id,
{
underline: field.underline,
bold: field.bold,
italic: field.italic,
},
]),
)
})
return {
underline: fi?.underline,
bold: fi?.bold,
italic: fi?.italic,
}
const getFieldStyle = (field: ColumnType) => {
return fieldStyles.value.get(field.id)
}
const calculateNewDates = useMemoize(
({
startDate,
endDate,
scheduleStart,
scheduleEnd,
}: {
startDate: dayjs.Dayjs
endDate: dayjs.Dayjs
scheduleStart: dayjs.Dayjs
scheduleEnd: dayjs.Dayjs
}) => {
// If the end date is not valid, we set it to 15 minutes after the start date
if (!endDate?.isValid()) {
endDate = startDate.clone().add(15, 'minutes')
}
// If the start date is before the start of the schedule, we set it to the start of the schedule
// If the end date is after the end of the schedule, we set it to the end of the schedule
// This is to ensure that the records are within the bounds of the schedule and do not overflow
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart.clone()
}
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd.clone()
}
return { startDate, endDate }
},
)
// Since it is a datetime Week view, we need to create a 2D array of dayjs objects to represent the hours in a day for each day in the week
const datesHours = computed(() => {
@ -71,22 +107,122 @@ const datesHours = computed(() => {
return datesHours
})
const recordsAcrossAllRange = computed<{
records: Array<Row>
count: {
[key: string]: {
[key: string]: {
id: Array<string>
overflow: boolean
overflowCount: number
const getDayIndex = (date: dayjs.Dayjs) => {
let dayIndex = date.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
return dayIndex
}
const getGridTime = (date: dayjs.Dayjs, round = false) => {
const gridCalc = date.hour() * 60 + date.minute()
if (round) {
return Math.ceil(gridCalc)
} else {
return Math.floor(gridCalc)
}
}
const getGridTimeSlots = (from: dayjs.Dayjs, to: dayjs.Dayjs) => {
return {
from: getGridTime(from, false),
to: getGridTime(to, true) - 1,
dayIndex: getDayIndex(from),
}
}
const hasSlotForRecord = (
columnArray: Row[],
dates: {
fromDate: dayjs.Dayjs
toDate: dayjs.Dayjs
},
) => {
const { fromDate, toDate } = dates
if (!fromDate || !toDate) return false
for (const column of columnArray) {
const columnFromCol = column.rowMeta.range?.fk_from_col
const columnToCol = column.rowMeta.range?.fk_to_col
if (!columnFromCol) return false
const { startDate: columnFromDate, endDate: columnToDate } = calculateNewDates({
startDate: dayjs(column.row[columnFromCol.title!]),
endDate: columnToCol
? dayjs(column.row[columnToCol.title!])
: dayjs(column.row[columnFromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
scheduleStart: dayjs(selectedDateRange.value.start).startOf('day'),
scheduleEnd: dayjs(selectedDateRange.value.end).endOf('day'),
})
if (
fromDate.isBetween(columnFromDate, columnToDate, null, '[]') ||
toDate.isBetween(columnFromDate, columnToDate, null, '[]')
) {
return false
}
}
return true
}
const getMaxOverlaps = ({
row,
columnArray,
graph,
}: {
row: Row
columnArray: Array<Array<Array<Row>>>
graph: Map<string, Set<string>>
}) => {
const id = row.rowMeta.id as string
const visited: Set<string> = new Set()
const dayIndex = row.rowMeta.dayIndex
const overlapIndex = columnArray[dayIndex].findIndex((column) => column.findIndex((r) => r.rowMeta.id === id) !== -1) + 1
const dfs = (id: string): number => {
visited.add(id)
let maxOverlaps = 1
const neighbors = graph.get(id)
if (neighbors) {
for (const neighbor of neighbors) {
if (maxOverlaps >= columnArray[dayIndex].length) return maxOverlaps
if (!visited.has(neighbor)) {
maxOverlaps = Math.min(Math.max(maxOverlaps, dfs(neighbor) + 1), columnArray[dayIndex].length)
}
}
}
return maxOverlaps
}
let maxOverlaps = 1
if (graph.has(id)) {
maxOverlaps = dfs(id)
}
return { maxOverlaps, dayIndex, overlapIndex }
}
const recordsAcrossAllRange = computed<{
records: Array<Row>
gridTimeMap: Map<
number,
Map<
number,
{
count: number
id: string[]
}
>
>
}>(() => {
if (!formattedData.value || !calendarRange.value || !container.value || !scrollContainer.value)
return {
records: [],
count: {},
gridTimeMap: new Map(),
}
const perWidth = containerWidth.value / 7
const perHeight = 52
@ -94,20 +230,18 @@ const recordsAcrossAllRange = computed<{
const scheduleStart = dayjs(selectedDateRange.value.start).startOf('day')
const scheduleEnd = dayjs(selectedDateRange.value.end).endOf('day')
// We need to keep track of the overlaps for each day and hour, minute in the week to calculate the width and left position of each record
// The first key is the date, the second key is the hour, and the value is an object containing the ids of the records that overlap
// The key is in the format YYYY-MM-DD and the hour is in the format HH:mm
const overlaps: {
[key: string]: {
[key: string]: {
id: Array<string>
overflow: boolean
overflowCount: number
const columnArray: Array<Array<Array<Row>>> = [[[]]]
const gridTimeMap = new Map<
number,
Map<
number,
{
count: number
id: string[]
}
}
} = {}
let recordsToDisplay: Array<Row> = []
>
>()
const recordsToDisplay: Array<Row> = []
calendarRange.value.forEach((range) => {
const fromCol = range.fk_from_col
@ -115,123 +249,36 @@ const recordsAcrossAllRange = computed<{
// We fetch all the records that match the calendar ranges in a single time.
// But not all fetched records are valid for the certain range, so we filter them out & sort them
const sortedFormattedData = [...formattedData.value].filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
const sortedFormattedData = [...formattedData.value]
.filter((record) => {
const fromDate = record.row[fromCol!.title!] ? dayjs(record.row[fromCol!.title!]) : null
if (fromCol && toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
return fromDate && toDate && !toDate.isBefore(fromDate)
} else if (fromCol && !toCol) {
return !!fromDate
}
return false
})
sortedFormattedData.forEach((record: Row) => {
if (!toCol && fromCol) {
// If there is no toColumn chosen in the range
const ogStartDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
if (!ogStartDate) return
let endDate = ogStartDate.clone().add(1, 'hour')
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
const id = record.rowMeta.id ?? generateRandomNumber()
let startDate = ogStartDate.clone()
let style: Partial<CSSStyleDeclaration> = {}
while (startDate.isBefore(endDate, 'minutes')) {
const dateKey = startDate?.format('YYYY-MM-DD')
const hourKey = startDate?.format('HH:mm')
// If the dateKey and hourKey are valid, we add the id to the overlaps object
if (dateKey && hourKey) {
if (!overlaps[dateKey]) {
overlaps[dateKey] = {}
}
if (!overlaps[dateKey][hourKey]) {
overlaps[dateKey][hourKey] = {
id: [],
overflow: false,
overflowCount: 0,
}
}
overlaps[dateKey][hourKey].id.push(id)
}
// If the number of records that overlap in a single hour is more than 4, we hide the record and set the overflow flag to true
// We also keep track of the number of records that overflow
if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true
style.display = 'none'
overlaps[dateKey][hourKey].overflowCount += 1
}
// TODO: dayIndex is not calculated perfectly
// Should revisit this part in next iteration
let dayIndex = dayjs(dateKey).day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
if (fromCol && toCol) {
const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
startDate = startDate.add(1, 'minute')
return fromDate && toDate && !toDate.isBefore(fromDate)
} else if (fromCol && !toCol) {
return !!fromDate
}
return false
})
.sort((a, b) => {
const aDate = dayjs(a.row[fromCol!.title!])
const bDate = dayjs(b.row[fromCol!.title!])
return aDate.isBefore(bDate) ? 1 : -1
})
let dayIndex = ogStartDate.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
const minutes = (ogStartDate.minute() / 60 + ogStartDate.hour()) * 52
style = {
...style,
top: `${minutes + 1}px`,
height: `${perHeight - 2}px`,
}
for (const record of sortedFormattedData) {
const id = record.rowMeta.id ?? generateRandomNumber()
recordsToDisplay.push({
...record,
rowMeta: {
...record.rowMeta,
id,
position: 'rounded',
style,
range,
dayIndex,
},
if (fromCol && toCol) {
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: dayjs(record.row[toCol.title!]),
scheduleStart,
scheduleEnd,
})
} else if (fromCol && toCol) {
const id = record.rowMeta.id ?? generateRandomNumber()
let startDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
let endDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null
// If the start date is not valid, we skip the record
if (!startDate?.isValid()) return
// If the end date is not valid, we set it to 30 minutes after the start date
if (!endDate?.isValid()) {
endDate = startDate.clone().add(30, 'minutes')
}
// If the start date is before the start of the schedule, we set it to the start of the schedule
// If the end date is after the end of the schedule, we set it to the end of the schedule
// This is to ensure that the records are within the bounds of the schedule and do not overflow
if (startDate.isBefore(scheduleStart, 'minutes')) {
startDate = scheduleStart
}
if (endDate.isAfter(scheduleEnd, 'minutes')) {
endDate = scheduleEnd
}
// Setting the current start date to the start date of the record
let currentStartDate: dayjs.Dayjs = startDate.clone()
@ -242,14 +289,7 @@ const recordsAcrossAllRange = computed<{
const recordStart: dayjs.Dayjs = currentEndDate.isSame(startDate, 'day') ? startDate : currentStartDate
const recordEnd = currentEndDate.isSame(endDate, 'day') ? endDate : currentEndDate
const dateKey = recordStart.format('YYYY-MM-DD')
// TODO: dayIndex is not calculated perfectly
// Should revisit this part in next iteration
let dayIndex = recordStart.day() - 1
if (dayIndex === -1) {
dayIndex = 6
}
const dayIndex = getDayIndex(recordStart)
// We calculate the index of the start and end hour in the day
const startHourIndex = Math.max(
@ -278,36 +318,8 @@ const recordsAcrossAllRange = computed<{
position = 'none'
}
let _startHourIndex = startHourIndex
let style: Partial<CSSStyleDeclaration> = {}
// We loop through the start hour index to the end hour index and add the id to the overlaps object
while (_startHourIndex <= endHourIndex) {
const hourKey = datesHours.value[dayIndex][_startHourIndex].format('HH:mm')
if (!overlaps[dateKey]) {
overlaps[dateKey] = {}
}
if (!overlaps[dateKey][hourKey]) {
overlaps[dateKey][hourKey] = {
id: [],
overflow: false,
overflowCount: 0,
}
}
overlaps[dateKey][hourKey].id.push(id)
// If the number of records that overlap in a single hour is more than 4, we hide the record and set the overflow flag to true
// We also keep track of the number of records that overflow
if (overlaps[dateKey][hourKey].id.length > 4) {
overlaps[dateKey][hourKey].overflow = true
style.display = 'none'
overlaps[dateKey][hourKey].overflowCount += 1
}
_startHourIndex++
}
const spanHours = endHourIndex - startHourIndex + 1
const top = startHourIndex * perHeight
@ -334,45 +346,170 @@ const recordsAcrossAllRange = computed<{
// We set the current start date to the next day
currentStartDate = currentStartDate.add(1, 'day').hour(0).minute(0)
}
} else if (fromCol) {
// If there is no toColumn chosen in the range
const { startDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: dayjs(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
scheduleStart,
scheduleEnd,
})
let style: Partial<CSSStyleDeclaration> = {}
const dayIndex = getDayIndex(startDate)
const minutes = (startDate.minute() / 60 + startDate.hour()) * perHeight
style = {
...style,
top: `${minutes + 1}px`,
height: `${perHeight - 2}px`,
}
recordsToDisplay.push({
...record,
rowMeta: {
...record.rowMeta,
id,
position: 'rounded',
style,
range,
dayIndex,
},
})
}
}
recordsToDisplay.sort((a, b) => {
const fromColA = a.rowMeta.range?.fk_from_col
const fromColB = b.rowMeta.range?.fk_from_col
if (!fromColA || !fromColB) return 0
return dayjs(a.row[fromColA.title!]).isBefore(dayjs(b.row[fromColB.title!])) ? -1 : 1
})
// With can't find the left and width of the record without knowing the overlaps
// Hence the first iteration is to find the overlaps, top, height and then the second iteration is to find the left and width
// This is because the left and width of the record depends on the overlaps
recordsToDisplay = recordsToDisplay.map((record) => {
// maxOverlaps is the maximum number of records that overlap in a single hour
// overlapIndex is the index of the record in the overlaps object
let maxOverlaps = 1
let overlapIndex = 0
const dayIndex = record.rowMeta.dayIndex as number
const dateKey = dayjs(selectedDateRange.value.start).add(dayIndex, 'day').format('YYYY-MM-DD')
for (const hours in overlaps[dateKey]) {
// We are checking if the overlaps object contains the id of the record
// If it does, we set the maxOverlaps and overlapIndex
if (overlaps[dateKey][hours].id.includes(record.rowMeta.id!)) {
maxOverlaps = Math.max(maxOverlaps, overlaps[dateKey][hours].id.length - overlaps[dateKey][hours].overflowCount)
overlapIndex = Math.max(overlapIndex, overlaps[dateKey][hours].id.indexOf(record.rowMeta.id!))
for (const record of recordsToDisplay) {
const fromCol = record.rowMeta.range?.fk_from_col
const toCol = record.rowMeta.range?.fk_to_col
if (!fromCol) continue
const { startDate, endDate } = calculateNewDates({
startDate: dayjs(record.row[fromCol.title!]),
endDate: toCol ? dayjs(record.row[toCol.title!]) : dayjs(record.row[fromCol.title!]).add(1, 'hour').subtract(1, 'minute'),
scheduleStart,
scheduleEnd,
})
const gridTimes = getGridTimeSlots(startDate, endDate)
const dayIndex = record.rowMeta.dayIndex ?? gridTimes.dayIndex
for (let gridCounter = gridTimes.from; gridCounter <= gridTimes.to; gridCounter++) {
if (!gridTimeMap.has(dayIndex)) {
gridTimeMap.set(
dayIndex,
new Map<
number,
{
count: number
id: string[]
}
>(),
)
}
if (!gridTimeMap.get(dayIndex)?.has(gridCounter)) {
gridTimeMap.set(dayIndex, (gridTimeMap.get(dayIndex) ?? new Map()).set(gridCounter, { count: 0, id: [] }))
}
const idArray = gridTimeMap.get(dayIndex)!.get(gridCounter)!.id
idArray.push(record.rowMeta.id!)
const count = gridTimeMap.get(dayIndex)!.get(gridCounter)!.count + 1
gridTimeMap.set(dayIndex, (gridTimeMap.get(dayIndex) ?? new Map()).set(gridCounter, { count, id: idArray }))
}
let foundAColumn = false
if (!columnArray[dayIndex]) {
columnArray[dayIndex] = []
}
for (const column in columnArray[dayIndex]) {
if (hasSlotForRecord(columnArray[dayIndex][column], { fromDate: startDate, toDate: endDate })) {
columnArray[dayIndex][column].push(record)
foundAColumn = true
break
}
}
if (!foundAColumn) {
columnArray[dayIndex].push([record])
}
}
const graph: Map<number, Map<string, Set<string>>> = new Map()
for (const dayIndex of gridTimeMap.keys()) {
if (!graph.has(dayIndex)) {
graph.set(dayIndex, new Map())
}
for (const [_gridTime, { id: ids }] of gridTimeMap.get(dayIndex)) {
for (const id1 of ids) {
if (!graph.get(dayIndex).has(id1)) {
graph.get(dayIndex).set(id1, new Set())
}
for (const id2 of ids) {
if (id1 !== id2) {
if (!graph.get(dayIndex).get(id1).has(id2)) {
graph.get(dayIndex).get(id1).add(id2)
}
}
}
}
}
}
for (const dayIndex in columnArray) {
for (const columnIndex in columnArray[dayIndex]) {
for (const record of columnArray[dayIndex][columnIndex]) {
record.rowMeta.overLapIteration = parseInt(columnIndex) + 1
}
}
const spacing = 0.1
const widthPerRecord = (100 - spacing * (maxOverlaps - 1)) / maxOverlaps / 7
const leftPerRecord = widthPerRecord * overlapIndex
}
for (const record of recordsToDisplay) {
const { maxOverlaps, overlapIndex } = getMaxOverlaps({
row: record,
columnArray,
graph: graph.get(record.rowMeta.dayIndex!) ?? new Map(),
})
const dayIndex = record.rowMeta.dayIndex ?? tDayIndex
record.rowMeta.numberOfOverlaps = maxOverlaps
let width = 0
let left = 100
const majorLeft = dayIndex * perWidth
let display = 'block'
if (record.rowMeta.overLapIteration! - 1 > 2) {
display = 'none'
} else {
width = 100 / Math.min(maxOverlaps, 3) / 7
left = width * (overlapIndex - 1)
}
record.rowMeta.style = {
...record.rowMeta.style,
left: `calc(${dayIndex * perWidth}px + ${leftPerRecord}% )`,
width: `calc(${widthPerRecord - 0.1}%)`,
left: `calc(${majorLeft}px + ${left}%)`,
width: `calc(${width}%)`,
display,
}
return record
})
}
})
return {
records: recordsToDisplay,
count: overlaps,
gridTimeMap,
}
})
@ -497,9 +634,11 @@ const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: R
const calculateNewRow = (
event: MouseEvent,
updateSideBar?: boolean,
skipChangeCheck?: boolean,
): {
newRow: Row | null
updatedProperty: string[]
skipChangeCheck?: boolean
} => {
const { width, left, top } = container.value.getBoundingClientRect()
@ -528,7 +667,7 @@ const calculateNewRow = (
...dragRecord.value,
row: {
...dragRecord.value.row,
[fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD HH:mm:ssZ'),
[fromCol.title!]: dayjs(newStartDate).utc().format('YYYY-MM-DD HH:mm:ssZ'),
},
}
@ -546,11 +685,16 @@ const calculateNewRow = (
endDate = newStartDate.clone()
}
newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD HH:mm:ssZ')
newRow.row[toCol.title!] = dayjs(endDate).utc().format('YYYY-MM-DD HH:mm:ssZ')
updatedProperty.push(toCol.title!)
}
if (!newRow) return { newRow: null, updatedProperty }
// If from and to columns of the dragRecord and the newRow are the same, we don't manipulate the formattedRecords and formattedSideBarData. This removes unwanted computation
if (dragRecord.value.row[fromCol.title!] === newRow.row[fromCol.title!] && !skipChangeCheck) {
return { newRow: null, updatedProperty: [] }
}
if (!newRow) return { newRow: null, updatedProperty: [] }
const newPk = extractPkFromRow(newRow.row, meta.value!.columns!)
@ -565,6 +709,10 @@ const calculateNewRow = (
const pk = extractPkFromRow(r.row, meta.value!.columns!)
return pk === newPk ? newRow : r
})
dragRecord.value = {
...dragRecord.value,
row: newRow.row,
}
}
return { newRow, updatedProperty }
@ -591,7 +739,7 @@ const stopDrag = (event: MouseEvent) => {
event.preventDefault()
clearTimeout(dragTimeout.value!)
const { newRow, updatedProperty } = calculateNewRow(event, false)
const { newRow, updatedProperty } = calculateNewRow(event, false, true)
// We set the visibility and opacity of the records back to normal
const allRecords = document.querySelectorAll('.draggable-record')
@ -676,33 +824,19 @@ const viewMore = (hour: dayjs.Dayjs) => {
}
const isOverflowAcrossHourRange = (hour: dayjs.Dayjs) => {
let startOfHour = hour.startOf('hour')
const endOfHour = hour.endOf('hour')
const ids: Array<string> = []
let isOverflow = false
if (!recordsAcrossAllRange.value || !recordsAcrossAllRange.value.gridTimeMap) return { isOverflow: false, overflowCount: 0 }
const { gridTimeMap } = recordsAcrossAllRange.value
const dayIndex = getDayIndex(hour)
const startMinute = hour.hour() * 60 + hour.minute()
const endMinute = hour.hour() * 60 + hour.minute() + 59
let overflowCount = 0
while (startOfHour.isBefore(endOfHour, 'minute')) {
const dateKey = startOfHour.format('YYYY-MM-DD')
const hourKey = startOfHour.format('HH:mm')
if (recordsAcrossAllRange.value?.count?.[dateKey]?.[hourKey]?.overflow) {
isOverflow = true
recordsAcrossAllRange.value?.count?.[dateKey]?.[hourKey]?.id.forEach((id) => {
if (!ids.includes(id)) {
ids.push(id)
overflowCount += 1
}
})
}
startOfHour = startOfHour.add(1, 'minute')
for (let minute = startMinute; minute <= endMinute; minute++) {
const recordCount = gridTimeMap.get(dayIndex)?.get(minute)?.count ?? 0
overflowCount = Math.max(overflowCount, recordCount)
}
overflowCount = overflowCount > 4 ? overflowCount - 4 : 0
return { isOverflow, overflowCount }
return { isOverflow: overflowCount - 3 > 0, overflowCount: overflowCount - 3 }
}
// TODO: Add Support for multiple ranges when multiple ranges are supported
@ -773,7 +907,6 @@ watch(
@click="
() => {
selectedTime = hour
selectedDate = hour
dragRecord = undefined
}
"

4
packages/nc-gui/components/smartsheet/column/DefaultValue.vue

@ -7,8 +7,6 @@ const props = defineProps<{
}>()
const emits = defineEmits(['update:value'])
const meta = inject(MetaInj, ref())
provide(EditColumnInj, ref(true))
const vModel = useVModel(props, 'value', emits)
@ -20,7 +18,7 @@ const rowRef = ref({
},
})
useProvideSmartsheetRowStore(meta, rowRef)
useProvideSmartsheetRowStore(rowRef)
const cdfValue = ref<string | null>(null)

4
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -261,8 +261,8 @@ const onClickAudit = () => {
{{ log.description.substring(log.description.indexOf(':') + 1) }}
</div>
<div v-if="log.id === editLog?.id" class="flex justify-end gap-1">
<NcButton type="secondary" size="sm" @click="onCancel"> Cancel </NcButton>
<NcButton v-e="['a:row-expand:comment:save']" size="sm" @click="onEditComment"> Save </NcButton>
<NcButton size="small" type="secondary" @click="onCancel"> Cancel </NcButton>
<NcButton v-e="['a:row-expand:comment:save']" size="small" @click="onEditComment"> Save </NcButton>
</div>
</div>
</div>

21
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -51,6 +51,7 @@ interface Props {
lastRow?: boolean
closeAfterSave?: boolean
newRecordHeader?: string
skipReload?: boolean
}
const props = defineProps<Props>()
@ -102,7 +103,7 @@ const expandedFormScrollWrapper = ref()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const reloadViewDataTrigger = inject(ReloadViewDataHookInj)
const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
@ -137,6 +138,8 @@ provide(MetaInj, meta)
const isLoading = ref(true)
const isSaving = ref(false)
const {
commentsDrawer,
changedColumns,
@ -157,6 +160,8 @@ const duplicatingRowInProgress = ref(false)
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
useProvideSmartsheetLtarHelpers(meta)
watch(
state,
() => {
@ -205,26 +210,31 @@ const onDuplicateRow = () => {
}
const save = async () => {
isSaving.value = true
let kanbanClbk
if (activeView.value?.type === ViewTypes.KANBAN) {
kanbanClbk = (row: any, isNewRow: boolean) => {
addOrEditStackRow(row, isNewRow)
}
}
if (isNew.value) {
await _save(rowState.value, undefined, {
kanbanClbk,
})
reloadTrigger?.trigger()
reloadViewDataTrigger?.trigger()
} else {
await _save(undefined, undefined, {
kanbanClbk,
})
_loadRow()
}
if (!props.skipReload) {
reloadTrigger?.trigger()
reloadViewDataTrigger?.trigger()
}
isUnsavedFormExist.value = false
if (props.closeAfterSave) {
@ -232,6 +242,8 @@ const save = async () => {
}
emits('createdRecord', _row.value.row)
isSaving.value = false
}
const isPreventChangeModalOpen = ref(false)
@ -869,6 +881,7 @@ export default {
<NcButton
v-e="['c:row-expand:save']"
:disabled="changedColumns.size === 0 && !isUnsavedFormExist"
:loading="isSaving"
class="nc-expand-form-save-btn !xs:(text-base)"
data-testid="nc-expanded-form-save"
type="primary"
@ -915,7 +928,7 @@ export default {
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton key="submit" type="primary" label="Rename Table" loading-label="Renaming Table" @click="saveChanges">
<NcButton key="submit" type="primary" :loading="isSaving" @click="saveChanges">
{{ $t('tooltip.saveChanges') }}
</NcButton>
</div>

62
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -37,6 +37,16 @@ const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const _loadGroupData = async (group: Group, force?: boolean, params?: any) => {
isViewDataLoading.value = true
isPaginationLoading.value = true
await props.loadGroupData(group, force, params)
isViewDataLoading.value = false
isPaginationLoading.value = false
}
const _depth = props.depth ?? 0
const wrapper = ref<HTMLElement | undefined>()
@ -67,12 +77,12 @@ const findAndLoadSubGroup = (key: any) => {
if (key.length > 0 && vGroup.value.children) {
if (!oldActiveGroups.value.includes(key[key.length - 1])) {
const k = key[key.length - 1].replace('group-panel-', '')
const grp = vGroup.value.children[k]
const grp = vGroup.value.children.find((g) => `${g.key}` === k)
if (grp) {
if (grp.nested) {
if (!grp.children?.length) props.loadGroups({}, grp)
} else {
if (!grp.rows?.length || grp.count !== grp.rows?.length) props.loadGroupData(grp)
if (!grp.rows?.length || grp.count !== grp.rows?.length) _loadGroupData(grp)
}
}
}
@ -84,37 +94,35 @@ const reloadViewDataHandler = (params: void | { shouldShowLoading?: boolean | un
if (vGroup.value.nested) {
props.loadGroups({ ...(params?.offset !== undefined ? { offset: params.offset } : {}) }, vGroup.value)
} else {
props.loadGroupData(vGroup.value, true, {
_loadGroupData(vGroup.value, true, {
...(params?.offset !== undefined ? { offset: params.offset } : {}),
})
}
}
onMounted(async () => {
reloadViewDataHook?.on(reloadViewDataHandler)
})
onBeforeUnmount(async () => {
reloadViewDataHook?.off(reloadViewDataHandler)
})
reloadViewDataHook?.on(reloadViewDataHandler)
watch(
[() => vGroup.value.key],
async (n, o) => {
if (n !== o) {
isViewDataLoading.value = true
isPaginationLoading.value = true
if (vGroup.value.nested) {
await props.loadGroups({}, vGroup.value)
} else {
await props.loadGroupData(vGroup.value, true)
}
isViewDataLoading.value = false
isPaginationLoading.value = false
watch([() => vGroup.value.key], async (n, o) => {
if (n !== o) {
if (!vGroup.value.nested) {
await _loadGroupData(vGroup.value, true)
} else if (vGroup.value.nested) {
await props.loadGroups({}, vGroup.value)
}
},
{ immediate: true },
)
}
})
onMounted(async () => {
if (vGroup.value.root === true) {
await props.loadGroups({}, vGroup.value)
}
})
if (vGroup.value.root === true) provide(ScrollParentInj, wrapper)
@ -231,7 +239,7 @@ const shouldRenderCell = (column) =>
>
<a-collapse-panel
v-for="[i, grp] of Object.entries(vGroup?.children ?? [])"
:key="`group-panel-${i}`"
:key="`group-panel-${grp.key}`"
class="!border-1 nc-group rounded-[12px]"
:class="{ 'mb-4': vGroup.children && +i !== vGroup.children.length - 1 }"
:style="`background: rgb(${245 - _depth * 10}, ${245 - _depth * 10}, ${245 - _depth * 10})`"
@ -243,7 +251,7 @@ const shouldRenderCell = (column) =>
<span role="img" aria-label="right" class="anticon anticon-right ant-collapse-arrow">
<GeneralIcon
icon="chevronDown"
:style="`${activeGroups.includes(i) ? 'transform: rotate(360deg)' : 'transform: rotate(270deg)'}`"
:style="`${activeGroups.includes(grp.key) ? 'transform: rotate(360deg)' : 'transform: rotate(270deg)'}`"
></GeneralIcon>
</span>
</div>
@ -328,7 +336,7 @@ const shouldRenderCell = (column) =>
v-if="!grp.nested && grp.rows"
:group="grp"
:load-groups="loadGroups"
:load-group-data="loadGroupData"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"
@ -345,7 +353,7 @@ const shouldRenderCell = (column) =>
v-else
:group="grp"
:load-groups="loadGroups"
:load-group-data="loadGroupData"
:load-group-data="_loadGroupData"
:load-group-page="loadGroupPage"
:group-wrapper-change-page="groupWrapperChangePage"
:row-height="rowHeight"

1
packages/nc-gui/components/smartsheet/grid/GroupByTable.vue

@ -148,6 +148,7 @@ const pagination = computed(() => {
:hide-header="true"
:pagination="pagination"
:disable-skeleton="true"
:disable-virtual-y="true"
/>
</template>

943
packages/nc-gui/components/smartsheet/grid/Table.vue

File diff suppressed because it is too large Load Diff

4
packages/nc-gui/components/smartsheet/grid/index.vue

@ -20,7 +20,7 @@ import {
ref,
useSmartsheetStoreOrThrow,
useViewData,
useViewGroupBy,
useViewGroupByOrThrow,
} from '#imports'
import type { Row } from '#imports'
@ -166,7 +166,7 @@ const toggleOptimisedQuery = () => {
}
const { rootGroup, groupBy, isGroupBy, loadGroups, loadGroupData, loadGroupPage, groupWrapperChangePage, redistributeRows } =
useViewGroupBy(view, xWhere)
useViewGroupByOrThrow()
const coreWrapperRef = ref<HTMLElement>()

30
packages/nc-gui/components/smartsheet/grid/useColumnDrag.ts

@ -9,7 +9,7 @@ export const useColumnDrag = ({
tableBodyEl: Ref<HTMLElement | undefined>
gridWrapper: Ref<HTMLElement | undefined>
}) => {
const { eventBus } = useSmartsheetStoreOrThrow()
const { eventBus, isDefaultView, meta } = useSmartsheetStoreOrThrow()
const { addUndo, defineViewScope } = useUndoRedo()
const { activeView } = storeToRefs(useViewsStore())
@ -22,6 +22,24 @@ export const useColumnDrag = ({
const dragColPlaceholderDomRef = ref<HTMLElement | null>(null)
const toBeDroppedColId = ref<string | null>(null)
const updateDefaultViewColumnOrder = (columnId: string, order: number) => {
if (!meta.value?.columns) return
const colIndex = meta.value.columns.findIndex((c) => c.id === columnId)
if (colIndex !== -1) {
meta.value.columns[colIndex].meta = { ...(meta.value.columns[colIndex].meta || {}), defaultViewColOrder: order }
meta.value.columns = (meta.value.columns || []).map((c) => {
if (c.id !== columnId) return c
c.meta = { ...(c.meta || {}), defaultViewColOrder: order }
return c
})
}
if (meta.value.columnsById[columnId]) {
meta.value.columnsById[columnId].meta = { ...(meta.value.columnsById[columnId] || {}), defaultViewColOrder: order }
}
}
const reorderColumn = async (colId: string, toColId: string) => {
const toBeReorderedViewCol = gridViewCols.value[colId]
@ -46,12 +64,19 @@ export const useColumnDrag = ({
toBeReorderedViewCol.order = newOrder
if (isDefaultView.value && toBeReorderedViewCol.fk_column_id) {
updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, newOrder)
}
addUndo({
undo: {
fn: async () => {
if (!fields.value) return
toBeReorderedViewCol.order = oldOrder
if (isDefaultView.value) {
updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, oldOrder)
}
await updateGridViewColumn(colId, { order: oldOrder } as any)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)
@ -63,6 +88,9 @@ export const useColumnDrag = ({
if (!fields.value) return
toBeReorderedViewCol.order = newOrder
if (isDefaultView.value) {
updateDefaultViewColumnOrder(toBeReorderedViewCol.fk_column_id, newOrder)
}
await updateGridViewColumn(colId, { order: newOrder } as any)
eventBus.emit(SmartsheetStoreEvents.FIELD_RELOAD)

2
packages/nc-gui/components/smartsheet/header/Menu.vue

@ -56,7 +56,7 @@ const showDeleteColumnModal = ref(false)
const { gridViewCols } = useViewColumnsOrThrow()
const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view)
const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow(view)
const setAsDisplayValue = async () => {
try {

4
packages/nc-gui/components/smartsheet/toolbar/CreateGroupBy.vue

@ -12,13 +12,11 @@ const emits = defineEmits(['created'])
const { isParentOpen, columns } = toRefs(props)
const activeView = inject(ActiveViewInj, ref())
const meta = inject(MetaInj, ref())
const { showSystemFields, metaColumnById } = useViewColumnsOrThrow()
const { groupBy } = useViewGroupBy(activeView)
const { groupBy } = useViewGroupByOrThrow()
const options = computed<ColumnType[]>(
() =>

4
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -54,7 +54,7 @@ const {
toggleFieldVisibility,
} = useViewColumnsOrThrow()
const { eventBus } = useSmartsheetStoreOrThrow()
const { eventBus, isDefaultView } = useSmartsheetStoreOrThrow()
const { addUndo, defineViewScope } = useUndoRedo()
@ -127,7 +127,7 @@ const onMove = async (_event: { moved: { newIndex: number; oldIndex: number } },
fields.value.map(async (field, index) => {
if (field.order !== index + 1) {
field.order = index + 1
await saveOrUpdate(field, index, true)
await saveOrUpdate(field, index, true, !!isDefaultView.value)
}
}),
)

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

@ -22,7 +22,7 @@ const isLocked = inject(IsLockedInj, ref(false))
const { gridViewCols, updateGridViewColumn, metaColumnById, showSystemFields } = useViewColumnsOrThrow()
const { fieldsToGroupBy, groupByLimit } = useViewGroupBy(view)
const { fieldsToGroupBy, groupByLimit } = useViewGroupByOrThrow()
const { $e } = useNuxtApp()

10
packages/nc-gui/components/tabs/Smartsheet.vue

@ -21,9 +21,11 @@ import {
provide,
ref,
toRef,
useExpandedFormDetachedProvider,
useMetas,
useProvideCalendarViewStore,
useProvideKanbanViewStore,
useProvideSmartsheetLtarHelpers,
useProvideSmartsheetStore,
useRoles,
useSqlEditor,
@ -54,12 +56,10 @@ const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const { activeTableId } = storeToRefs(useTablesStore())
const { activeView, openedViewsTab, activeViewTitleOrId } = storeToRefs(useViewsStore())
const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar } = useProvideSmartsheetStore(activeView, meta)
const { isGallery, isGrid, isForm, isKanban, isLocked, isMap, isCalendar, xWhere } = useProvideSmartsheetStore(activeView, meta)
useSqlEditor()
const { isPanelExpanded } = useExtensions()
const reloadViewDataEventHook = createEventHook()
const reloadViewMetaEventHook = createEventHook<void | boolean>()
@ -84,8 +84,12 @@ provide(
ReadonlyInj,
computed(() => !isUIAllowed('dataEdit')),
)
useExpandedFormDetachedProvider()
useProvideViewColumns(activeView, meta, () => reloadViewDataEventHook?.trigger())
useProvideViewGroupBy(activeView, meta, xWhere)
useProvideSmartsheetLtarHelpers(meta)
const grid = ref()

99
packages/nc-gui/components/virtual-cell/BelongsTo.vue

@ -40,6 +40,8 @@ const { isUIAllowed } = useRoles()
const listItemsDlg = ref(false)
const isOpen = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } =
@ -47,8 +49,6 @@ const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, re
await loadRelatedTableMeta()
const addIcon = computed(() => (cellValue?.value ? 'expand' : 'plus'))
const value = computed(() => {
if (cellValue?.value) {
return cellValue?.value
@ -80,53 +80,70 @@ const belongsToColumn = computed(
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
const plusBtnRef = ref<HTMLElement | null>(null)
watch(listItemsDlg, () => {
isOpen.value = listItemsDlg.value
})
// When isOpen is false, ensure the listItemsDlg is also closed.
watch(
isOpen,
(next) => {
if (!next) {
listItemsDlg.value = false
}
},
{ flush: 'post' },
)
watch([listItemsDlg], () => {
if (!listItemsDlg.value) {
plusBtnRef.value?.focus()
watch(value, (next) => {
if (next) {
isOpen.value = false
}
})
</script>
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center w-full">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template>
</div>
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
class="flex-none flex group items-center min-w-4"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>
<GeneralIcon
icon="plus"
class="flex-none select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
/>
</div>
</div>
<template #overlay>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template>
</div>
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
ref="plusBtnRef"
class="flex justify-end group gap-1 min-h-[30px] items-center"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>
<GeneralIcon
:icon="addIcon"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
@attach-record="listItemsDlg = true"
/>
hide-back-btn
/> </template
></LazyVirtualCellComponentsLinkRecordDropdown>
</div>
</template>

127
packages/nc-gui/components/virtual-cell/HasMany.vue

@ -37,6 +37,10 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false)
const isOpen = ref(false)
const hideBackBtn = ref(false)
const { isUIAllowed } = useRoles()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -85,6 +89,31 @@ const hasManyColumn = computed(
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
hideBackBtn.value = false
}
const onAttachLinkedRecord = () => {
listItemsDlg.value = false
childListDlg.value = true
}
const openChildList = () => {
if (isUnderLookup.value) return
childListDlg.value = true
listItemsDlg.value = false
isOpen.value = true
hideBackBtn.value = false
}
const openListDlg = () => {
if (isUnderLookup.value) return
listItemsDlg.value = true
childListDlg.value = false
isOpen.value = true
hideBackBtn.value = true
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
@ -95,53 +124,75 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
break
}
})
watch([childListDlg, listItemsDlg], () => {
isOpen.value = childListDlg.value || listItemsDlg.value
})
watch(
isOpen,
(next) => {
if (!next) {
listItemsDlg.value = false
childListDlg.value = false
}
},
{ flush: 'post' },
)
</script>
<template>
<div class="flex items-center gap-1 w-full chips-wrapper">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
:column="hasManyColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
:column="hasManyColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
/>
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="openChildList"> more... </span>
</template>
</div>
<div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click.stop="openChildList"
/>
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">
more...
</span>
</template>
<GeneralIcon
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
icon="plus"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="openListDlg"
/>
</div>
</div>
<div v-if="!isUnderLookup && !isSystemColumn(column)" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click.stop="childListDlg = true"
<template #overlay>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="hasManyColumn"
:hide-back-btn="hideBackBtn"
@attach-linked-record="onAttachLinkedRecord"
/>
<GeneralIcon
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
icon="plus"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="listItemsDlg = true"
<LazyVirtualCellComponentsLinkedItems
v-if="childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="hasManyColumn"
@attach-record="onAttachRecord"
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems v-if="listItemsDlg || childListDlg" v-model="listItemsDlg" :column="hasManyColumn" />
<LazyVirtualCellComponentsLinkedItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="hasManyColumn"
@attach-record="onAttachRecord"
/>
</div>
</template>
</LazyVirtualCellComponentsLinkRecordDropdown>
</template>
<style scoped>

136
packages/nc-gui/components/virtual-cell/Links.vue

@ -25,6 +25,10 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false)
const isOpen = ref(false)
const hideBackBtn = ref(false)
const { isUIAllowed } = useRoles()
const { t } = useI18n()
@ -72,12 +76,22 @@ const toatlRecordsLinked = computed(() => {
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
hideBackBtn.value = false
}
const onAttachLinkedRecord = () => {
listItemsDlg.value = false
childListDlg.value = true
}
const openChildList = () => {
if (isUnderLookup.value) return
childListDlg.value = true
listItemsDlg.value = false
isOpen.value = true
hideBackBtn.value = false
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
@ -85,6 +99,7 @@ useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEven
case 'Enter':
if (listItemsDlg.value) return
childListDlg.value = true
isOpen.value = true
e.stopPropagation()
break
}
@ -101,69 +116,78 @@ const openListDlg = () => {
if (isUnderLookup.value) return
listItemsDlg.value = true
childListDlg.value = false
isOpen.value = true
hideBackBtn.value = true
}
const plusBtnRef = ref<HTMLElement | null>(null)
const childListDlgRef = ref<HTMLElement | null>(null)
watch([childListDlg], () => {
if (!childListDlg.value) {
childListDlgRef.value?.focus()
}
watch([childListDlg, listItemsDlg], () => {
isOpen.value = childListDlg.value || listItemsDlg.value
})
watch([listItemsDlg], () => {
if (!listItemsDlg.value) {
plusBtnRef.value?.focus()
}
})
watch(
isOpen,
(next) => {
if (!next) {
listItemsDlg.value = false
childListDlg.value = false
}
},
{ flush: 'post' },
)
</script>
<template>
<div class="nc-cell-field flex w-full group items-center nc-links-wrapper py-1" @dblclick.stop="openChildList">
<div class="block flex-shrink truncate">
<component
:is="isUnderLookup ? 'span' : 'a'"
ref="childListDlgRef"
v-e="['c:cell:links:modal:open']"
:title="textVal"
class="text-center nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !textVal }"
:tabindex="readOnly ? -1 : 0"
@click.stop.prevent="openChildList"
@keydown.enter.stop.prevent="openChildList"
>
{{ textVal }}
</component>
</div>
<div class="flex-grow" />
<div
v-if="!isUnderLookup"
ref="plusBtnRef"
:tabindex="readOnly ? -1 : 0"
class="!xs:hidden flex group justify-end group-hover:flex items-center"
@keydown.enter.stop="openListDlg"
>
<MdiPlus
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="openListDlg"
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg || childListDlg"
v-model="listItemsDlg"
:column="relatedTableDisplayColumn"
/>
<LazyVirtualCellComponentsLinkedItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:items="toatlRecordsLinked"
:column="relatedTableDisplayColumn"
:cell-value="localCellValue"
@attach-record="onAttachRecord"
/>
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full group items-center">
<div class="block flex-shrink truncate">
<component
:is="isUnderLookup ? 'span' : 'a'"
v-e="['c:cell:links:modal:open']"
:title="textVal"
class="text-center nc-datatype-link underline-transparent"
:class="{ '!text-gray-300': !textVal }"
:tabindex="readOnly ? -1 : 0"
@click.stop.prevent="openChildList"
@keydown.enter.stop.prevent="openChildList"
>
{{ textVal }}
</component>
</div>
<div class="flex-grow" />
<div
v-if="!isUnderLookup"
:tabindex="readOnly ? -1 : 0"
class="!xs:hidden flex group justify-end group-hover:flex items-center"
@keydown.enter.stop="openListDlg"
>
<MdiPlus
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="openListDlg"
/>
</div>
</div>
<template #overlay>
<LazyVirtualCellComponentsLinkedItems
v-if="childListDlg"
v-model="childListDlg"
:items="toatlRecordsLinked"
:column="relatedTableDisplayColumn"
:cell-value="localCellValue"
@attach-record="onAttachRecord"
/>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="relatedTableDisplayColumn"
:hide-back-btn="hideBackBtn"
@attach-linked-record="onAttachLinkedRecord"
/>
</template>
</LazyVirtualCellComponentsLinkRecordDropdown>
</div>
</template>

127
packages/nc-gui/components/virtual-cell/ManyToMany.vue

@ -38,6 +38,10 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false)
const isOpen = ref(false)
const hideBackBtn = ref(false)
const { isUIAllowed } = useRoles()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
@ -81,6 +85,31 @@ const unlinkRef = async (rec: Record<string, any>) => {
const onAttachRecord = () => {
childListDlg.value = false
listItemsDlg.value = true
hideBackBtn.value = false
}
const onAttachLinkedRecord = () => {
listItemsDlg.value = false
childListDlg.value = true
}
const openChildList = () => {
if (isUnderLookup.value) return
childListDlg.value = true
listItemsDlg.value = false
isOpen.value = true
hideBackBtn.value = false
}
const openListDlg = () => {
if (isUnderLookup.value) return
listItemsDlg.value = true
childListDlg.value = false
isOpen.value = true
hideBackBtn.value = true
}
useSelectedCellKeyupListener(inject(ActiveCellInj, ref(false)), (e: KeyboardEvent) => {
@ -96,53 +125,75 @@ const m2mColumn = computed(
() =>
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
watch([childListDlg, listItemsDlg], () => {
isOpen.value = childListDlg.value || listItemsDlg.value
})
watch(
isOpen,
(next) => {
if (!next) {
listItemsDlg.value = false
childListDlg.value = false
}
},
{ flush: 'post' },
)
</script>
<template>
<div class="flex items-center gap-1 w-full chips-wrapper">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
:column="m2mColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex items-center gap-1 w-full chips-wrapper">
<div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<VirtualCellComponentsItemChip
v-for="(cell, i) of cells"
:key="i"
:item="cell.item"
:value="cell.value"
:column="m2mColumn"
:show-unlink-button="true"
@unlink="unlinkRef(cell.item)"
/>
<span v-if="cells?.length === 10" class="caption pointer ml-1 grey--text" @click.stop="openChildList"> more... </span>
</template>
</div>
<div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click.stop="openChildList"
/>
<span v-if="cells?.length === 10" class="caption pointer ml-1 grey--text" @click.stop="childListDlg = true">
more...
</span>
</template>
<GeneralIcon
v-if="!readOnly && isUIAllowed('dataEdit')"
icon="plus"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="openListDlg"
/>
</div>
</div>
<div v-if="!isUnderLookup || isForm" class="flex justify-end gap-1 min-h-[30px] items-center">
<GeneralIcon
icon="expand"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click.stop="childListDlg = true"
<template #overlay>
<LazyVirtualCellComponentsLinkedItems
v-if="childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="m2mColumn"
@attach-record="onAttachRecord"
/>
<GeneralIcon
v-if="!readOnly && isUIAllowed('dataEdit')"
icon="plus"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click.stop="listItemsDlg = true"
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="m2mColumn"
:hide-back-btn="hideBackBtn"
@attach-linked-record="onAttachLinkedRecord"
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems v-if="listItemsDlg || childListDlg" v-model="listItemsDlg" :column="m2mColumn" />
<LazyVirtualCellComponentsLinkedItems
v-if="listItemsDlg || childListDlg"
v-model="childListDlg"
:cell-value="localCellValue"
:column="m2mColumn"
@attach-record="onAttachRecord"
/>
</div>
</template>
</LazyVirtualCellComponentsLinkRecordDropdown>
</template>
<style scoped>

93
packages/nc-gui/components/virtual-cell/OneToOne.vue

@ -40,6 +40,8 @@ const { isUIAllowed } = useRoles()
const listItemsDlg = ref(false)
const isOpen = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { relatedTableMeta, loadRelatedTableMeta, relatedTableDisplayValueProp, relatedTableDisplayValuePropId, unlink } =
@ -80,54 +82,63 @@ const belongsToColumn = computed(
relatedTableMeta.value?.columns?.find((c: any) => c.title === relatedTableDisplayValueProp.value) as ColumnType | undefined,
)
const plusBtnRef = ref<HTMLElement | null>(null)
watch([listItemsDlg], () => {
if (!listItemsDlg.value) {
plusBtnRef.value?.focus()
}
watch(listItemsDlg, () => {
isOpen.value = listItemsDlg.value
})
// When isOpen is false, ensure the listItemsDlg is also closed.
watch(
isOpen,
(next) => {
if (!next) {
listItemsDlg.value = false
}
},
{ flush: 'post' },
)
</script>
<template>
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
<LazyVirtualCellComponentsLinkRecordDropdown v-model:is-open="isOpen">
<div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="nc-cell-field chips flex items-center flex-1">
<template v-if="value && (relatedTableDisplayValueProp || relatedTableDisplayValuePropId)">
<VirtualCellComponentsItemChip
:item="value"
:value="
!Array.isArray(value) && typeof value === 'object'
? value[relatedTableDisplayValueProp] ?? value[relatedTableDisplayValuePropId]
: value
"
:column="belongsToColumn"
:show-unlink-button="true"
@unlink="unlinkRef(value)"
/>
</template>
</div>
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
class="flex justify-end group gap-1 min-h-[30px] items-center"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>
<GeneralIcon
:icon="addIcon"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
/>
</template>
</div>
</div>
<div
v-if="!readOnly && (isUIAllowed('dataEdit') || isForm) && !isUnderLookup"
ref="plusBtnRef"
class="flex justify-end group gap-1 min-h-[30px] items-center"
tabindex="0"
@keydown.enter.stop="listItemsDlg = true"
>
<GeneralIcon
:icon="addIcon"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus invisible group-hover:visible group-focus:visible"
@click.stop="listItemsDlg = true"
<template #overlay>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
hide-back-btn
/>
</div>
<LazyVirtualCellComponentsUnLinkedItems
v-if="listItemsDlg"
v-model="listItemsDlg"
:column="belongsToColumn"
@attach-record="listItemsDlg = true"
/>
</div>
</template>
</LazyVirtualCellComponentsLinkRecordDropdown>
</template>
<style scoped lang="scss">

2
packages/nc-gui/components/virtual-cell/QrCode.vue

@ -9,7 +9,7 @@ const cellValue = inject(CellValueInj)
const isGallery = inject(IsGalleryInj, ref(false))
const qrValue = computed(() => String(cellValue?.value))
const qrValue = computed(() => String(cellValue?.value || ''))
const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))

117
packages/nc-gui/components/virtual-cell/components/Header.vue

@ -1,20 +1,22 @@
<script lang="ts" setup>
import OnetoOneIcon from '~icons/nc-icons/onetoone'
import InfoIcon from '~icons/nc-icons/info'
import FileIcon from '~icons/nc-icons/file'
import { iconMap } from '#imports'
const { relation, relatedTableTitle, displayValue, header, tableTitle } = defineProps<{
const {
relation,
relatedTableTitle,
tableTitle,
linkedRecords = 0,
} = defineProps<{
relation: string
header?: string | null
tableTitle: string
relatedTableTitle: string
displayValue?: string
linkedRecords?: number
}>()
const { isMobileMode } = useGlobal()
const { t } = useI18n()
const relationMeta = computed(() => {
@ -52,73 +54,46 @@ const relationMeta = computed(() => {
</script>
<template>
<div class="flex sm:justify-between relative pb-2 items-center">
<div v-if="!isMobileMode" class="flex text-base font-bold justify-start items-center min-w-36">
{{ header ?? '' }}
</div>
<div class="flex flex-row sm:w-[calc(100%-16rem)] xs:w-full items-center justify-center gap-2 xs:(h-full)">
<div class="flex sm:justify-end w-[calc(50%-1.5rem)] xs:(w-[calc(50%-1.5rem)] h-full)">
<div
class="flex max-w-full xs:w-full flex-shrink-0 xs:(h-full) rounded-md gap-1 text-gray-700 items-center bg-gray-100 px-2 py-1"
>
<FileIcon class="w-4 h-4 min-w-4" />
<span class="truncate">
{{ displayValue }}
</span>
</div>
</div>
<NcTooltip class="flex-shrink-0">
<template #title> {{ relationMeta.title }} </template>
<component
:is="relationMeta.icon"
class="w-7 h-7 p-1 rounded-md"
:class="{
'!bg-orange-500': relation === 'hm',
'!bg-pink-500': relation === 'mm',
'!bg-blue-500': relation === 'bt',
}"
/>
</NcTooltip>
<div class="flex justify-start xs:w-[calc(50%-1.5rem)] w-[calc(50%-1.5rem)] xs:justify-start">
<div
class="flex rounded-md max-w-full flex-shrink-0 gap-1 items-center px-2 py-1 xs:w-full overflow-hidden"
:class="{
'!bg-orange-50 !text-orange-500': relation === 'hm',
'!bg-pink-50 !text-pink-500': relation === 'mm',
'!bg-blue-50 !text-blue-500': relation === 'bt',
}"
>
<MdiFileDocumentMultipleOutline
class="w-4 h-4 min-w-4"
:class="{
'!text-orange-500': relation === 'hm',
'!text-pink-500': relation === 'mm',
'!text-blue-500': relation === 'bt',
}"
/>
<span class="truncate"> {{ relatedTableTitle }} Records </span>
</div>
</div>
</div>
<div v-if="!isMobileMode" class="flex flex-row justify-end w-36">
<NcTooltip class="z-10" placement="bottom">
<template #title>
<div class="p-1">
<h1 class="text-white font-bold">{{ relationMeta.title }}</h1>
<div class="text-white">
{{ relationMeta.tooltip_desc }}
<span class="bg-gray-700 px-2 rounded-md">
{{ tableTitle }}
</span>
{{ relationMeta.tooltip_desc2 }}
<span class="bg-gray-700 px-2 rounded-md">
{{ relatedTableTitle }}
</span>
</div>
<div
class="flex-none flex rounded-md gap-1 items-center p-1 max-h-7"
:class="{
'bg-gray-200 text-gray-600': !linkedRecords,
'bg-orange-100 text-orange-700': relation === 'hm' && linkedRecords,
'bg-pink-100 text-pink-700': relation === 'mm' && linkedRecords,
'bg-blue-100 text-blue-700': relation === 'bt' && linkedRecords,
'bg-purple-100 text-purple-700': relation === 'oo' && linkedRecords,
}"
>
<NcTooltip class="z-10 flex" placement="bottom">
<template #title>
<div class="p-1">
<h1 class="text-white font-bold">{{ relationMeta.title }}</h1>
<div class="text-white">
{{ relationMeta.tooltip_desc }}
<span class="bg-gray-700 px-2 rounded-md">
{{ tableTitle }}
</span>
{{ relationMeta.tooltip_desc2 }}
<span class="bg-gray-700 px-2 rounded-md">
{{ relatedTableTitle }}
</span>
</div>
</template>
<InfoIcon class="w-4 h-4" />
</NcTooltip>
</div>
</template>
<component
:is="relationMeta.icon"
class="flex-none w-5 h-5 p-1 rounded-md"
:class="{
'!bg-orange-500': relation === 'hm',
'!bg-pink-500': relation === 'mm',
'!bg-blue-500': relation === 'bt',
}"
/>
</NcTooltip>
<div class="leading-[20px]">
{{ linkedRecords || 0 }} {{ $t('general.linked') }}
{{ linkedRecords === 1 ? $t('objects.record') : $t('objects.records') }}
</div>
</div>
</template>

85
packages/nc-gui/components/virtual-cell/components/LinkRecordDropdown.vue

@ -0,0 +1,85 @@
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
isOpen: boolean
}
const props = withDefaults(defineProps<Props>(), {
isOpen: false,
})
const emits = defineEmits(['update:isOpen'])
const isOpen = useVModel(props, 'isOpen', emits)
const ncLinksDropdownRef = ref<HTMLDivElement>()
const randomClass = `link-records_${Math.floor(Math.random() * 99999)}`
const addOrRemoveClass = (add: boolean = false) => {
const dropdownRoot = ncLinksDropdownRef.value?.parentElement?.parentElement?.parentElement?.parentElement as HTMLElement
if (dropdownRoot) {
if (add) {
dropdownRoot.classList.add('inset-0', 'nc-link-dropdown-root', `nc-root-${randomClass}`)
} else {
dropdownRoot.classList.remove('inset-0', 'nc-link-dropdown-root', `nc-root-${randomClass}`)
}
}
}
watch(
isOpen,
(next) => {
if (next) {
onClickOutside(document.querySelector(`.${randomClass}`)! as HTMLDivElement, (e) => {
const targetEl = e?.target as HTMLElement
if (!targetEl?.classList.contains(`nc-root-${randomClass}`) || targetEl?.closest(`.nc-${randomClass}`)) {
return
}
isOpen.value = false
addOrRemoveClass(false)
})
} else {
addOrRemoveClass(false)
}
},
{ flush: 'post' },
)
watch([ncLinksDropdownRef, isOpen], () => {
if (!ncLinksDropdownRef.value) return
if (isOpen.value) {
addOrRemoveClass(true)
} else {
addOrRemoveClass(false)
}
})
</script>
<template>
<NcDropdown
:visible="isOpen"
placement="bottom"
overlay-class-name="nc-links-dropdown !min-w-[540px]"
:class="`.nc-${randomClass}`"
>
<slot />
<template #overlay>
<div ref="ncLinksDropdownRef" class="h-[412px] w-[540px]" :class="`${randomClass}`">
<slot name="overlay" />
</div>
</template>
</NcDropdown>
</template>
<style lang="scss">
.nc-links-dropdown {
z-index: 1000 !important;
}
.nc-link-dropdown-root {
z-index: 1000;
}
</style>

416
packages/nc-gui/components/virtual-cell/components/LinkedItems.vue

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { type ColumnType, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import {
ColumnInj,
IsFormInj,
@ -30,10 +31,14 @@ const vModel = useVModel(props, 'modelValue', emit)
const { isMobileMode } = useGlobal()
const { t } = useI18n()
const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const isExpandedFormCloseAfterSave = ref(false)
const injectedColumn = inject(ColumnInj, ref())
const readOnly = inject(ReadonlyInj, ref(false))
@ -58,7 +63,7 @@ const {
relatedTableMeta,
link,
meta,
headerDisplayValue,
row,
resetChildrenListOffsetCount,
} = useLTARStoreOrThrow()
@ -68,7 +73,7 @@ watch(
[vModel, isForm],
(nextVal) => {
if ((nextVal[0] || nextVal[1]) && !isNew.value) {
loadChildrenList()
loadChildrenList(true)
}
// reset offset count when closing modal
@ -102,20 +107,96 @@ const attachmentCol = computedInject(FieldsInj, (_fields) => {
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
.slice(0, isMobileMode.value ? 1 : 4)
.sort((a, b) => {
if (a.meta?.defaultViewColOrder !== undefined && b.meta?.defaultViewColOrder !== undefined) {
return a.meta.defaultViewColOrder - b.meta.defaultViewColOrder
}
return 0
})
.slice(0, isMobileMode.value ? 1 : 3)
})
const expandedFormDlg = ref(false)
const expandedFormRow = ref({})
/** populate initial state for a new row which is parent/child of current record */
const newRowState = computed(() => {
if (isNew.value) return {}
const colOpt = (injectedColumn?.value as ColumnType)?.colOptions as LinkToAnotherRecordType
const colInRelatedTable: ColumnType | undefined = relatedTableMeta?.value?.columns?.find((col) => {
// Links as for the case of 'mm' we need the 'Links' column
if (!isLinksOrLTAR(col)) return false
const colOpt1 = col?.colOptions as LinkToAnotherRecordType
if (colOpt1?.fk_related_model_id !== meta.value.id) return false
if (colOpt.type === RelationTypes.MANY_TO_MANY && colOpt1?.type === RelationTypes.MANY_TO_MANY) {
return (
colOpt.fk_parent_column_id === colOpt1.fk_child_column_id && colOpt.fk_child_column_id === colOpt1.fk_parent_column_id
)
} else {
return (
colOpt.fk_parent_column_id === colOpt1.fk_parent_column_id && colOpt.fk_child_column_id === colOpt1.fk_child_column_id
)
}
})
if (!colInRelatedTable) return {}
const relatedTableColOpt = colInRelatedTable?.colOptions as LinkToAnotherRecordType
if (!relatedTableColOpt) return {}
if (relatedTableColOpt.type === RelationTypes.BELONGS_TO) {
return {
[colInRelatedTable.title as string]: row?.value?.row,
}
} else {
return {
[colInRelatedTable.title as string]: row?.value && [row.value.row],
}
}
})
const colTitle = computed(() => injectedColumn.value?.title || '')
const onClick = (row: Row) => {
if (readOnly.value) return
if (readOnly.value || isForm.value) return
expandedFormRow.value = row
expandedFormDlg.value = true
}
const addNewRecord = () => {
expandedFormRow.value = {}
expandedFormDlg.value = true
isExpandedFormCloseAfterSave.value = true
}
const onCreatedRecord = (record: any) => {
const msgVNode = h(
'div',
{
class: 'ml-1 inline-flex flex-col gap-1 items-start',
},
[
h(
'span',
{
class: 'font-semibold',
},
t('activity.recordCreatedLinked'),
),
h(
'span',
{
class: 'text-gray-500',
},
t('activity.gotSavedLinkedSuccessfully', {
tableName: relatedTableMeta.value?.title,
recordTitle: record[relatedTableDisplayValueProp.value],
}),
),
],
)
message.success(msgVNode)
}
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
@ -129,6 +210,9 @@ watch(
)
watch(expandedFormDlg, () => {
if (!expandedFormDlg.value) {
isExpandedFormCloseAfterSave.value = false
}
childrenExcludedOffsetCount.value = 0
childrenListOffsetCount.value = 0
})
@ -154,6 +238,10 @@ const skeletonCount = computed(() => {
})
const totalItemsToShow = computed(() => {
if (isForm.value || isNew.value) {
return state.value?.[colTitle.value]?.length
}
if (isChildrenLoading.value) {
return props.items
}
@ -204,6 +292,10 @@ const linkedShortcuts = (e: KeyboardEvent) => {
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
setTimeout(() => {
filterQueryRef.value?.focus()
}, 100)
})
const childrenListRef = ref<HTMLDivElement>()
@ -226,167 +318,151 @@ const onFilterChange = () => {
</script>
<template>
<NcModal
v-model:visible="vModel"
:body-style="{ 'max-height': '640px', 'height': '85vh' }"
:class="{ active: vModel }"
:closable="false"
:footer="null"
:width="isForm ? 600 : 800"
size="medium"
wrap-class-name="nc-modal-child-list"
>
<LazyVirtualCellComponentsHeader
v-if="!isForm"
:display-value="headerDisplayValue"
:header="$t('activity.linkedRecords')"
:linked-records="childrenListCount"
:related-table-title="relatedTableMeta?.title"
:relation="relation"
:table-title="meta?.title"
/>
<div v-if="!isForm" class="flex mt-2 mb-2 items-center gap-2">
<div class="flex items-center border-1 p-1 rounded-md w-full border-gray-200 !focus-within:border-primary">
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
v-model:value="childrenListPagination.query"
:bordered="false"
:placeholder="`Search in ${relatedTableMeta?.title}`"
class="w-full !sm:rounded-md xs:min-h-8 !xs:rounded-xl"
size="small"
@change="onFilterChange"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
<div class="nc-modal-child-list h-full w-full" :class="{ active: vModel }" @keydown.enter.stop>
<div class="flex flex-col h-full">
<div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-md flex justify-between pl-3 pr-2 gap-2">
<div v-if="!isForm" class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md">
<MdiMagnify class="nc-search-icon w-5 h-5" />
<a-input
ref="filterQueryRef"
v-model:value="childrenListPagination.query"
:bordered="false"
placeholder="Search linked records..."
class="w-full min-h-4"
size="small"
@change="onFilterChange"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
}
"
>
</a-input>
"
>
</a-input>
</div>
<div v-else>&nbsp;</div>
<LazyVirtualCellComponentsHeader
data-testid="nc-link-count-info"
:linked-records="totalItemsToShow"
:related-table-title="relatedTableMeta?.title"
:relation="relation"
:table-title="meta?.title"
/>
</div>
</div>
<div ref="childrenListRef" class="flex flex-col flex-grow nc-scrollbar-md cursor-pointer pr-1">
<div v-if="isDataExist || isChildrenLoading" class="mt-2 mb-2">
<div class="cursor-pointer pr-1">
<template v-if="isChildrenLoading">
<div
v-for="(_x, i) in Array.from({ length: skeletonCount })"
:key="i"
class="!border-2 flex flex-row gap-2 mb-2 transition-all !rounded-xl relative !border-gray-200 hover:bg-gray-50"
>
<a-skeleton-image class="h-24 w-24 !rounded-xl" />
<div class="flex flex-col m-[.5rem] gap-2 flex-grow justify-center">
<a-skeleton-input active class="!w-48 !rounded-xl" size="small" />
<div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!h-4 !w-24" size="small" />
<div ref="childrenListRef" class="flex-1 overflow-auto nc-scrollbar-thin">
<div v-if="isDataExist || isChildrenLoading">
<div class="cursor-pointer">
<template v-if="isChildrenLoading">
<div
v-for="(_x, i) in Array.from({ length: skeletonCount })"
:key="i"
class="flex flex-row gap-2 mb-2 transition-all relative !border-gray-200 hover:bg-gray-50"
>
<div class="flex items-center">
<a-skeleton-image class="h-14 w-14 !rounded-xl children:!h-full" />
</div>
<div class="flex flex-col gap-2 flex-grow justify-center">
<a-skeleton-input active class="h-3 !w-48 !rounded-xl" size="small" />
<div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenList?.list ?? state?.[colTitle] ?? []"
:key="id"
:attachment="attachmentCol"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:fields="fields"
:is-linked="childrenList?.list ? isChildrenListLinked[Number.parseInt(id)] : true"
:is-loading="isChildrenListLoading[Number.parseInt(id)]"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:row="refRow"
data-testid="nc-child-list-item"
@click="linkOrUnLink(refRow, id)"
@expand="onClick(refRow)"
@keydown.space.prevent="linkOrUnLink(refRow, id)"
@keydown.enter.prevent="() => onClick(refRow, id)"
/>
</template>
</div>
</div>
<div v-else class="pt-1 flex flex-col gap-4 my-auto items-center justify-center text-gray-500 text-center">
<img
:alt="$t('msg.clickLinkRecordsToAddLinkFromTable', { tableName: relatedTableMeta?.title })"
class="!w-[18.5rem] flex-none"
src="~assets/img/placeholder/link-records.png"
/>
<div class="text-2xl text-gray-700 font-bold">{{ $t('msg.noLinkedRecords') }}</div>
<div class="text-gray-700">
{{ $t('msg.clickLinkRecordsToAddLinkFromTable', { tableName: relatedTableMeta?.title }) }}
</template>
<template v-else>
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenList?.list ?? state?.[colTitle] ?? []"
:key="id"
:attachment="attachmentCol"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:fields="fields"
:is-linked="childrenList?.list ? isChildrenListLinked[Number.parseInt(id)] : true"
:is-loading="isChildrenListLoading[Number.parseInt(id)]"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:row="refRow"
data-testid="nc-child-list-item"
@link-or-unlink="linkOrUnLink(refRow, id)"
@expand="onClick(refRow)"
@keydown.space.prevent.stop="linkOrUnLink(refRow, id)"
@keydown.enter.prevent.stop="() => onClick(refRow, id)"
/>
</template>
</div>
</div>
<div v-else class="h-full flex flex-col gap-2 my-auto items-center justify-center text-gray-500 text-center">
<img
:alt="$t('msg.clickLinkRecordsToAddLinkFromTable')"
class="!w-[158px] flex-none"
src="~assets/img/placeholder/link-records.png"
/>
<div class="text-base text-gray-700 font-bold">{{ $t('msg.noLinkedRecords') }}</div>
<div class="text-gray-700">
{{ $t('msg.clickLinkRecordsToAddLinkFromTable') }}
</div>
<NcButton
v-if="!readOnly && childrenListCount < 1"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to"
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1"><MdiPlus /> {{ $t('title.linkRecords') }}</div>
</NcButton>
<NcButton
v-if="!readOnly && (childrenListCount < 1 || (childrenList?.list ?? state?.[colTitle] ?? []).length > 0)"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to"
size="small"
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1"><MdiPlus /> {{ $t('title.linkRecords') }}</div>
</NcButton>
</div>
</div>
</div>
<div v-if="isMobileMode" class="flex flex-row justify-center items-center w-full my-2">
<NcPagination
v-if="!isNew && childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
:total="+childrenList.pageInfo.totalRows!"
/>
</div>
<div class="my-2 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex flex-row justify-between bg-white relative pt-1">
<div v-if="!isForm" class="flex items-center justify-center px-2 rounded-md text-gray-500 bg-brand-50">
{{ totalItemsToShow || 0 }} {{ !isMobileMode ? $t('objects.records') : '' }}
{{ !isMobileMode && totalItemsToShow !== 0 ? $t('general.are') : '' }}
{{ $t('general.linked') }}
</div>
<div v-else class="flex items-center justify-center px-2 rounded-md text-gray-500 bg-brand-50">
<span class="">
{{ state?.[colTitle]?.length || 0 }} {{ $t('objects.records') }}
{{ state?.[colTitle]?.length !== 0 ? $t('general.are') : '' }}
{{ $t('general.linked') }}
</span>
</div>
<div class="!xs:hidden flex absolute -mt-0.75 items-center py-2 justify-center w-full">
<NcPagination
v-if="!isNew && childrenList?.pageInfo"
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
:total="+childrenList.pageInfo.totalRows!"
mode="simple"
/>
</div>
<div class="flex flex-row gap-2">
<NcButton v-if="!isForm" class="nc-close-btn" type="ghost" @click="vModel = false"> {{ $t('general.finish') }} </NcButton>
<NcButton
v-if="!readOnly && childrenListCount > 0"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to"
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1">
<MdiPlus class="!xs:hidden" /> {{ isMobileMode ? $t('title.linkMore') : $t('title.linkMoreRecords') }}
<div class="bg-gray-100 px-3 py-2 rounded-b-md flex items-center justify-between gap-3 min-h-12">
<div class="flex items-center gap-2">
<NcButton
v-if="!isPublic"
v-e="['c:row-expand:open']"
size="small"
class="!hover:(bg-white text-brand-500)"
type="secondary"
@click="addNewRecord"
>
<div class="flex items-center gap-1">
<MdiPlus v-if="!isMobileMode" class="h-4 w-4" /> {{ $t('activity.newRecord') }}
</div>
</NcButton>
<NcButton
v-if="!readOnly && (childrenListCount > 0 || (childrenList?.list ?? state?.[colTitle] ?? []).length > 0)"
v-e="['c:links:link']"
data-testid="nc-child-list-button-link-to"
class="!hover:(bg-white text-brand-500)"
size="small"
type="secondary"
@click="emit('attachRecord')"
>
<div class="flex items-center gap-1">
<GeneralIcon icon="link2" class="!xs:hidden h-4 w-4" />
{{ isMobileMode ? $t('title.linkMore') : $t('title.linkMoreRecords') }}
</div>
</NcButton>
</div>
<template v-if="!isNew && childrenList?.pageInfo && +childrenList.pageInfo.totalRows! > childrenListPagination.size">
<div class="flex justify-center items-center">
<NcPagination
v-model:current="childrenListPagination.page"
v-model:page-size="childrenListPagination.size"
:total="+childrenList.pageInfo.totalRows!"
mode="simple"
/>
</div>
</NcButton>
</template>
</div>
</div>
@ -394,7 +470,15 @@ const onFilterChange = () => {
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:close-after-save="isExpandedFormCloseAfterSave"
:meta="relatedTableMeta"
:new-record-header="
isExpandedFormCloseAfterSave
? $t('activity.tableNameCreateNewRecord', {
tableName: relatedTableMeta?.title,
})
: undefined
"
:row="{
row: expandedFormRow,
oldRow: expandedFormRow,
@ -405,11 +489,13 @@ const onFilterChange = () => {
new: true,
},
}"
:state="newRowState"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
use-meta-fields
@created-record="onCreatedRecord"
/>
</Suspense>
</NcModal>
</div>
</template>
<style lang="scss" scoped>
@ -420,10 +506,22 @@ const onFilterChange = () => {
:deep(.ant-modal-content) {
@apply !p-0;
}
:deep(.ant-skeleton-element .ant-skeleton-image) {
@apply !h-full;
}
</style>
<style lang="scss">
.nc-modal-child-list > .ant-modal > .ant-modal-content {
@apply !p-0;
.nc-dropdown-link-record-search-wrapper {
.nc-search-icon {
@apply flex-none text-gray-500;
}
&:focus-within {
.nc-search-icon {
@apply text-gray-600;
}
}
}
</style>

291
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -16,19 +16,23 @@ import {
useVModel,
} from '#imports'
import MaximizeIcon from '~icons/nc-icons/maximize'
import LinkIcon from '~icons/nc-icons/link'
const props = defineProps<{
row: any
fields: any[]
attachment: any
relatedTableDisplayValueProp: string
displayValueTypeAndFormatProp: { type: string; format: string }
isLoading: boolean
isLinked: boolean
}>()
const props = withDefaults(
defineProps<{
row: any
fields: any[]
attachment: any
relatedTableDisplayValueProp: string
displayValueTypeAndFormatProp: { type: string; format: string }
isLoading: boolean
isLinked: boolean
}>(),
{
isLoading: false,
},
)
defineEmits(['expand'])
defineEmits(['expand', 'linkOrUnlink'])
provide(IsExpandedFormOpenInj, ref(true))
@ -88,116 +92,198 @@ const displayValue = computed(() => {
</script>
<template>
<a-card
tabindex="0"
class="nc-list-item !outline-brand-500 !border-1 group transition-all !rounded-xl relative !mb-2 !border-gray-200 hover:bg-gray-50"
:class="{
'!bg-white': isLoading,
'!border-1': isLinked && !isLoading,
'!cursor-auto !hover:bg-white': readOnly,
}"
:body-style="{ padding: 0 }"
:hoverable="false"
>
<div class="flex flex-row items-center justify-start w-full">
<a-carousel v-if="attachment && attachments && attachments.length" autoplay class="!w-24 !h-24 !max-h-24 !max-w-24">
<template #customPaging> </template>
<template v-for="(attachmentObj, index) in attachments">
<LazyCellAttachmentImage
v-if="isImage(attachmentObj.title, attachmentObj.mimetype ?? attachmentObj.type)"
:key="`carousel-${attachmentObj.title}-${index}`"
class="!h-24 !w-24 !max-h-24 !max-w-24 object-cover !rounded-l-xl"
:srcs="getPossibleAttachmentSrc(attachmentObj)"
/>
</template>
</a-carousel>
<div
v-else-if="attachment"
class="h-24 w-24 !min-h-24 !min-w-24 !max-h-24 !max-w-24 !flex flex-row items-center !rounded-l-xl justify-center"
>
<GeneralIcon class="w-full h-full !text-6xl !leading-10 !text-transparent rounded-lg" icon="fileImage" />
</div>
<div class="nc-list-item-wrapper group px-[1px] hover:bg-gray-50 border-y-1 border-gray-200 border-t-transparent">
<a-card
tabindex="0"
class="nc-list-item !outline-none transition-all relative group-hover:bg-gray-50 cursor-auto"
:class="{
'!bg-white': isLoading,
'!hover:bg-white': readOnly,
}"
:body-style="{ padding: '6px 10px !important', borderRadius: 0 }"
:hoverable="false"
>
<div class="flex items-center gap-3">
<div v-if="isLoading" class="flex">
<MdiLoading class="flex-none w-7 h-7 !text-brand-500 animate-spin" />
</div>
<div class="flex flex-col m-[.75rem] gap-1 flex-grow justify-center overflow-hidden">
<div class="flex justify-between xs:gap-x-2">
<span class="font-semibold text-brand-500 nc-display-value xs:(truncate)">
{{ displayValue }}
</span>
<div
v-if="isLinked && !isLoading"
class="text-brand-500 text-0.875"
<NcTooltip v-else class="z-10 flex">
<template #title> {{ isLinked ? 'Unlink' : 'Link' }}</template>
<button
tabindex="-1"
class="nc-list-item-link-unlink-btn p-1.5 flex rounded-lg transition-all"
:class="{
'!group-hover:mr-12': fields.length === 0 && !readOnly,
'bg-red-100 text-red-500 hover:bg-red-200': isLinked,
'bg-green-100 text-green-500 hover:bg-green-200': !isLinked,
}"
@click="$emit('linkOrUnlink')"
>
<LinkIcon class="w-4 h-4" />
Linked
<GeneralIcon :icon="isLinked ? 'minus' : 'plus'" class="flex-none w-4 h-4 !font-extrabold" />
</button>
</NcTooltip>
<template v-if="attachment">
<div v-if="attachments && attachments.length">
<a-carousel autoplay class="!w-11 !h-11 !max-h-11 !max-w-11">
<template #customPaging> </template>
<template v-for="(attachmentObj, index) in attachments">
<LazyCellAttachmentImage
v-if="isImage(attachmentObj.title, attachmentObj.mimetype ?? attachmentObj.type)"
:key="`carousel-${attachmentObj.title}-${index}`"
class="!w-11 !h-11 !max-h-11 !max-w-11object-cover !rounded-l-xl"
:srcs="getPossibleAttachmentSrc(attachmentObj)"
/>
</template>
</a-carousel>
</div>
<MdiLoading
v-else-if="isLoading"
:class="{
'!group-hover:mr-8': fields.length === 0 && !readOnly,
}"
class="w-6 h-6 !text-brand-500 animate-spin"
/>
</div>
<div
v-else
class="h-11 w-11 !min-h-11 !min-w-11 !max-h-11 !max-w-11 !flex flex-row items-center !rounded-l-xl justify-center"
>
<GeneralIcon class="w-full h-full !text-6xl !leading-10 !text-transparent rounded-lg" icon="fileImage" />
</div>
</template>
<div
v-if="fields.length > 0 && !isPublic && !isForm"
class="flex ml-[-0.25rem] sm:flex-row xs:(flex-col mt-2) gap-4 w-10/12"
>
<div v-for="field in fields" :key="field.id" :class="attachment ? 'sm:w-1/3' : 'sm:w-1/4'">
<div class="flex flex-col gap-[-1] max-w-72">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
class="!scale-60"
:column="field"
:hide-menu="true"
:hide-icon="true"
/>
<LazySmartsheetHeaderCell v-else class="!scale-70" :column="field" :hide-menu="true" :hide-icon="true" />
<div v-if="!isRowEmpty(row, field)">
<LazySmartsheetVirtualCell v-if="isVirtualCol(field)" v-model="row[field.title]" :row="row" :column="field" />
<LazySmartsheetCell
v-else
v-model="row[field.title]"
class="!text-gray-600 ml-1"
:column="field"
:edit-enabled="false"
:read-only="true"
/>
<div class="flex-1 flex flex-col gap-1 justify-center overflow-hidden">
<div class="flex justify-start">
<span class="font-semibold text-brand-500 nc-display-value truncate leading-[20px]">
{{ displayValue }}
</span>
</div>
<div
v-if="fields.length > 0 && !isPublic && !isForm"
class="flex ml-[-0.25rem] sm:flex-row xs:(flex-col mt-2) gap-4 min-h-5"
>
<div v-for="field in fields" :key="field.id" class="sm:(w-1/3 max-w-1/3 overflow-hidden)">
<div v-if="!isRowEmpty(row, field)" class="flex flex-col gap-[-1]">
<NcTooltip class="z-10 flex" placement="bottom">
<template #title>
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
class="!scale-60 text-gray-100 !text-sm"
:column="field"
:hide-menu="true"
/>
<LazySmartsheetHeaderCell v-else class="!scale-70 text-gray-100 !text-sm" :column="field" :hide-menu="true" />
</template>
<div class="nc-link-record-cell flex w-full max-w-full">
<LazySmartsheetVirtualCell v-if="isVirtualCol(field)" v-model="row[field.title]" :row="row" :column="field" />
<LazySmartsheetCell
v-else
v-model="row[field.title]"
:column="field"
:edit-enabled="false"
:read-only="true"
/>
</div>
</NcTooltip>
</div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
<div v-else class="flex flex-row w-full max-w-72 h-5 pl-1 items-center justify-start">-</div>
</div>
</div>
</div>
<div v-if="!isForm && !isPublic && !readOnly" class="flex-none flex items-center w-7">
<button
v-e="['c:row-expand:open']"
:tabindex="-1"
class="z-10 flex items-center justify-center nc-expand-item !group-hover:visible !invisible !h-7 !w-7 transition-all !hover:children:(w-4.5 h-4.5)"
@click.stop="$emit('expand', row)"
>
<MaximizeIcon class="flex-none w-4 h-4 scale-125" />
</button>
</div>
</div>
</div>
<NcButton
v-if="!isForm && !isPublic && !readOnly"
v-e="['c:row-expand:open']"
type="text"
size="medium"
class="!px-2 nc-expand-item !group-hover:block !hidden !border-1 !shadow-sm !border-gray-200 !bg-white !absolute right-3 bottom-3"
:class="{
'!group-hover:right-1.8 !group-hover:bottom-1.7': fields.length === 0,
}"
@click.stop="$emit('expand', row)"
>
<MaximizeIcon class="w-4 h-4" />
</NcButton>
</a-card>
</a-card>
</div>
</template>
<style lang="scss" scoped>
:deep(.slick-list) {
@apply rounded-lg;
}
.nc-list-item-link-unlink-btn {
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.06), 0px 5px 3px -2px rgba(0, 0, 0, 0.02);
}
.nc-link-record-cell {
:deep(.nc-cell),
:deep(.nc-virtual-cell) {
@apply !text-small !text-gray-600 ml-1;
.nc-cell-field,
.nc-cell-field-link,
input,
textarea {
@apply !text-small !p-0 m-0;
}
&:not(.nc-display-value-cell) {
@apply text-gray-600;
font-weight: 500;
.nc-cell-field,
input,
textarea {
@apply text-gray-600;
font-weight: 500;
}
}
.nc-cell-field,
a.nc-cell-field-link,
input,
textarea {
@apply !p-0 m-0;
}
&.nc-cell-longtext {
@apply leading-[18px];
textarea {
@apply pr-2;
}
.long-text-wrapper {
@apply !min-h-4;
.nc-rich-text-grid {
@apply pl-0 -ml-1;
}
}
}
.ant-picker-input {
@apply text-small leading-4;
font-weight: 500;
input {
@apply text-small leading-4;
font-weight: 500;
}
}
.ant-select:not(.ant-select-customize-input) {
.ant-select-selector {
@apply !border-none flex-nowrap pr-4.5;
}
.ant-select-arrow {
@apply right-[3px];
}
}
}
}
</style>
<style lang="scss">
.nc-list-item {
@apply border-1 border-transparent rounded-md;
&:focus-visible {
@apply border-brand-500;
box-shadow: 0 0 0 1px #3366ff;
}
&:hover {
.nc-text-area-expand-btn {
@apply !hidden;
@ -206,13 +292,14 @@ const displayValue = computed(() => {
.long-text-wrapper {
@apply select-none pointer-events-none;
.nc-readonly-rich-text-wrapper {
@apply !min-h-6 !max-h-6;
@apply !min-h-5 !max-h-5;
}
.nc-rich-text-embed {
@apply -mt-0.5;
.nc-textarea-rich-editor {
@apply !overflow-hidden;
.ProseMirror {
@apply !overflow-hidden line-clamp-1;
@apply !overflow-hidden line-clamp-1 h-[18px] pt-0.4;
}
}
}

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

@ -14,9 +14,9 @@ import {
useVModel,
} from '#imports'
const props = defineProps<{ modelValue: boolean; column: any }>()
const props = defineProps<{ modelValue: boolean; column: any; hideBackBtn?: boolean }>()
const emit = defineEmits(['update:modelValue', 'addNewRecord'])
const emit = defineEmits(['update:modelValue', 'addNewRecord', 'attachLinkedRecord'])
const vModel = useVModel(props, 'modelValue', emit)
@ -50,7 +50,6 @@ const {
meta,
unlink,
row,
headerDisplayValue,
resetChildrenExcludedOffsetCount,
} = useLTARStoreOrThrow()
@ -66,6 +65,10 @@ const isForm = inject(IsFormInj, ref(false))
const saveRow = inject(SaveRowInj, () => {})
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const reloadViewDataTrigger = inject(ReloadViewDataHookInj, createEventHook())
const linkRow = async (row: Record<string, any>, id: number) => {
if (isNew.value) {
addLTARRef(row, injectedColumn?.value as ColumnType)
@ -100,7 +103,7 @@ watch(
if (!isForm.value) {
loadChildrenList()
}
loadChildrenExcludedList(rowState.value)
loadChildrenExcludedList(rowState.value, true)
}
if (!nextVal) {
resetChildrenExcludedOffsetCount()
@ -157,13 +160,31 @@ const attachmentCol = computedInject(FieldsInj, (_fields) => {
const fields = computedInject(FieldsInj, (_fields) => {
return (relatedTableMeta.value.columns ?? [])
.filter((col) => !isSystemColumn(col) && !isPrimary(col) && !isLinksOrLTAR(col) && !isAttachment(col))
.slice(0, isMobileMode.value ? 1 : 4)
.sort((a, b) => {
if (a.meta?.defaultViewColOrder !== undefined && b.meta?.defaultViewColOrder !== undefined) {
return a.meta.defaultViewColOrder - b.meta.defaultViewColOrder
}
return 0
})
.slice(0, isMobileMode.value ? 1 : 3)
})
const relation = computed(() => {
return injectedColumn!.value?.colOptions?.type
})
const totalItemsToShow = computed(() => {
if (relation.value === 'bt') {
return row.value.row[relatedTableMeta.value?.title] ? 1 : 0
}
if (isForm.value || isNew.value) {
return rowState.value?.[injectedColumn!.value?.title]?.length ?? 0
}
return childrenListCount.value ?? 0
})
watch(expandedFormDlg, () => {
if (!expandedFormDlg.value) {
isExpandedFormCloseAfterSave.value = false
@ -196,6 +217,15 @@ const addNewRecord = () => {
}
const onCreatedRecord = (record: any) => {
addLTARRef(record, injectedColumn?.value as ColumnType)
reloadTrigger?.trigger({
shouldShowLoading: false,
})
reloadViewDataTrigger?.trigger({
shouldShowLoading: false,
})
const msgVNode = h(
'div',
{
@ -223,6 +253,8 @@ const onCreatedRecord = (record: any) => {
)
message.success(msgVNode)
vModel.value = false
}
const linkedShortcuts = (e: KeyboardEvent) => {
@ -253,6 +285,10 @@ watch(childrenExcludedListPagination, () => {
onMounted(() => {
window.addEventListener('keydown', linkedShortcuts)
setTimeout(() => {
filterQueryRef.value?.focus()
}, 100)
})
onUnmounted(() => {
@ -268,154 +304,148 @@ const onFilterChange = () => {
</script>
<template>
<NcModal
v-model:visible="vModel"
:body-style="{ 'max-height': '640px', 'height': '85vh' }"
:class="{ active: vModel }"
:closable="false"
:footer="null"
:width="isForm ? 600 : 800"
wrap-class-name="nc-modal-link-record"
>
<LazyVirtualCellComponentsHeader
v-if="!isForm"
:display-value="headerDisplayValue"
:header="$t('activity.addNewLink')"
:related-table-title="relatedTableMeta?.title"
:relation="relation"
:table-title="meta?.title"
/>
<div class="flex mt-2 mb-2 items-center gap-2">
<div class="flex items-center border-1 p-1 rounded-md w-full border-gray-200 !focus-within:border-primary">
<MdiMagnify class="w-5 h-5 ml-2 text-gray-500" />
<a-input
ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query"
:bordered="false"
:placeholder="`${$t('general.searchIn')} ${relatedTableMeta?.title}`"
class="w-full !rounded-md nc-excluded-search xs:min-h-8"
size="small"
@change="onFilterChange"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
"
>
</a-input>
</div>
<div class="flex-1" />
<!-- Add new record -->
<NcButton
v-if="!isPublic"
v-e="['c:row-expand:open']"
:size="isMobileMode ? 'medium' : 'small'"
class="!text-brand-500"
type="secondary"
@click="addNewRecord"
>
<div class="flex items-center gap-1 px-4"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div>
</NcButton>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div ref="childrenExcludedListRef" class="overflow-scroll nc-scrollbar-md pr-1 cursor-pointer flex flex-col flex-grow">
<template v-if="isChildrenExcludedLoading">
<div
v-for="(_x, i) in Array.from({ length: 10 })"
:key="i"
class="!border-2 flex flex-row gap-2 mb-2 transition-all !rounded-xl relative !border-gray-200 hover:bg-gray-50"
<div class="nc-modal-link-record h-full w-full overflow-hidden" :class="{ active: vModel }" @keydown.enter.stop>
<div class="flex flex-col h-full">
<div class="nc-dropdown-link-record-header bg-gray-100 py-2 rounded-t-md flex justify-between pl-3 pr-2 gap-2">
<div class="flex-1 gap-2 flex items-center">
<button
v-if="!hideBackBtn"
class="!text-brand-500 hover:!text-brand-700 p-1.5 flex"
@click="emit('attachLinkedRecord')"
>
<a-skeleton-image class="h-24 w-24 !rounded-xl" />
<div class="flex flex-col m-[.5rem] gap-2 flex-grow justify-center">
<a-skeleton-input active class="!xs:w-30 !w-48 !rounded-xl" size="small" />
<div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
<GeneralIcon icon="ncArrowLeft" class="flex-none h-4 w-4" />
</button>
<div class="flex-1 nc-dropdown-link-record-search-wrapper flex items-center py-0.5 rounded-md">
<MdiMagnify class="nc-search-icon w-5 h-5" />
<a-input
ref="filterQueryRef"
v-model:value="childrenExcludedListPagination.query"
:bordered="false"
placeholder="Search records to link..."
class="w-full nc-excluded-search min-h-4"
size="small"
@change="onFilterChange"
@keydown.capture.stop="
(e) => {
if (e.key === 'Escape') {
filterQueryRef?.blur()
}
}
"
>
</a-input>
</div>
</div>
<LazyVirtualCellComponentsHeader
data-testid="nc-link-count-info"
:linked-records="totalItemsToShow"
:related-table-title="relatedTableMeta?.title"
:relation="relation"
:table-title="meta?.title"
/>
</div>
<div class="flex-1 overflow-auto nc-scrollbar-thin">
<template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div ref="childrenExcludedListRef">
<template v-if="isChildrenExcludedLoading">
<div
v-for="(_x, i) in Array.from({ length: 10 })"
:key="i"
class="flex flex-row gap-2 mb-2 transition-all relative !border-gray-200 hover:bg-gray-50"
>
<div class="flex items-center">
<a-skeleton-image class="h-14 w-14 !rounded-xl children:!h-full" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-4 !w-12" size="small" />
<a-skeleton-input active class="!xs:hidden !h-4 !w-24" size="small" />
<div class="flex flex-col gap-2 flex-grow justify-center">
<a-skeleton-input active class="h-3 !w-48 !rounded-xl" size="small" />
<div class="flex flex-row gap-6 w-10/12">
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
<div class="flex flex-col gap-0.5">
<a-skeleton-input active class="!h-2 !w-12" size="small" />
<a-skeleton-input active class="!h-2 !w-24" size="small" />
</div>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenExcludedList?.list ?? []"
:key="id"
:attachment="attachmentCol"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:fields="fields"
:is-linked="isChildrenExcludedListLinked[Number.parseInt(id)]"
:is-loading="isChildrenExcludedListLoading[Number.parseInt(id)]"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:row="refRow"
data-testid="nc-excluded-list-item"
@link-or-unlink="onClick(refRow, id)"
@expand="
() => {
expandedFormRow = refRow
expandedFormDlg = true
}
"
@keydown.space.prevent.stop="() => onClick(refRow, id)"
@keydown.enter.prevent.stop="() => onClick(refRow, id)"
/>
</template>
</div>
</template>
<template v-else>
<LazyVirtualCellComponentsListItem
v-for="(refRow, id) in childrenExcludedList?.list ?? []"
:key="id"
:attachment="attachmentCol"
:display-value-type-and-format-prop="displayValueTypeAndFormatProp"
:fields="fields"
:is-linked="isChildrenExcludedListLinked[Number.parseInt(id)]"
:is-loading="isChildrenExcludedListLoading[Number.parseInt(id)]"
:related-table-display-value-prop="relatedTableDisplayValueProp"
:row="refRow"
data-testid="nc-excluded-list-item"
@click="() => onClick(refRow, id)"
@expand="
() => {
expandedFormRow = refRow
expandedFormDlg = true
}
"
@keydown.space.prevent="() => onClick(refRow, id)"
@keydown.enter.prevent="() => onClick(refRow, id)"
/>
</template>
</div>
</template>
<div v-else class="my-auto py-2 flex flex-col gap-3 items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>
{{ $t('msg.thereAreNoRecordsInTable') }}
{{ relatedTableMeta?.title }}
</p>
</div>
<div v-if="isMobileMode" class="flex flex-row justify-center items-center w-full my-2">
<NcPagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
/>
</div>
<div class="mb-2 bg-gray-50 border-gray-50 border-b-2"></div>
<div class="flex flex-row justify-between items-center bg-white relative pt-1">
<div v-if="!isForm" class="flex items-center justify-center px-2 rounded-md text-gray-500 bg-brand-50 h-9.5">
{{ relation === 'bt' ? (row.row[relatedTableMeta?.title] ? '1' : 0) : childrenListCount ?? 'No' }}
{{ !isMobileMode ? $t('objects.records') : '' }} {{ !isMobileMode && childrenListCount !== 0 ? 'are' : '' }}
{{ $t('general.linked') }}
<div v-else class="h-full my-auto py-2 flex flex-col gap-3 items-center justify-center text-gray-500">
<InboxIcon class="w-16 h-16 mx-auto" />
<p>
{{ $t('msg.thereAreNoRecordsInTable') }}
{{ relatedTableMeta?.title }}
</p>
</div>
</div>
<div class="!xs:hidden flex absolute -mt-0.75 items-center py-2 justify-center w-full">
<NcPagination
v-if="childrenExcludedList?.pageInfo"
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
mode="simple"
/>
<div class="bg-gray-100 px-3 py-2 rounded-b-md flex items-center justify-between min-h-12">
<div class="flex">
<NcButton
v-if="!isPublic"
v-e="['c:row-expand:open']"
size="small"
class="!hover:(bg-white text-brand-500)"
type="secondary"
@click="addNewRecord"
>
<div class="flex items-center gap-1"><MdiPlus v-if="!isMobileMode" /> {{ $t('activity.newRecord') }}</div>
</NcButton>
</div>
<template
v-if="
childrenExcludedList?.pageInfo && +childrenExcludedList?.pageInfo?.totalRows > childrenExcludedListPagination.size
"
>
<div v-if="isMobileMode" class="flex items-center">
<NcPagination
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
/>
</div>
<div v-else class="flex items-center">
<NcPagination
v-model:current="childrenExcludedListPagination.page"
v-model:page-size="childrenExcludedListPagination.size"
:total="+childrenExcludedList?.pageInfo?.totalRows"
entity-name="links-excluded-list"
mode="simple"
/>
</div>
</template>
</div>
<NcButton class="nc-close-btn ml-auto" type="ghost" @click="vModel = false"> {{ $t('general.finish') }} </NcButton>
</div>
<Suspense>
<LazySmartsheetExpandedForm
@ -443,14 +473,29 @@ const onFilterChange = () => {
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState"
use-meta-fields
:skip-reload="true"
@created-record="onCreatedRecord"
/>
</Suspense>
</NcModal>
</div>
</template>
<style lang="scss" scoped>
:deep(.ant-skeleton-element .ant-skeleton-image) {
@apply !h-full;
}
</style>
<style lang="scss">
.nc-modal-link-record > .ant-modal > .ant-modal-content {
@apply !p-0;
.nc-dropdown-link-record-search-wrapper {
.nc-search-icon {
@apply flex-none text-gray-500;
}
&:focus-within {
.nc-search-icon {
@apply text-gray-600;
}
}
}
</style>

184
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -1,6 +1,10 @@
<script lang="ts" setup>
import { OrderedWorkspaceRoles, WorkspaceUserRoles, parseStringDateTime, timeAgo } from 'nocodb-sdk'
import { storeToRefs, useUserSorts, useWorkspace } from '#imports'
import { OrderedWorkspaceRoles, WorkspaceUserRoles } from 'nocodb-sdk'
import { IsAdminPanelInj, storeToRefs, useUserSorts, useWorkspace } from '#imports'
const props = defineProps<{
workspaceId?: string
}>()
const { workspaceRoles, loadRoles } = useRoles()
@ -8,12 +12,22 @@ const workspaceStore = useWorkspace()
const { removeCollaborator, updateCollaborator: _updateCollaborator } = workspaceStore
const { collaborators, workspaceRole } = storeToRefs(workspaceStore)
const { collaborators, activeWorkspace: _activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const currentWorkspace = computed(() => {
return props.workspaceId ? workspaces.value.get(props.workspaceId) : _activeWorkspace.value
})
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = useUserSorts('Workspace')
const userSearchText = ref('')
const isAdminPanel = inject(IsAdminPanelInj, ref(false))
const { isUIAllowed } = useRoles()
const inviteDlg = ref(false)
const filterCollaborators = computed(() => {
if (!userSearchText.value) return collaborators.value ?? []
@ -26,13 +40,34 @@ const filterCollaborators = computed(() => {
)
})
const selected = reactive<{
[key: number]: boolean
}>({})
const toggleSelectAll = (value: boolean) => {
filterCollaborators.value.forEach((_, i) => {
selected[i] = value
})
}
const sortedCollaborators = computed(() => {
return handleGetSortedData(filterCollaborators.value, sorts.value)
})
const selectAll = computed({
get: () =>
Object.values(selected).every((v) => v) &&
Object.keys(selected).length > 0 &&
Object.values(selected).length === sortedCollaborators.value.length,
set: (value) => {
toggleSelectAll(value)
},
})
const updateCollaborator = async (collab: any, roles: WorkspaceUserRoles) => {
try {
await _updateCollaborator(collab.id, roles)
console.log()
await _updateCollaborator(collab.id, roles, currentWorkspace.value.id)
message.success('Successfully updated user role')
collaborators.value?.forEach((collaborator) => {
@ -54,81 +89,89 @@ const accessibleRoles = computed<WorkspaceUserRoles[]>(() => {
})
onMounted(async () => {
await loadRoles()
await loadRoles(null, {}, currentWorkspace.value?.id)
loadSorts()
})
</script>
<template>
<div class="nc-collaborator-table-container mt-4 mx-6 h-[calc(100vh-12rem)]">
<div class="w-full flex justify-between items-baseline mt-6.5 mb-2 pr-0.25 ml-2">
<div class="text-xl">Invite Members By Email</div>
<DlgInviteDlg v-model:model-value="inviteDlg" :workspace-id="currentWorkspace.id" type="workspace" />
<div class="nc-collaborator-table-container mt-4 h-[calc(100vh-10rem)]">
<div class="w-full flex justify-between mt-6.5 mb-2">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md mr-4" placeholder="Search members">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<NcButton data-testid="nc-add-member-btn" @click="inviteDlg = true">
<div class="flex items-center gap-2">
<component :is="iconMap.plus" class="!h-4 !w-4" />
{{ $t('labels.addMember') }}
</div>
</NcButton>
</div>
<WorkspaceInviteSection v-if="workspaceRole !== WorkspaceUserRoles.VIEWER" />
<div v-if="!filterCollaborators?.length" class="w-full h-full flex flex-col items-center justify-center mt-36">
<a-empty description="No members found" />
</div>
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center">
<div class="text-gray-700 users-email-grid w-3/8 ml-10 mr-3 flex items-center space-x-2">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-4rem)]">
<div class="flex flex-row bg-gray-50 min-h-11 items-center border-b-1">
<div class="py-3 px-6"><NcCheckbox v-model:checked="selectAll" /></div>
<div class="text-gray-700 w-[30rem] users-email-grid flex items-center space-x-2">
<span>
{{ $t('objects.users') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.email" field="email" :handle-user-sort="saveOrUpdate" />
<LazyAccountUserMenu :direction="sortDirection.email" :handle-user-sort="saveOrUpdate" field="email" />
</div>
<div class="text-gray-700 user-access-grid w-2/8 mr-3 flex items-center space-x-2">
<div class="text-gray-700 w-full flex-1 px-6 py-3 flex items-center space-x-2">
<span>
{{ $t('general.access') }}
</span>
<LazyAccountUserMenu :direction="sortDirection.roles" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
<div class="text-gray-700 date-joined-grid w-2/8 mr-3">{{ $t('title.dateJoined') }}</div>
<div class="text-gray-700 user-access-grid w-1/8">Actions</div>
<div class="text-gray-700 w-full flex-1 px-6 py-3">{{ $t('title.dateJoined') }}</div>
<div class="text-gray-700 w-full text-right flex-1 px-6 py-3">{{ $t('labels.actions') }}</div>
</div>
<div class="flex flex-col nc-scrollbar-md">
<div
v-for="(collab, i) of sortedCollaborators"
:key="i"
class="flex flex-row border-b-1 py-1 min-h-14 items-center justify-around last"
class="user-row flex hover:bg-gray-50 flex-row last:border-b-0 border-b-1 py-1 min-h-14 items-center"
>
<div class="flex gap-3 items-center users-email-grid w-3/8 ml-10">
<GeneralUserIcon size="base" :name="collab.email" :email="collab.email" />
<NcTooltip v-if="collab.display_name">
<template #title>
<div class="py-3 px-6">
<NcCheckbox v-model:checked="selected[i]" />
</div>
<div class="flex gap-3 w-[30rem] items-center users-email-grid">
<GeneralUserIcon :email="collab.email" size="base" />
<div class="flex flex-col">
<div class="flex gap-3">
<span class="text-gray-800 capitalize font-semibold">
{{ collab.display_name || collab.email.slice(0, collab.email.indexOf('@')) }}
</span>
</div>
<span class="text-xs text-gray-600">
{{ collab.email }}
</template>
<span class="truncate">
{{ collab.display_name }}
</span>
</NcTooltip>
<span v-else class="truncate">
{{ collab.email }}
</span>
</div>
</div>
<div class="user-access-grid w-2/8">
<template v-if="accessibleRoles.includes(collab.roles)">
<div class="w-[30px]">
<div class="w-full flex-1 px-6 py-3">
<div class="w-[30px]">
<template v-if="accessibleRoles.includes(collab.roles)">
<RolesSelector
:description="false"
:on-role-change="(role) => updateCollaborator(collab, role)"
:role="collab.roles"
:roles="accessibleRoles"
:description="false"
class="cursor-pointer"
:on-role-change="(role) => updateCollaborator(collab, role)"
/>
</div>
</template>
<template v-else>
<RolesBadge :role="collab.roles" class="cursor-default" />
</template>
</template>
<template v-else>
<RolesBadge :border="false" :role="collab.roles" class="cursor-default" />
</template>
</div>
</div>
<div class="date-joined-grid w-2/8 flex justify-start">
<div class="w-full flex-1 px-6 py-3">
<NcTooltip class="max-w-full">
<template #title>
{{ parseStringDateTime(collab.created_at) }}
@ -138,14 +181,35 @@ onMounted(async () => {
</span>
</NcTooltip>
</div>
<div class="w-1/8 pl-6">
<NcDropdown v-if="collab.roles !== WorkspaceUserRoles.OWNER" :trigger="['click']">
<MdiDotsVertical
class="border-1 !text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
/>
<div class="w-full justify-end flex-1 flex px-6 py-3">
<NcDropdown v-if="collab.roles !== WorkspaceUserRoles.OWNER">
<NcButton size="small" type="secondary">
<component :is="iconMap.threeDotVertical" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click="removeCollaborator(collab.id)">
<template v-if="isAdminPanel">
<NcMenuItem data-testid="nc-admin-org-user-delete">
<GeneralIcon class="text-gray-800" icon="signout" />
<span>{{ $t('labels.signOutUser') }}</span>
</NcMenuItem>
<a-menu-divider class="my-1.5" />
</template>
<NcMenuItem
v-if="isUIAllowed('transferWorkspaceOwnership')"
data-testid="nc-admin-org-user-assign-admin"
@click="updateCollaborator(collab, WorkspaceUserRoles.OWNER)"
>
<GeneralIcon class="text-gray-800" icon="user" />
<span>{{ $t('labels.assignAs') }}</span>
<RolesBadge :border="false" :show-icon="false" role="owner" />
</NcMenuItem>
<NcMenuItem
class="!text-red-500 !hover:bg-red-50"
@click="removeCollaborator(collab.id, currentWorkspace.id)"
>
<MaterialSymbolsDeleteOutlineRounded />
Remove user
</NcMenuItem>
@ -154,15 +218,15 @@ onMounted(async () => {
</NcDropdown>
</div>
</div>
<div v-if="sortedCollaborators.length === 1" class="pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center">
<div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img src="~assets/img/placeholder/invite-team.png" class="!w-[30rem] flex-none" />
</div>
<div v-if="sortedCollaborators.length === 1" class="pt-12 pb-4 px-2 flex flex-col items-center gap-6 text-center">
<div class="text-2xl text-gray-800 font-bold">
{{ $t('placeholder.inviteYourTeam') }}
</div>
<div class="text-sm text-gray-700">
{{ $t('placeholder.inviteYourTeamLabel') }}
</div>
<img alt="Invite Team" class="!w-[30rem] flex-none" src="~assets/img/placeholder/invite-team.png" />
</div>
</div>
</div>
@ -170,6 +234,18 @@ onMounted(async () => {
</template>
<style scoped lang="scss">
.ant-input::placeholder {
@apply text-gray-500;
}
.ant-input:placeholder-shown {
@apply text-gray-500 !text-md;
}
.ant-input-affix-wrapper {
@apply px-4 rounded-lg py-2 w-84 border-1 focus:border-brand-500 border-gray-200 !ring-0;
}
.badge-text {
@apply text-[14px] pt-1 text-center;
}

40
packages/nc-gui/components/workspace/Settings.vue

@ -1,13 +1,17 @@
<script lang="ts" setup>
import { ref, storeToRefs, useGlobal, useI18n, useWorkspace, watch } from '#imports'
const props = defineProps<{
workspaceId?: string
}>()
const { signOut } = useGlobal()
const { t } = useI18n()
const { deleteWorkspace, navigateToWorkspace, updateWorkspace } = useWorkspace()
const { workspacesList, activeWorkspaceId, activeWorkspace, workspaces } = storeToRefs(useWorkspace())
const { workspacesList, activeWorkspace, workspaces } = storeToRefs(useWorkspace())
const formValidator = ref()
@ -33,19 +37,29 @@ const formRules = {
],
}
const currentWorkspace = computed(() => {
return props.workspaceId ? workspaces.value.get(props.workspaceId) : activeWorkspace.value
})
const onDelete = async () => {
isDeleting.value = true
try {
await deleteWorkspace(activeWorkspaceId.value, { skipStateUpdate: true })
await deleteWorkspace(currentWorkspace.value.id, { skipStateUpdate: true })
isConfirmed.value = false
isDeleting.value = false
// We only remove the delete workspace from the list after the api call is successful
workspaces.value.delete(activeWorkspaceId.value)
workspaces.value.delete(currentWorkspace.value.id)
if (workspacesList.value.length > 1) {
await navigateToWorkspace(workspacesList.value[0].id)
// WorkspaceId is provided from the admin Panel. If deleted navigate to the workspace list page
if (!props.workspaceId) {
await navigateToWorkspace(workspacesList.value[0].id)
} else {
// #TODO: @DarkPhoenix2704
// Navigate BackPage
}
} else {
// As signin page will clear the workspaces, we need to check if there are more than one workspace
await signOut(false)
@ -69,7 +83,7 @@ const titleChange = async () => {
isErrored.value = false
try {
await updateWorkspace(activeWorkspaceId.value, {
await updateWorkspace(currentWorkspace.value.id, {
title: form.value.title,
})
} catch (e: any) {
@ -81,9 +95,9 @@ const titleChange = async () => {
}
watch(
() => activeWorkspace.value.title,
() => currentWorkspace.value.id,
() => {
form.value.title = activeWorkspace.value.title
form.value.title = currentWorkspace.value.title
},
{
immediate: true,
@ -94,11 +108,7 @@ watch(
() => form.value.title,
async () => {
try {
if (form.value.title !== activeWorkspace.value?.title) {
isCancelButtonVisible.value = true
} else {
isCancelButtonVisible.value = false
}
isCancelButtonVisible.value = form.value.title !== currentWorkspace.value?.title
isErrored.value = !(await formValidator.value.validate())
} catch (e: any) {
isErrored.value = true
@ -107,7 +117,7 @@ watch(
)
const onCancel = () => {
form.value.title = activeWorkspace.value?.title
form.value.title = currentWorkspace.value?.title
}
</script>
@ -140,7 +150,7 @@ const onCancel = () => {
v-e="['c:workspace:settings:rename']"
type="primary"
html-type="submit"
:disabled="isErrored || (form.title && form.title === activeWorkspace.title)"
:disabled="isErrored || (form.title && form.title === currentWorkspace.title)"
:loading="isDeleting"
data-testid="nc-workspace-settings-settings-rename-submit"
>
@ -175,7 +185,7 @@ const onCancel = () => {
</template>
<style lang="scss" scoped>
.item {
.item-card {
@apply p-6 rounded-2xl border-1 max-w-180 mt-10 min-w-100 w-full;
}
</style>

91
packages/nc-gui/components/workspace/View.vue

@ -1,5 +1,10 @@
<script lang="ts" setup>
import { useTitle } from '@vueuse/core'
import { storeToRefs } from '#imports'
const props = defineProps<{
workspaceId?: string
}>()
const router = useRouter()
const route = router.currentRoute
@ -7,21 +12,38 @@ const route = router.currentRoute
const { isUIAllowed } = useRoles()
const workspaceStore = useWorkspace()
const { activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { loadCollaborators } = workspaceStore
const { activeWorkspace: _activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { loadCollaborators, loadWorkspace } = workspaceStore
const orgStore = useOrg()
const { orgId } = storeToRefs(orgStore)
const currentWorkspace = computedAsync(async () => {
let ws
if (props.workspaceId) {
ws = workspaces.value.get(props.workspaceId)
if (!ws) {
await loadWorkspace(props.workspaceId)
ws = workspaces.value.get(props.workspaceId)
}
} else {
ws = _activeWorkspace.value
}
return ws
})
const tab = computed({
get() {
return route.value.query?.tab ?? 'collaborators'
},
set(tab: string) {
if (tab === 'collaborators') loadCollaborators()
if (tab === 'collaborators') loadCollaborators({} as any, props.workspaceId)
router.push({ query: { ...route.value.query, tab } })
},
})
watch(
() => activeWorkspace.value?.title,
() => currentWorkspace.value?.title,
(title: string) => {
if (!title) return
@ -35,26 +57,40 @@ watch(
)
onMounted(() => {
until(() => activeWorkspace.value?.id)
until(() => currentWorkspace.value?.id)
.toMatch((v) => !!v)
.then(() => {
until(() => workspaces.value)
.toMatch((v) => v.has(activeWorkspace.value.id))
.then(() => {
loadCollaborators()
})
.then(async () => {
await loadCollaborators({} as any, currentWorkspace.value.id)
})
})
</script>
<template>
<div v-if="activeWorkspace" class="flex flex-col nc-workspace-settings">
<div class="flex gap-2 items-center min-w-0 p-6">
<GeneralWorkspaceIcon :workspace="activeWorkspace" />
<h1 class="text-3xl font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize">
{{ activeWorkspace?.title }}
<div v-if="currentWorkspace" class="flex w-full px-6 max-w-[97.5rem] flex-col nc-workspace-settings">
<div v-if="!props.workspaceId" class="flex gap-2 items-center min-w-0 py-6">
<GeneralWorkspaceIcon :workspace="currentWorkspace" />
<h1 class="text-3xl capitalize font-weight-bold tracking-[0.5px] mb-0 nc-workspace-title truncate min-w-10 capitalize">
{{ currentWorkspace?.title }}
</h1>
</div>
<div v-else>
<div class="font-bold w-full !mb-5 text-2xl" data-rec="true">
<div class="flex items-center gap-3">
<NuxtLink
:href="`/admin/${orgId}/workspaces`"
class="!hover:(text-black underline-gray-600) !text-black !underline-transparent ml-0.75 max-w-1/4"
>
{{ $t('labels.workspaces') }}
</NuxtLink>
<span class="text-2xl"> / </span>
<GeneralWorkspaceIcon :workspace="currentWorkspace" hide-label />
<span class="text-base capitalize">
{{ currentWorkspace?.title }}
</span>
</div>
</div>
</div>
<NcTabs v-model:activeKey="tab">
<template v-if="isUIAllowed('workspaceSettings')">
@ -65,7 +101,7 @@ onMounted(() => {
Members
</div>
</template>
<WorkspaceCollaboratorsList />
<WorkspaceCollaboratorsList :workspace-id="currentWorkspace.id" />
</a-tab-pane>
</template>
@ -77,7 +113,7 @@ onMounted(() => {
Settings
</div>
</template>
<WorkspaceSettings />
<WorkspaceSettings :workspace-id="currentWorkspace.id" />
</a-tab-pane>
</template>
</NcTabs>
@ -90,7 +126,24 @@ onMounted(() => {
font-size: 0.7rem;
}
.tab {
@apply flex flex-row items-center gap-x-2;
}
:deep(.ant-tabs-nav) {
@apply !pl-0;
}
:deep(.ant-tabs-nav-list) {
@apply !ml-3;
@apply !gap-5;
}
:deep(.ant-tabs-tab) {
@apply !pt-0 !pb-2.5 !ml-0;
}
.ant-tabs-content {
@apply !h-full;
}
.ant-tabs-content-top {
@apply !h-full;
}
</style>

3
packages/nc-gui/composables/useCalendarViewStore.ts

@ -738,7 +738,8 @@ const [useProvideCalendarViewStore, useCalendarViewStore] = useInjectionState(
watch(activeCalendarView, async (value, oldValue) => {
if (oldValue === 'week') {
pageDate.value = selectedDate.value
selectedMonth.value = selectedDate.value ?? selectedDateRange.value.start
selectedMonth.value = selectedTime.value ?? selectedDate.value ?? selectedDateRange.value.start
selectedDate.value = selectedTime.value ?? selectedDateRange.value.start
selectedTime.value = selectedDate.value ?? selectedDateRange.value.start
} else if (oldValue === 'month') {
selectedDate.value = selectedMonth.value

66
packages/nc-gui/composables/useData.ts

@ -477,8 +477,8 @@ export function useData(args: {
try {
await $api.dbTableRow.nestedAdd(
NOCO,
base.value.title as string,
metaValue?.title as string,
base.value.id as string,
metaValue?.id as string,
encodeURIComponent(rowId),
type as RelationTypes,
column.title as string,
@ -630,23 +630,25 @@ export function useData(args: {
async function deleteSelectedRows() {
let row = formattedData.value.length
let removedRowsData: Record<string, any>[] = []
const removedRowsData: Record<string, any>[] = []
let compositePrimaryKey = ''
while (row--) {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any>
const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.selected) {
continue
}
if (!rowMeta.new) {
const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowObj, meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[])
const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])
if (extractedPk && compositePkValue) {
if (!compositePrimaryKey) compositePrimaryKey = extractedPk
removedRowsData.push({
[compositePrimaryKey]: compositePkValue as string,
pkData,
row: clone(formattedData.value[row]) as Row,
rowIndex: row as number,
})
@ -670,20 +672,7 @@ export function useData(args: {
rowObj.row = clone(fullRecord)
}
const removedRowIds: Record<string, any>[] = await bulkDeleteRows(
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) {
const removedRowsDataSet = new Set(removedRowIds.map((row) => row[compositePrimaryKey]))
removedRowsData = removedRowsData.filter((row) => removedRowsDataSet.has(row[compositePrimaryKey] as string))
const rowIndexesSet = new Set(removedRowsData.map((row) => row.rowIndex))
formattedData.value = formattedData.value.filter((_, index) => rowIndexesSet.has(index))
} else {
removedRowsData = []
}
await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
} catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
@ -692,10 +681,8 @@ export function useData(args: {
addUndo({
redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[], compositePrimaryKey: string) {
const removedRowIds = await bulkDeleteRows(
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) {
const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
if (Array.isArray(removedRowIds)) {
for (const { row } of removedRowsData) {
@ -708,7 +695,7 @@ export function useData(args: {
await callbacks?.syncPagination?.()
},
args: [removedRowsData, compositePrimaryKey],
args: [removedRowsData],
},
undo: {
fn: async function undo(
@ -764,22 +751,24 @@ export function useData(args: {
// plus one because we want to include the end row
let row = start + 1
let removedRowsData: Record<string, any>[] = []
const removedRowsData: Record<string, any>[] = []
let compositePrimaryKey = ''
while (row--) {
try {
const { row: rowObj, rowMeta } = formattedData.value[row] as Record<string, any>
const { row: rowData, rowMeta } = formattedData.value[row] as Record<string, any>
if (!rowMeta.new) {
const extractedPk = extractPk(meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowObj, meta?.value?.columns as ColumnType[])
const compositePkValue = extractPkFromRow(rowData, meta?.value?.columns as ColumnType[])
const pkData = rowPkData(rowData, meta?.value?.columns as ColumnType[])
if (extractedPk && compositePkValue) {
if (!compositePrimaryKey) compositePrimaryKey = extractedPk
removedRowsData.push({
[compositePrimaryKey]: compositePkValue as string,
pkData,
row: clone(formattedData.value[row]) as Row,
rowIndex: row as number,
})
@ -808,20 +797,7 @@ export function useData(args: {
rowObj.row = clone(fullRecord)
}
const removedRowIds: Record<string, any>[] = await bulkDeleteRows(
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
if (Array.isArray(removedRowIds)) {
const removedRowsDataSet = new Set(removedRowIds.map((row) => row[compositePrimaryKey]))
removedRowsData = removedRowsData.filter((row) => removedRowsDataSet.has(row[compositePrimaryKey] as string))
const rowIndexesSet = new Set(removedRowsData.map((row) => row.rowIndex))
formattedData.value = formattedData.value.filter((_, index) => rowIndexesSet.has(index))
} else {
removedRowsData = []
}
await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
} catch (e: any) {
return message.error(`${t('msg.error.deleteRowFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
@ -830,10 +806,8 @@ export function useData(args: {
addUndo({
redo: {
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[], compositePrimaryKey: string) {
const removedRowIds = await bulkDeleteRows(
removedRowsData.map((row) => ({ [compositePrimaryKey]: row[compositePrimaryKey] as string })),
)
fn: async function redo(this: UndoRedoAction, removedRowsData: Record<string, any>[]) {
const removedRowIds = await bulkDeleteRows(removedRowsData.map((row) => row.pkData))
if (Array.isArray(removedRowIds)) {
for (const { row } of removedRowsData) {
@ -846,7 +820,7 @@ export function useData(args: {
await callbacks?.syncPagination?.()
},
args: [removedRowsData, compositePrimaryKey],
args: [removedRowsData],
},
undo: {
fn: async function undo(

2
packages/nc-gui/composables/useExpandedFormDetached/index.ts

@ -19,6 +19,8 @@ const [setup, use] = useInjectionState(() => {
return ref<UseExpandedFormDetachedProps[]>([])
})
export { setup as useExpandedFormDetachedProvider }
export function useExpandedFormDetached() {
let states = use()!

6
packages/nc-gui/composables/useExpandedFormStore.ts

@ -56,7 +56,9 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
: ({ row: {}, oldRow: {}, rowMeta: {} } as Row),
)
const rowStore = useProvideSmartsheetRowStore(meta, row)
row.value.rowMeta.fromExpandedForm = true
const rowStore = useProvideSmartsheetRowStore(row)
const activeView = inject(ActiveViewInj, ref())
@ -304,6 +306,8 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
const loadRow = async (rowId?: string, onlyVirtual = false) => {
if (row.value.rowMeta.new) return
if (isPublic.value || !meta.value?.id) return
let record = await $api.dbTableRow.read(
NOCO,

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

@ -12,9 +12,11 @@ import {
IsPublicInj,
Modal,
NOCO,
NcErrorType,
SharedViewPasswordInj,
computed,
extractSdkResponseErrorMsg,
extractSdkResponseErrorMsgv2,
inject,
message,
parseProp,
@ -188,15 +190,16 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
return row.value.row[displayValueProp.value]
})
const loadChildrenExcludedList = async (activeState?: any) => {
const loadChildrenExcludedList = async (activeState?: any, resetOffset: boolean = false) => {
if (activeState) newRowState.state = activeState
try {
let offset =
childrenExcludedListPagination.size * (childrenExcludedListPagination.page - 1) - childrenExcludedOffsetCount.value
if (offset < 0) {
if (offset < 0 || resetOffset) {
offset = 0
childrenExcludedOffsetCount.value = 0
childrenExcludedListPagination.page = 1
}
isChildrenExcludedLoading.value = true
if (isPublic.value) {
@ -266,7 +269,11 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
// Mark out exact same objects in activeState[column.value.title] as Linked
// compare all keys and values
childrenExcludedList.value.list.forEach((row: any, index: number) => {
const found = activeState[column.value.title].find((a: any) => {
const found = (
[RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)
? [activeState[column.value.title]]
: activeState[column.value.title]
).find((a: any) => {
let isSame = true
for (const key in a) {
@ -284,27 +291,29 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}
} catch (e: any) {
// temporary fix to handle when offset is beyond limit
if ((await extractSdkResponseErrorMsg(e)) === 'Offset is beyond the total number of records') {
const error = await extractSdkResponseErrorMsgv2(e)
if (error.error === NcErrorType.INVALID_OFFSET_VALUE) {
childrenExcludedListPagination.page = 0
return loadChildrenExcludedList(activeState)
return loadChildrenExcludedList(activeState, true)
}
message.error(`${t('msg.error.failedToLoadList')}: ${await extractSdkResponseErrorMsg(e)}`)
message.error(`${t('msg.error.failedToLoadList')}: ${error.message}`)
} finally {
isChildrenExcludedLoading.value = false
}
}
const loadChildrenList = async () => {
const loadChildrenList = async (resetOffset: boolean = false) => {
try {
isChildrenLoading.value = true
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes(colOptions.value.type)) return
if (!rowId.value || !column.value) return
let offset = childrenListPagination.size * (childrenListPagination.page - 1) + childrenListOffsetCount.value
if (offset < 0) {
if (offset < 0 || resetOffset) {
offset = 0
childrenListOffsetCount.value = 0
childrenListPagination.page = 1
} else if (offset >= childrenListCount.value) {
offset = 0
}
@ -347,6 +356,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
isChildrenListLinked.value[index] = true
isChildrenListLoading.value[index] = false
})
if (!childrenListPagination.query) {
childrenListCount.value = childrenList.value?.pageInfo.totalRows ?? 0
}

4
packages/nc-gui/composables/useMetas.ts

@ -3,7 +3,7 @@ import type { WatchStopHandle } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { extractSdkResponseErrorMsg, storeToRefs, useBase, useNuxtApp, useState, watch } from '#imports'
export function useMetas() {
export const useMetas = createSharedComposable(() => {
const { $api } = useNuxtApp()
const { tables: _tables } = storeToRefs(useBase())
@ -118,4 +118,4 @@ export function useMetas() {
}
return { getMeta, clearAllMeta, metas, metasWithIdAsKey, removeMeta, setMeta }
}
})

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

@ -300,7 +300,10 @@ export function useMultiSelect(
const blobHTML = new Blob([copyHTML], { type: 'text/html' })
const blobPlainText = new Blob([copyPlainText], { type: 'text/plain' })
return navigator.clipboard.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })])
return (
navigator.clipboard?.write([new ClipboardItem({ [blobHTML.type]: blobHTML, [blobPlainText.type]: blobPlainText })]) ??
copy(copyPlainText)
)
}
async function copyValue(ctx?: Cell) {
@ -332,25 +335,43 @@ export function useMultiSelect(
}
}
function isCellSelected(row: number, col: number) {
if (activeCell.col === col && activeCell.row === row) {
return true
const fillRangeMap = computed(() => {
/*
`${rowIndex}-${colIndex}`: true | false
*/
const map: Record<string, boolean> = {}
if (fillRange._start === null || fillRange._end === null) {
return map
}
return selectedRange.isCellInRange({ row, col })
}
for (let row = fillRange.start.row; row <= fillRange.end.row; row++) {
for (let col = fillRange.start.col; col <= fillRange.end.col; col++) {
map[`${row}-${col}`] = true
}
}
function isCellInFillRange(row: number, col: number) {
if (fillRange._start === null || fillRange._end === null) {
return false
return map
})
const selectRangeMap = computed(() => {
/*
`${rowIndex}-${colIndex}`: true | false
*/
const map: Record<string, boolean> = {}
if (selectedRange._start === null || selectedRange._end === null) {
return map
}
if (selectedRange.isCellInRange({ row, col })) {
return false
for (let row = selectedRange.start.row; row <= selectedRange.end.row; row++) {
for (let col = selectedRange.start.col; col <= selectedRange.end.col; col++) {
map[`${row}-${col}`] = true
}
}
return fillRange.isCellInRange({ row, col })
}
return map
})
const isPasteable = (row?: Row, col?: ColumnType, showInfo = false) => {
if (!row || !col) {
@ -417,7 +438,7 @@ export function useMultiSelect(
// if there was a right click on selected range, don't restart the selection
if (
(event?.button !== MAIN_MOUSE_PRESSED || (event?.button === MAIN_MOUSE_PRESSED && event.ctrlKey)) &&
isCellSelected(row, col)
selectRangeMap.value[`${row}-${col}`]
) {
return
}
@ -486,7 +507,7 @@ export function useMultiSelect(
fillDirection === 1 ? row <= fillRange._end.row : row >= fillRange._end.row;
row += fillDirection
) {
if (isCellSelected(row, selectedRange.start.col)) {
if (selectRangeMap.value[`${row}-${selectedRange.start.col}`]) {
continue
}
@ -1295,14 +1316,14 @@ export function useMultiSelect(
handleMouseOver,
clearSelectedRange,
copyValue,
isCellSelected,
activeCell,
handleCellClick,
resetSelectedRange,
selectedRange,
makeActive,
isCellInFillRange,
isMouseDown,
isFillMode,
selectRangeMap,
fillRangeMap,
}
}

23
packages/nc-gui/composables/useOrganization.ts

@ -0,0 +1,23 @@
export const useOrganization = () => {
const workspaces = ref([])
const members = ref([])
const bases = ref([])
const { orgId } = storeToRefs(useOrg())
const listWorkspaces = async (..._args: any) => {}
const fetchOrganizationMembers = async (..._args: any) => {}
const fetchOrganizationBases = async (..._args: any) => {}
return {
orgId,
workspaces,
listWorkspaces,
fetchOrganizationMembers,
fetchOrganizationBases,
bases,
members,
}
}

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

@ -1,7 +1,6 @@
import useVuelidate from '@vuelidate/core'
import { helpers, minLength, required, sameAs } from '@vuelidate/validators'
import dayjs from 'dayjs'
import type { Ref } from 'vue'
import type {
BoolType,
ColumnType,
@ -91,8 +90,9 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
const preFilledDefaultValueformState = ref<Record<string, any>>({})
useProvideSmartsheetLtarHelpers(meta)
const { state: additionalState } = useProvideSmartsheetRowStore(
meta as Ref<TableType>,
ref({
row: formState,
rowMeta: { new: true },
@ -217,7 +217,7 @@ const [useProvideSharedFormStore, useSharedFormStore] = useInjectionState((share
((column.rqd && !column.cdf) || (column.pk && !(column.ai || column.cdf)) || column.required)
) {
obj.localState[column.title!] = {
required: fieldRequired(undefined, column.uidt === UITypes.Checkbox && column.required ? true : false),
required: fieldRequired(undefined, !!(column.uidt === UITypes.Checkbox && column.required)),
}
} else if (
isLinksOrLTAR(column) &&

249
packages/nc-gui/composables/useSmartsheetLtarHelpers.ts

@ -0,0 +1,249 @@
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import {
NOCO,
deepCompare,
extractPkFromRow,
extractSdkResponseErrorMsg,
isBt,
isHm,
isMm,
isOo,
message,
storeToRefs,
unref,
useBase,
useI18n,
useInjectionState,
useMetas,
useNuxtApp,
} from '#imports'
import type { Row } from '#imports'
const [useProvideSmartsheetLtarHelpers, useSmartsheetLtarHelpers] = useInjectionState(
(meta: Ref<TableType | undefined> | ComputedRef<TableType | undefined>) => {
const { $api } = useNuxtApp()
const { t } = useI18n()
const { base } = storeToRefs(useBase())
const { metas } = useMetas()
const getRowLtarHelpers = (row: Row) => {
if (!row.rowMeta) {
row.rowMeta = {}
}
if (!row.rowMeta.ltarState) {
row.rowMeta.ltarState = {}
}
return row.rowMeta.ltarState
}
// actions
const addLTARRef = async (row: Row, value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
if (!getRowLtarHelpers(row)[column.title!]) getRowLtarHelpers(row)[column.title!] = []
if (getRowLtarHelpers(row)[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
// This value is already in the list
return message.info(t('msg.info.valueAlreadyInList'))
}
if (Array.isArray(value)) {
getRowLtarHelpers(row)[column.title!]!.push(...value)
} else {
getRowLtarHelpers(row)[column.title!]!.push(value)
}
} else if (isBt(column) || isOo(column)) {
getRowLtarHelpers(row)[column.title!] = value
}
}
// actions
const removeLTARRef = async (row: Row, value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
getRowLtarHelpers(row)[column.title!]?.splice(getRowLtarHelpers(row)[column.title!]?.indexOf(value), 1)
} else if (isBt(column) || isOo(column)) {
getRowLtarHelpers(row)[column.title!] = null
}
}
const linkRecord = async (
rowId: string,
relatedRowId: string,
column: ColumnType,
type: RelationTypes,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
) => {
try {
await $api.dbTableRow.nestedAdd(
NOCO,
base.value.id as string,
metaValue?.id as string,
encodeURIComponent(rowId),
type,
column.id as string,
encodeURIComponent(relatedRowId),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
/** sync LTAR relations kept in local state */
const syncLTARRefs = async (
row: Row,
rowData: Record<string, any>,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
) => {
const id = extractPkFromRow(rowData, metaValue?.columns as ColumnType[])
for (const column of metaValue?.columns ?? []) {
if (!isLinksOrLTAR(column)) continue
const colOptions = column.colOptions as LinkToAnotherRecordType
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string]
if (isHm(column) || isMm(column)) {
const relatedRows = (getRowLtarHelpers(row)?.[column.title!] ?? []) as Record<string, any>[]
for (const relatedRow of relatedRows) {
await linkRecord(
id,
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
} else if ((isBt(column) || isOo(column)) && getRowLtarHelpers(row)?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(
getRowLtarHelpers(row)?.[column.title!] as Record<string, any>,
relatedTableMeta.columns as ColumnType[],
),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
// clear LTAR refs after sync
getRowLtarHelpers(row)[column.title!] = null
}
}
// clear LTAR cell
const clearLTARCell = async (row: Row, column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
if (row.rowMeta.new) {
getRowLtarHelpers(row)[column.title!] = null
} else {
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((<LinkToAnotherRecordType>column.colOptions)?.type)) {
if (!row.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
extractPkFromRow(row.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column.colOptions)?.type as any,
column.id as string,
extractPkFromRow(row.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)
row.row[column.title!] = null
} else {
for (const link of (row.row[column.title!] as Record<string, any>[]) || []) {
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
encodeURIComponent(extractPkFromRow(row.row, meta.value?.columns as ColumnType[])),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string,
encodeURIComponent(extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[])),
)
}
row.row[column.title!] = []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const loadRow = async (row: Row) => {
const record = await $api.dbTableRow.read(
NOCO,
base.value?.id as string,
meta.value?.title as string,
encodeURIComponent(extractPkFromRow(row.row, meta.value?.columns as ColumnType[])),
)
Object.assign(unref(row), {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
// clear MM cell
const cleaMMCell = async (row: Row, column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
if (row.rowMeta.new) {
getRowLtarHelpers(row)[column.title!] = null
} else {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.MANY_TO_MANY) {
if (!row.row[column.title!]) return
const result = await $api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
meta.value?.id as string,
column.id as string,
[
{
operation: 'deleteAll',
rowId: extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) as string,
columnId: column.id as string,
fk_related_model_id: (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
},
],
)
row.row[column.title!] = null
return Array.isArray(result.unlink) ? result.unlink : []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
return {
addLTARRef,
removeLTARRef,
syncLTARRefs,
loadRow,
clearLTARCell,
cleaMMCell,
}
},
'smartsheet-ltar-helpers',
)
export { useProvideSmartsheetLtarHelpers }
export function useSmartsheetLtarHelpersOrThrow() {
const smartsheetLtarHelpers = useSmartsheetLtarHelpers()
if (smartsheetLtarHelpers == null) throw new Error('Please call `useSmartsheetLtarHelpers` on the appropriate parent component')
return smartsheetLtarHelpers
}

266
packages/nc-gui/composables/useSmartsheetRowStore.ts

@ -1,242 +1,42 @@
import { RelationTypes, isLinksOrLTAR } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import type { MaybeRef } from '@vueuse/core'
import {
NOCO,
computed,
deepCompare,
extractPkFromRow,
extractSdkResponseErrorMsg,
isBt,
isHm,
isMm,
isOo,
message,
ref,
storeToRefs,
unref,
useBase,
useI18n,
useInjectionState,
useMetas,
useNuxtApp,
} from '#imports'
import { computed, ref, unref, useInjectionState } from '#imports'
import type { Row } from '#imports'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
(meta: Ref<TableType | undefined>, row: MaybeRef<Row>) => {
const { $api } = useNuxtApp()
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((row: MaybeRef<Row>) => {
const currentRow = ref(row)
const { t } = useI18n()
const { base } = storeToRefs(useBase())
const { metas } = useMetas()
const currentRow = ref(row)
// state
const state = ref<Record<string, Record<string, any> | Record<string, any>[] | null>>({})
// getters
const isNew = computed(() => unref(row).rowMeta.new ?? false)
// actions
const addLTARRef = async (value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
if (!state.value[column.title!]) state.value[column.title!] = []
if (state.value[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
// This value is already in the list
return message.info(t('msg.info.valueAlreadyInList'))
}
if (Array.isArray(value)) {
state.value[column.title!]!.push(...value)
} else {
state.value[column.title!]!.push(value)
// state
const state = computed({
get: () => currentRow.value?.rowMeta?.ltarState ?? {},
set: (value) => {
if (currentRow.value) {
if (!currentRow.value.rowMeta) {
currentRow.value.rowMeta = {}
}
} else if (isBt(column) || isOo(column)) {
state.value[column.title!] = value
currentRow.value.rowMeta.ltarState = value
}
}
// actions
const removeLTARRef = async (value: Record<string, any>, column: ColumnType) => {
if (isHm(column) || isMm(column)) {
state.value[column.title!]?.splice(state.value[column.title!]?.indexOf(value), 1)
} else if (isBt(column) || isOo(column)) {
state.value[column.title!] = null
}
}
const linkRecord = async (
rowId: string,
relatedRowId: string,
column: ColumnType,
type: RelationTypes,
{ metaValue = meta.value }: { metaValue?: TableType } = {},
) => {
try {
await $api.dbTableRow.nestedAdd(
NOCO,
base.value.id as string,
metaValue?.id as string,
encodeURIComponent(rowId),
type,
column.id as string,
encodeURIComponent(relatedRowId),
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
/** sync LTAR relations kept in local state */
const syncLTARRefs = async (row: Record<string, any>, { metaValue = meta.value }: { metaValue?: TableType } = {}) => {
const id = extractPkFromRow(row, metaValue?.columns as ColumnType[])
for (const column of metaValue?.columns ?? []) {
if (!isLinksOrLTAR(column)) continue
const colOptions = column.colOptions as LinkToAnotherRecordType
const relatedTableMeta = metas.value?.[colOptions?.fk_related_model_id as string]
if (isHm(column) || isMm(column)) {
const relatedRows = (state.value?.[column.title!] ?? []) as Record<string, any>[]
for (const relatedRow of relatedRows) {
await linkRecord(
id,
extractPkFromRow(relatedRow, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
} else if ((isBt(column) || isOo(column)) && state.value?.[column.title!]) {
await linkRecord(
id,
extractPkFromRow(state.value?.[column.title!] as Record<string, any>, relatedTableMeta.columns as ColumnType[]),
column,
colOptions.type as RelationTypes,
{ metaValue },
)
}
// clear LTAR refs after sync
state.value[column.title!] = null
}
}
// clear LTAR cell
const clearLTARCell = async (column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
const relatedTableMeta = metas.value?.[(<LinkToAnotherRecordType>column?.colOptions)?.fk_related_model_id as string]
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ([RelationTypes.BELONGS_TO, RelationTypes.ONE_TO_ONE].includes((<LinkToAnotherRecordType>column.colOptions)?.type)) {
if (!currentRow.value.row[column.title!]) return
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]),
(<LinkToAnotherRecordType>column.colOptions)?.type as any,
column.id as string,
extractPkFromRow(currentRow.value.row[column.title!], relatedTableMeta?.columns as ColumnType[]),
)
currentRow.value.row[column.title!] = null
} else {
for (const link of (currentRow.value.row[column.title!] as Record<string, any>[]) || []) {
await $api.dbTableRow.nestedRemove(
NOCO,
base.value.id as string,
meta.value?.id as string,
encodeURIComponent(extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[])),
(<LinkToAnotherRecordType>column?.colOptions).type as 'hm' | 'mm',
column.id as string,
encodeURIComponent(extractPkFromRow(link, relatedTableMeta?.columns as ColumnType[])),
)
}
currentRow.value.row[column.title!] = []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const loadRow = async () => {
const record = await $api.dbTableRow.read(
NOCO,
base.value?.id as string,
meta.value?.title as string,
encodeURIComponent(extractPkFromRow(unref(row)?.row, meta.value?.columns as ColumnType[])),
)
Object.assign(unref(row), {
row: record,
oldRow: { ...record },
rowMeta: {},
})
}
// clear MM cell
const cleaMMCell = async (column: ColumnType) => {
try {
if (!column || !isLinksOrLTAR(column)) return
if (isNew.value) {
state.value[column.title!] = null
} else if (currentRow.value) {
if ((<LinkToAnotherRecordType>column.colOptions)?.type === RelationTypes.MANY_TO_MANY) {
if (!currentRow.value.row[column.title!]) return
console.log('currentRow.value.row, meta.value?.columns', currentRow.value.row, meta.value?.columns)
const result = await $api.dbDataTableRow.nestedListCopyPasteOrDeleteAll(
meta.value?.id as string,
column.id as string,
[
{
operation: 'deleteAll',
rowId: extractPkFromRow(currentRow.value.row, meta.value?.columns as ColumnType[]) as string,
columnId: column.id as string,
fk_related_model_id: (column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id as string,
},
],
)
currentRow.value.row[column.title!] = null
return Array.isArray(result.unlink) ? result.unlink : []
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
return {
row,
state,
isNew,
// todo: use better name
addLTARRef,
removeLTARRef,
syncLTARRefs,
loadRow,
currentRow,
clearLTARCell,
cleaMMCell,
}
},
'smartsheet-row-store',
)
},
})
// getters
const isNew = computed(() => unref(row).rowMeta?.new ?? false)
const { addLTARRef, removeLTARRef, syncLTARRefs, loadRow, clearLTARCell, cleaMMCell } = useSmartsheetLtarHelpersOrThrow()
return {
row,
state,
isNew,
// todo: use better name
addLTARRef: (...args: any) => addLTARRef(currentRow.value, ...args),
removeLTARRef: (...args: any) => removeLTARRef(currentRow.value, ...args),
syncLTARRefs: (...args: any) => syncLTARRefs(currentRow.value, ...args),
loadRow: (...args: any) => loadRow(currentRow.value, ...args),
currentRow,
clearLTARCell: (...args: any) => clearLTARCell(currentRow.value, ...args),
cleaMMCell: (...args: any) => cleaMMCell(currentRow.value, ...args),
}
}, 'smartsheet-row-store')
export { useProvideSmartsheetRowStore }

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

@ -38,6 +38,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
const isKanban = computed(() => view.value?.type === ViewTypes.KANBAN)
const isMap = computed(() => view.value?.type === ViewTypes.MAP)
const isSharedForm = computed(() => isForm.value && shared)
const isDefaultView = computed(() => view.value?.is_default)
const xWhere = computed(() => {
let where
const col =
@ -100,6 +101,7 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
eventBus,
sqlUi,
allFilters,
isDefaultView,
}
},
'smartsheet-store',

11
packages/nc-gui/composables/useUserSorts.ts

@ -9,7 +9,7 @@ import { useGlobal } from '#imports'
* @param {string} roleType - The type of role for which user sorts are managed ('Workspace', 'Org', or 'Project').
* @returns {object} An object containing reactive values and functions related to user sorts.
*/
export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project') {
export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project' | 'Organization') {
const clone = rfdc()
const { user } = useGlobal()
@ -110,6 +110,8 @@ export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project') {
userRoleOrder = Object.values(OrderedOrgRoles)
} else if (roleType === 'Project') {
userRoleOrder = Object.values(OrderedProjectRoles)
} else if (roleType === 'Organization') {
userRoleOrder = Object.values(OrderedOrgRoles)
}
data = clone(data)
@ -136,6 +138,13 @@ export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project') {
return b[sortsConfig.field]?.localeCompare(a[sortsConfig.field])
}
}
case 'title': {
if (sortsConfig.direction === 'asc') {
return a[sortsConfig.field] - b[sortsConfig.field]
} else {
return b[sortsConfig.field] - a[sortsConfig.field]
}
}
}
return 0

8
packages/nc-gui/composables/useViewColumns.ts

@ -159,7 +159,12 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
$e('a:fields:show-all')
}
const saveOrUpdate = async (field: any, index: number, disableDataReload: boolean = false) => {
const saveOrUpdate = async (
field: any,
index: number,
disableDataReload: boolean = false,
updateDefaultViewColumnOrder: boolean = false,
) => {
if (isLocalMode.value && fields.value) {
fields.value[index] = field
meta.value!.columns = meta.value!.columns?.map((column: ColumnType) => {
@ -168,6 +173,7 @@ const [useProvideViewColumns, useViewColumns] = useInjectionState(
...column,
...field,
id: field.fk_column_id,
...(updateDefaultViewColumnOrder ? { meta: { ...parseProp(column.meta), defaultViewColOrder: field.order } } : {}),
}
}
return column

897
packages/nc-gui/composables/useViewGroupBy.ts

File diff suppressed because it is too large Load Diff

2
packages/nc-gui/context/index.ts

@ -57,3 +57,5 @@ export const TreeViewInj: InjectionKey<{
export const CalendarViewTypeInj: InjectionKey<Ref<'week' | 'month' | 'day' | 'year'>> = Symbol('calendar-view-type-injection')
export const JsonExpandInj: InjectionKey<Ref<boolean>> = Symbol('json-expand-injection')
export const AllFiltersInj: InjectionKey<Ref<Record<string, FilterType[]>>> = Symbol('all-filters-injection')
export const IsAdminPanelInj: InjectionKey<Ref<boolean>> = Symbol('is-admin-panel-injection')

70
packages/nc-gui/lang/ar.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "الرئيسية",
"load": "تحميل",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "مشاهد",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "ليس فارغاً"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "البحث عن {بحث} لم يتم العثور على نتائج",

70
packages/nc-gui/lang/bn_IN.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "বি",
"load": "ভর",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "দরশক",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "নল নয"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "আপনর অনসনন {search} এর জনয কনও ফলফল পওযি",

70
packages/nc-gui/lang/cs.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "Domů",
"load": "Načíst",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Sledující",
"noaccess": "No Access",
"superAdmin": "Hlavní administrátor",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Tvůrce na úrovni organizace",
"orgLevelViewer": "Prohlížeč na úrovni organizace"
},
@ -313,6 +319,10 @@
"isNotNull": "není null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Dokumentace",
"forum": "Fórum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operace Vložit není v aktivní buňce podporována.",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Vaše hledání na {search} nenašlo žádné výsledky",

70
packages/nc-gui/lang/da.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "Forside",
"load": "Indlæs",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Viewer.",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Skaberen på organisationsniveau",
"orgLevelViewer": "Visning på organisationsniveau"
},
@ -313,6 +319,10 @@
"isNotNull": "er ikke null."
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Indsæt er ikke understøttet på den aktive celle",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Din søgning efter {Søg} viste ingen resultater",

70
packages/nc-gui/lang/de.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Beenden",
"home": "Start",
"load": "Laden",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown-Liste",
"list": "Liste",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Tag",
"week": "Woche",
"month": "Monat",
@ -247,6 +252,7 @@
"viewer": "Betrachter",
"noaccess": "Kein Zugriff",
"superAdmin": "Super-Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organisationsebenen-Ersteller",
"orgLevelViewer": "Organisationsebenen-Betrachter"
},
@ -313,6 +319,10 @@
"isNotNull": "ist nicht Null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentifizierung (SSO)",
"docs": "Dokumentation",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Vorherige",
"nextMonth": "Folgender Monat",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Passwort eingeben",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Der Vorgang Einfügen wird auf der aktiven Zelle nicht unterstützt",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Ihre Suche nach {search} fand keine Ergebnisse",

70
packages/nc-gui/lang/en.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "Home",
"load": "Load",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Viewer",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "is not null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results"
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace":"-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "SAML",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Your search for {search} found no results",

70
packages/nc-gui/lang/es.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Salir",
"home": "Inicio",
"load": "Cargar",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Día",
"week": "Semana",
"month": "Mes",
@ -247,6 +252,7 @@
"viewer": "Visor",
"noaccess": "Sin acceso",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Creador a nivel de organización",
"orgLevelViewer": "Visor de nivel de organización"
},
@ -313,6 +319,10 @@
"isNotNull": "no es nulo"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Autenticación (SSO)",
"docs": "Documentos",
"forum": "Foro",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Seleccionar Año",
"save": "Guardar",
"cancel": "Cancelar",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "Nuevo proveedor",
"generalSettings": "Ajustes Generales",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "Ajustes SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organizar por",
"previous": "Anterior",
"nextMonth": "Mes siguiente",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "La vista del calendario requiere un rango de fechas",
"goToToday": "Ir a Hoy",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Introducir la contraseña",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "Un único registro de la tabla ",
"tooltip_desc2": " puede vincularse con un único registro de la tabla "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No hay registros vinculados",
"noLinkedRecords": "No linked records",
"recordsLinked": "registros vinculados",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "No se admite la operación de pegado en la celda activa",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Tu búsqueda de {search} no encontró resultados",

70
packages/nc-gui/lang/eu.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Irten",
"home": "Hasiera",
"load": "Kargatu",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Ikuslea",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "is not null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Dokumentuak",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Your search for {search} found no results",

70
packages/nc-gui/lang/fa.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "خروج",
"home": "خانه",
"load": "لود کردن",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "بیننده",
"noaccess": "بدون دسترسی",
"superAdmin": "مدیر ارشد",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "تولید کننده سطح سازمانی",
"orgLevelViewer": "مشاهده کننده سطح سازمانی"
},
@ -313,6 +319,10 @@
"isNotNull": "تهی نیست"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "مستندات",
"forum": "انجمن",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "عملیات جایگذاری در سلول فعال پشتیبانی نمیشود",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "جستوجوی شما برای {search} نتیجهای نداشت",

70
packages/nc-gui/lang/fi.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "Koti",
"load": "Ladata",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Katselija",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organisaatiotason luoja",
"orgLevelViewer": "Organisaatiotason katseluohjelma"
},
@ -313,6 +319,10 @@
"isNotNull": "ei ole nolla"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Liitä-toimintoa ei tueta aktiivisessa solussa.",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Hakusi {haku} ei löytänyt tuloksia",

70
packages/nc-gui/lang/fr.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quitter",
"home": "Accueil",
"load": "Charger",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Liste déroulante",
"list": "Liste",
"verify": "Verify",
"apply": "Appliquer",
"text": "Texte",
"appearance": "Apparence"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Jour",
"week": "Semaine",
"month": "Mois",
@ -247,6 +252,7 @@
"viewer": "Lecture seule",
"noaccess": "Accès interdit",
"superAdmin": "Super administrateur",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Créateur au niveau de l'organisation",
"orgLevelViewer": "Visualiseur de niveau d'organisation"
},
@ -313,6 +319,10 @@
"isNotNull": "est non null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Documents",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Langage de balisage d'assertion de sécurité (SAML)",
"newProvider": "Nouveau fournisseur",
"generalSettings": "Paramètres généraux",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "Paramètres SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Cliquer pour copier l'ID du champ",
"enterPassword": "Saisir le mot de passe",
"bySigningUp": "En vous inscrivant, vous acceptez les",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Désactivé car la vue est verrouillée",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "L'opération de collage n'est pas prise en charge sur la cellule active",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Le nom doit comporter au moins 2 caractères",
"nameMaxLength": "Le nom doit comporter au maximum 60 caractères",
"viewNameRequired": "Le nom de la vue est obligatoire",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Le nom doit comporter au maximum 256 caractères",
"viewNameUnique": "Le nom de la vue doit être unique",
"searchProject": "Votre recherche pour {search} n'a renvoyé aucun résultat",

70
packages/nc-gui/lang/he.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "יציאה",
"home": "בית",
"load": "טען",
@ -198,11 +200,14 @@
"logo": "סמליל",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "צוֹפֶה",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "is not null"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "לבחור שנה",
"save": "שמירה",
"cancel": "ביטול",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "ספק חדש",
"generalSettings": "הגדרות כלליות",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "הגדרות SSO",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "מאורגן על-ידי",
"previous": "הקודם",
"nextMonth": "החודש הבא",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "החיפוש שלך {חיפוש} לא נמצא תוצאות",

70
packages/nc-gui/lang/hi.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "घर",
"load": "भर",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "दरशक",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Organization Level Creator",
"orgLevelViewer": "Organization Level Viewer"
},
@ -313,6 +319,10 @@
"isNotNull": "निररथक नह"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Paste operation is not supported on the active cell",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "{search} किए आपकज कई परिम नहि",

70
packages/nc-gui/lang/hr.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Quit",
"home": "Dom",
"load": "Opterećenje",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Preglednika",
"noaccess": "No Access",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Stvaratelj na razini organizacije",
"orgLevelViewer": "Preglednik na razini organizacije"
},
@ -313,6 +319,10 @@
"isNotNull": "nije ništavan"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operacija \"lijepljenje\" nije podržana u aktivnoj ćeliji",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Vaša potraga za {Search} nije pronašla rezultate",

70
packages/nc-gui/lang/id.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Keluar",
"home": "Rumah",
"load": "Memuat",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Penonton",
"noaccess": "Tidak ada akses",
"superAdmin": "Super Admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Pembuat Tingkat Organisasi",
"orgLevelViewer": "Penampil Tingkat Organisasi"
},
@ -313,6 +319,10 @@
"isNotNull": "bukan null."
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Docs",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Click to copy Field Id",
"enterPassword": "Enter password",
"bySigningUp": "By signing up, you agree to the",
@ -1118,7 +1181,7 @@
"tooltip_desc": "A single record from table ",
"tooltip_desc2": " can be linked with a single record from table "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "No records linked",
"noLinkedRecords": "No linked records",
"recordsLinked": "records linked",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabled as View is locked",
"basesMigrated": "Bases are migrated. Please try again.",
"pasteNotSupported": "Operasi tempel tidak didukung pada sel aktif",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Name must be at least 2 characters long",
"nameMaxLength": "Name must be at most 60 characters long",
"viewNameRequired": "View name is required",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Name must be at most 256 characters long",
"viewNameUnique": "View name should be unique",
"searchProject": "Pencarian Anda untuk {Search} tidak menemukan hasil",

70
packages/nc-gui/lang/it.json

@ -39,6 +39,8 @@
}
},
"general": {
"role": "Role",
"general": "General",
"quit": "Esci",
"home": "Pagina iniziale",
"load": "Carica",
@ -198,11 +200,14 @@
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List",
"verify": "Verify",
"apply": "Apply",
"text": "Text",
"appearance": "Appearance"
},
"objects": {
"owner": "Owner",
"member": "Member",
"day": "Day",
"week": "Week",
"month": "Month",
@ -247,6 +252,7 @@
"viewer": "Spettatore",
"noaccess": "Nessun Accesso",
"superAdmin": "Super admin",
"orgLevelOwner": "Organization Level Owner",
"orgLevelCreator": "Creatore a livello di organizzazione",
"orgLevelViewer": "Visualizzatore a livello di organizzazione"
},
@ -313,6 +319,10 @@
"isNotNull": "non è nullo"
},
"title": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename Workspace",
"renamingWorkspace": "Renaming Workspace",
"renamingBase": "Renaming Base",
"sso": "Authentication (SSO)",
"docs": "Documentazione",
"forum": "Forum",
@ -437,6 +447,39 @@
"noResultsMatchedYourSearch": "Your search did not yield any matching results."
},
"labels": {
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",
"goToMembers": "Go to Members",
"addMember": "Add Member",
"numberOfMembers": "No. Members",
"numberOfBases": "No. Bases",
"numberOfRecords": "No. Records",
"workspaceName": "Workspace Name",
"workspaceWithoutOwner": "Workspace without Owners",
"inviteUsersToWorkspace": "Invite Users to Workspace",
"selectWorkspace": "-select workspaces to invite to-",
"addMembersToOrganization": "Add Members to Organization",
"memberIn": "Member in:",
"assignAs": "Assign as",
"signOutUser": "Sign out user",
"signOutUsers": "Sign out users",
"deactivateUser": "Deactivate User",
"deactivateUsers": "Deactivate Users",
"lastActive": "Last Active",
"dateAdded": "Date Added",
"uploadImage": "Upload Image",
"organizationProfile": "Organisation Profile",
"organizationImage": "Organisation Image",
"organizationName": "Organisation Name",
"activeDomains": "Active Domains",
"domains": "Domains",
"disablePublicSharing": "Disable Public Sharing",
"shareSettings": "Share Settings",
"deleteUserAndData": "Delete User and their data",
"userOptions": "User Options",
"deleteThisOrganization": "Delete this Organisation",
"dangerZone": "Dangerzone",
"selectYear": "Select Year",
"save": "Save",
"cancel": "Cancel",
@ -447,7 +490,15 @@
"saml": "Security Assertion Markup Language (SAML)",
"newProvider": "New Provider",
"generalSettings": "General Settings",
"adminPanel": "Admin Panel",
"moveWorkspaceToOrg": "Move Workspace To Organisation",
"ssoSettings": "SSO Settings",
"addDomain": "Add Domain",
"domain": "Domain",
"settings": "Settings",
"workspaces": "Workspaces",
"back": "Back",
"dashboard": "Dashboard",
"organizeBy": "Organize by",
"previous": "Previous",
"nextMonth": "Next Month",
@ -709,9 +760,16 @@
"clearSelection": "Clear selection"
},
"activity": {
"renameBase": "Rename Base",
"renameWorkspace": "Rename workspace",
"deactivate": "De-activate",
"manageUsers": "Manage Users",
"newWorkspace": "New Workspace",
"addDomain": "Add Domain",
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"inviteToWorkspace": "Invite to Workspace",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
@ -1036,6 +1094,11 @@
"searchOptions": "Search options"
},
"msg": {
"controlOrgAppearance": "Control your organisations name and appearance.",
"addCompanyDomains": "Add company domains to restrict access to unwanted users.",
"restrictUsersFromSharing": "Restrict users from being able to share bases publicly.",
"selectUsersToBeRemoved": "Select users to be removed and deleted from all organisation workspaces.",
"deleteOrganization": "Delete all users, bases and data related to this organization",
"clickToCopyFieldId": "Fare clic per copiare l'Id del campo",
"enterPassword": "Inserisci password",
"bySigningUp": "Registrandoti, accetti i",
@ -1118,7 +1181,7 @@
"tooltip_desc": "Una singola riga dalla tabella ",
"tooltip_desc2": " può essere collegato con una riga della tabella "
},
"clickLinkRecordsToAddLinkFromTable": "Click 'Link Records' to begin associating data with '{tableName}'.",
"clickLinkRecordsToAddLinkFromTable": "Looks like no records have been linked yet.",
"noRecordsLinked": "Nessun record collegato",
"noLinkedRecords": "No linked records",
"recordsLinked": "record collegati",
@ -1162,8 +1225,11 @@
}
},
"info": {
"enterWorkspaceName": "Enter workspace name",
"enterBaseName": "Enter base name",
"idpPaste": "Paste these URL in your Identity Providers console",
"noSaml": "There are no configured SAML authentications.",
"noOIDC": "There are no configured OpenID authentications.",
"disabledAsViewLocked": "Disabilitato perché la Vista è bloccata",
"basesMigrated": "Le Basi sono migrate. Per favore riprova.",
"pasteNotSupported": "L'operazione di incollamento non è supportata sulla cella attiva.",
@ -1339,6 +1405,7 @@
"fetchingCalendarData": "Error fetching calendar data",
"fetchingActiveDates": "Error fetching active dates",
"scopesRequired": "Scopes required",
"domainRequired": "Domain name is required",
"authUrlRequired": "Auth URL is required",
"userNameAttributeRequired": "Username attribute is required",
"clientIdRequired": "Client ID is required",
@ -1352,6 +1419,7 @@
"nameMinLength": "Il nome deve essere lungo almeno 2 caratteri",
"nameMaxLength": "Il nome deve essere lungo al più 60 caratteri",
"viewNameRequired": "È necessario inserire il nome della vista",
"domainNameRequired": "Domain name is required",
"nameMaxLength256": "Il nome deve essere lungo al più 256 caratteri",
"viewNameUnique": "Il nome della Visualizzazione deve essere unico",
"searchProject": "La tua ricerca di {search} non ha trovato risultati",

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

Loading…
Cancel
Save