-
+
{{ email }}
(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"
/>
{{
emailValidation.message
}}
+
+
+
+
+ {{ workspace.title }}
+
+
+
+
+
+
+
+
+ {{ workspace.title }}
+
+
+
+
+
+
{{ $t('labels.cancel') }}
- {{ $t('activity.inviteToBase') }}
+ {{ type === 'base' ? $t('activity.inviteToBase') : $t('activity.inviteToWorkspace') }}
+
+
diff --git a/packages/nc-gui/components/general/BaseIconColorPicker.vue b/packages/nc-gui/components/general/BaseIconColorPicker.vue
index d88ca36ebb..05d39a43b9 100644
--- a/packages/nc-gui/components/general/BaseIconColorPicker.vue
+++ b/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',
diff --git a/packages/nc-gui/components/general/CopyButton.vue b/packages/nc-gui/components/general/CopyButton.vue
new file mode 100644
index 0000000000..cc2b1c6584
--- /dev/null
+++ b/packages/nc-gui/components/general/CopyButton.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
diff --git a/packages/nc-gui/components/general/WorkspaceIcon.vue b/packages/nc-gui/components/general/WorkspaceIcon.vue
index e5d14087ce..963078eb3f 100644
--- a/packages/nc-gui/components/general/WorkspaceIcon.vue
+++ b/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 }"
>
diff --git a/packages/nc-gui/components/nc/Badge.vue b/packages/nc-gui/components/nc/Badge.vue
index 4ba282e4a4..774b1eecbf 100644
--- a/packages/nc-gui/components/nc/Badge.vue
+++ b/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',
},
)
diff --git a/packages/nc-gui/components/nc/ErrorBoundary.vue b/packages/nc-gui/components/nc/ErrorBoundary.vue
index cb3ce25abd..567a956f8a 100644
--- a/packages/nc-gui/components/nc/ErrorBoundary.vue
+++ b/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
diff --git a/packages/nc-gui/components/nc/Select.vue b/packages/nc-gui/components/nc/Select.vue
index bd109bdcf2..553501fee6 100644
--- a/packages/nc-gui/components/nc/Select.vue
+++ b/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) => {
-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()
+})
-
-
+
+
+
+
+
+ {{ $t('objects.projects') }}
+
+
+ /
+
+
+ {{ currentBase?.title }}
+
+
+
+
-
-
+
+
-
+
{{ $t('activity.addMembers') }}
@@ -188,7 +231,7 @@ watch(isInviteModalVisible, () => {
- {{ $t('general.access') }}
+ {{ $t('general.role') }}
@@ -203,17 +246,16 @@ watch(isInviteModalVisible, () => {
>
-
-
+
+
+
+ {{ collab.display_name || collab.email.slice(0, collab.email.indexOf('@')) }}
+
+
+
{{ collab.email }}
-
-
- {{ collab.display_name }}
-
-
- {{ collab.email }}
-
+
@@ -230,7 +272,7 @@ watch(isInviteModalVisible, () => {
/>
-
+
@@ -252,6 +294,18 @@ watch(isInviteModalVisible, () => {
diff --git a/packages/nc-gui/components/workspace/View.vue b/packages/nc-gui/components/workspace/View.vue
index 50750cf56a..d5f25a9254 100644
--- a/packages/nc-gui/components/workspace/View.vue
+++ b/packages/nc-gui/components/workspace/View.vue
@@ -1,5 +1,10 @@
-
-
-
-
- {{ activeWorkspace?.title }}
+
+
+
+
+ {{ currentWorkspace?.title }}
+
+
+
+
+ {{ $t('labels.workspaces') }}
+
+
+ /
+
+
+ {{ currentWorkspace?.title }}
+
+
+
+
@@ -65,7 +101,7 @@ onMounted(() => {
Members
-
+
@@ -77,7 +113,7 @@ onMounted(() => {
Settings
-
+
@@ -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;
}
diff --git a/packages/nc-gui/composables/useOrganization.ts b/packages/nc-gui/composables/useOrganization.ts
new file mode 100644
index 0000000000..3dea47c819
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/packages/nc-gui/composables/useUserSorts.ts b/packages/nc-gui/composables/useUserSorts.ts
index 9182a701e5..1692d51952 100644
--- a/packages/nc-gui/composables/useUserSorts.ts
+++ b/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
diff --git a/packages/nc-gui/context/index.ts b/packages/nc-gui/context/index.ts
index dd5d02c427..cfc411a49e 100644
--- a/packages/nc-gui/context/index.ts
+++ b/packages/nc-gui/context/index.ts
@@ -57,3 +57,5 @@ export const TreeViewInj: InjectionKey<{
export const CalendarViewTypeInj: InjectionKey
[> = Symbol('calendar-view-type-injection')
export const JsonExpandInj: InjectionKey][> = Symbol('json-expand-injection')
export const AllFiltersInj: InjectionKey][>> = Symbol('all-filters-injection')
+
+export const IsAdminPanelInj: InjectionKey][> = Symbol('is-admin-panel-injection')
diff --git a/packages/nc-gui/lang/en.json b/packages/nc-gui/lang/en.json
index b0164345ec..f94af5d57a 100644
--- a/packages/nc-gui/lang/en.json
+++ b/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",
@@ -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",
diff --git a/packages/nc-gui/lib/acl.ts b/packages/nc-gui/lib/acl.ts
index c38247b0ce..837f14ddb4 100644
--- a/packages/nc-gui/lib/acl.ts
+++ b/packages/nc-gui/lib/acl.ts
@@ -36,6 +36,7 @@ const rolePermissions = {
newUser: true,
viewCreateOrEdit: true,
baseReorder: true,
+ orgAdminPanel: true,
},
},
[OrgUserRoles.VIEWER]: {
diff --git a/packages/nc-gui/lib/types.ts b/packages/nc-gui/lib/types.ts
index 0961e14ed5..2f8f1aa6e6 100644
--- a/packages/nc-gui/lib/types.ts
+++ b/packages/nc-gui/lib/types.ts
@@ -202,7 +202,7 @@ interface SidebarTableNode extends TableType {
}
interface UsersSortType {
- field?: 'email' | 'roles'
+ field?: 'email' | 'roles' | 'title' | 'id'
direction?: 'asc' | 'desc'
}
diff --git a/packages/nc-gui/middleware/02.auth.global.ts b/packages/nc-gui/middleware/02.auth.global.ts
index e4c8a4bf33..660ce7a2d3 100644
--- a/packages/nc-gui/middleware/02.auth.global.ts
+++ b/packages/nc-gui/middleware/02.auth.global.ts
@@ -37,6 +37,8 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
const { allRoles, loadRoles } = useRoles()
+ await checkForRedirect()
+
/** If baseHostname defined block home page access under subdomains, and redirect to workspace page */
if (
state.appInfo.value.baseHostName &&
@@ -196,3 +198,19 @@ async function tryShortTokenAuth(api: Api, signIn: Actions['signIn']) {
window.location.reload()
}
}
+
+/** Check if url contains redirect param and redirect to the url if found */
+async function checkForRedirect() {
+ if (window.location.search && /\bui-redirect=/.test(window.location.search)) {
+ let url
+ try {
+ url = decodeURIComponent(window.location.search.split('=')[1])
+ } catch (e: any) {
+ message.error(await extractSdkResponseErrorMsg(e))
+ }
+
+ const newURL = window.location.href.split('?')[0]
+ window.history.pushState('object', document.title, `${newURL}#${url}`)
+ window.location.reload()
+ }
+}
diff --git a/packages/nc-gui/store/workspace.ts b/packages/nc-gui/store/workspace.ts
index 8dc04f1efe..b989c9baa4 100644
--- a/packages/nc-gui/store/workspace.ts
+++ b/packages/nc-gui/store/workspace.ts
@@ -2,6 +2,7 @@ import type { BaseType } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { message } from 'ant-design-vue'
import { isString } from '@vue/shared'
+import type { ThemeConfig } from '#imports'
import {
computed,
navigateTo,
@@ -13,7 +14,6 @@ import {
useRouter,
useTheme,
} from '#imports'
-import type { ThemeConfig } from '#imports'
export const useWorkspace = defineStore('workspaceStore', () => {
const basesStore = useBases()
@@ -92,6 +92,8 @@ export const useWorkspace = defineStore('workspaceStore', () => {
const loadWorkspace = async (..._args: any) => {}
+ const moveToOrg = async (..._args: any) => {}
+
async function populateWorkspace(..._args: any) {
isWorkspaceLoading.value = true
@@ -265,6 +267,7 @@ export const useWorkspace = defineStore('workspaceStore', () => {
workspaceUserCount,
getPlanLimit,
workspaceRole,
+ moveToOrg,
}
})
diff --git a/packages/nc-gui/utils/datetimeUtils.ts b/packages/nc-gui/utils/datetimeUtils.ts
new file mode 100644
index 0000000000..7b0979ac66
--- /dev/null
+++ b/packages/nc-gui/utils/datetimeUtils.ts
@@ -0,0 +1,28 @@
+import dayjs from 'dayjs'
+import { dateFormats, timeFormats } from 'nocodb-sdk'
+
+export function parseStringDateTime(
+ v: string,
+ dateTimeFormat: string = `${dateFormats[0]} ${timeFormats[0]}`,
+ toLocal: boolean = true,
+) {
+ const dayjsObj = toLocal ? dayjs(v).local() : dayjs(v)
+
+ if (dayjsObj.isValid()) {
+ v = dayjsObj.format(dateTimeFormat)
+ } else {
+ v = toLocal ? dayjs(v, dateTimeFormat).local().format(dateTimeFormat) : dayjs(v, dateTimeFormat).format(dateTimeFormat)
+ }
+
+ return v
+}
+
+export const timeAgo = (date: any) => {
+ if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(date)) {
+ // if there is no timezone info, consider as UTC
+ // e.g. 2023-01-01 08:00:00 (MySQL)
+ date += '+00:00'
+ }
+ // show in local time
+ return dayjs(date).fromNow()
+}
diff --git a/packages/nc-gui/utils/iconUtils.ts b/packages/nc-gui/utils/iconUtils.ts
index a286a5da2d..96ebd44266 100644
--- a/packages/nc-gui/utils/iconUtils.ts
+++ b/packages/nc-gui/utils/iconUtils.ts
@@ -125,6 +125,9 @@ import NcArrowLeft from '~icons/nc-icons/arrow-left'
import NcArrowRight from '~icons/nc-icons/arrow-right'
import NcUpload from '~icons/nc-icons/upload'
import NcDownload from '~icons/nc-icons/download'
+import NcOffice from '~icons/nc-icons/office'
+import NcArrowUpRight from '~icons/nc-icons/arrow-up-right'
+import NcSlash from '~icons/nc-icons/slash'
// import NcProjectGray from '~icons/nc-icons/project-gray'
import NcPhoneCall from '~icons/nc-icons/phone-call'
import NcItalic from '~icons/nc-icons/italic'
@@ -132,6 +135,9 @@ import NcBold from '~icons/nc-icons/bold'
import NcUnderline from '~icons/nc-icons/underline'
import NcCrop from '~icons/nc-icons/crop'
import NcLink from '~icons/nc-icons/link'
+import NcControlPanel from '~icons/nc-icons/control-panel'
+import NcHome from '~icons/nc-icons/home'
+import NcWorkspace from '~icons/nc-icons/workspace'
import NcCellBarcode from '~icons/nc-icons/cell-barcode'
import NcCellCheckbox from '~icons/nc-icons/cell-checkbox'
@@ -325,6 +331,11 @@ import NcHelp from '~icons/nc-icons/help'
} as const */
export const iconMap = {
+ slash: NcSlash,
+ arrowUpRight: NcArrowUpRight,
+ ncWorkspace: NcWorkspace,
+ controlPanel: NcControlPanel,
+ home: NcHome,
cellBarcode: NcCellBarcode,
cellCheckbox: NcCellCheckbox,
cellDate: NcCellDate,
@@ -358,6 +369,7 @@ export const iconMap = {
cellSystemText: NcCellSystemText,
cellAttachment: NcCellAttachment,
+ office: NcOffice,
sort: Sort,
group: Group,
filter: Filter,
diff --git a/packages/nc-gui/utils/index.ts b/packages/nc-gui/utils/index.ts
index afcd69c42a..0507da297b 100644
--- a/packages/nc-gui/utils/index.ts
+++ b/packages/nc-gui/utils/index.ts
@@ -24,5 +24,6 @@ export * from './parseUtils'
export * from './cell'
export * from './workerUtils'
export * from './parsersUtils'
+export * from './datetimeUtils'
export const isEeUI = false
diff --git a/packages/nocodb-sdk/src/lib/Api.ts b/packages/nocodb-sdk/src/lib/Api.ts
index 189ffded61..1587e0bbd8 100644
--- a/packages/nocodb-sdk/src/lib/Api.ts
+++ b/packages/nocodb-sdk/src/lib/Api.ts
@@ -296,7 +296,8 @@ export interface SourceType {
| 'oracledb'
| 'pg'
| 'snowflake'
- | 'sqlite3';
+ | 'sqlite3'
+ | 'databricks';
}
/**
@@ -342,7 +343,8 @@ export interface BaseReqType {
| 'oracledb'
| 'pg'
| 'snowflake'
- | 'sqlite3';
+ | 'sqlite3'
+ | 'databricks';
}
/**
@@ -2914,6 +2916,87 @@ export type NestedListCopyPasteOrDeleteAllReqType = {
fk_related_model_id: string;
}[];
+/**
+ * Model for Kanban Column Request
+ */
+export interface KanbanColumnReqType {
+ /** Title */
+ title?: string;
+ /** Is this column shown? */
+ show?: BoolType;
+ /**
+ * Column Order
+ * @example 1
+ */
+ order?: number;
+}
+
+/**
+ * Model for Gallery Column Request
+ */
+export interface GalleryColumnReqType {
+ /** Show */
+ show?: BoolType;
+ /**
+ * Order
+ * @example 1
+ */
+ order?: number;
+}
+
+/**
+ * Model for Calendar Column Request
+ */
+export interface CalendarColumnReqType {
+ /** Is this column shown? */
+ show?: BoolType;
+ /** Is this column shown as bold? */
+ bold?: BoolType;
+ /** Is this column shown as italic? */
+ italic?: BoolType;
+ /** Is this column shown underlines? */
+ underline?: BoolType;
+ /**
+ * Column Order
+ * @example 1
+ */
+ order?: number;
+}
+
+export interface ExtensionType {
+ /** Unique ID */
+ id?: IdType;
+ /** Unique Base ID */
+ base_id?: IdType;
+ /** Unique User ID */
+ fk_user_id?: IdType;
+ /** Extension ID */
+ extension_id?: string;
+ /** Extension Title */
+ title?: string;
+ /** Key Value Store for the extension */
+ kv_store?: MetaType;
+ /** Meta data for the extension */
+ meta?: MetaType;
+ /** Order of the extension */
+ order?: number;
+}
+
+export interface ExtensionReqType {
+ /** Unique Base ID */
+ base_id?: IdType;
+ /** Extension Title */
+ title?: string;
+ /** Extension ID */
+ extension_id?: string;
+ /** Key Value Store for the extension */
+ kv_store?: MetaType;
+ /** Meta data for the extension */
+ meta?: MetaType;
+ /** Order of the extension */
+ order?: number;
+}
+
import type {
AxiosInstance,
AxiosRequestConfig,
@@ -9819,7 +9902,8 @@ export class Api<
| 'oracledb'
| 'pg'
| 'snowflake'
- | 'sqlite3';
+ | 'sqlite3'
+ | 'databricks';
connection?: {
host?: string;
port?: string;
@@ -9880,7 +9964,7 @@ export class Api<
* DB Type
* @example mysql2
*\
- client?: "mssql" | "mysql" | "mysql2" | "oracledb" | "pg" | "snowflake" | "sqlite3",
+ client?: "mssql" | "mysql" | "mysql2" | "oracledb" | "pg" | "snowflake" | "sqlite3" | "databricks",
\** Connection Config *\
connection?: {
\** DB User *\
@@ -9926,7 +10010,8 @@ export class Api<
| 'oracledb'
| 'pg'
| 'snowflake'
- | 'sqlite3';
+ | 'sqlite3'
+ | 'databricks';
/** Connection Config */
connection?: {
/** DB User */
@@ -11454,6 +11539,104 @@ export class Api<
...params,
}),
};
+ extensions = {
+ /**
+ * @description Get all extensions for a given base
+ *
+ * @tags Extensions
+ * @name List
+ * @summary Get Extensions
+ * @request GET:/api/v2/extensions/{baseId}
+ * @response `200` `{
+ list?: (object)[],
+
+}` OK
+ */
+ list: (baseId: IdType, params: RequestParams = {}) =>
+ this.request<
+ {
+ list?: object[];
+ },
+ any
+ >({
+ path: `/api/v2/extensions/${baseId}`,
+ method: 'GET',
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * @description Create a new extension for a given base
+ *
+ * @tags Extensions
+ * @name Create
+ * @summary Create Extension
+ * @request POST:/api/v2/extensions/{baseId}
+ * @response `200` `any` OK
+ */
+ create: (baseId: IdType, data: object, params: RequestParams = {}) =>
+ this.request({
+ path: `/api/v2/extensions/${baseId}`,
+ method: 'POST',
+ body: data,
+ type: ContentType.Json,
+ format: 'json',
+ ...params,
+ }),
+
+ /**
+ * @description Get extension details
+ *
+ * @tags Extensions
+ * @name Read
+ * @summary Get Extension
+ * @request GET:/api/v2/extensions/{extensionId}
+ * @response `200` `object` OK
+ */
+ read: (extensionId: IdType, params: RequestParams = {}) =>
+ this.request]