Browse Source

feat: hook and dropdown menu for user sort management

feat/user-management-sort
Ramesh Mane 10 months ago
parent
commit
761e7152a2
  1. 39
      packages/nc-gui/components/account/UserList.vue
  2. 89
      packages/nc-gui/components/account/UserMenu.vue
  3. 133
      packages/nc-gui/composables/useUserSorts.ts
  4. 4
      packages/nc-gui/lang/en.json
  5. 6
      packages/nc-gui/lib/types.ts

39
packages/nc-gui/components/account/UserList.vue

@ -2,7 +2,16 @@
import { OrgUserRoles } from 'nocodb-sdk'
import type { OrgUserReqType, RequestParams, UserType } from 'nocodb-sdk'
import type { User } from '#imports'
import { extractSdkResponseErrorMsg, iconMap, useApi, useCopy, useDashboard, useDebounceFn, useNuxtApp } from '#imports'
import {
extractSdkResponseErrorMsg,
iconMap,
useApi,
useCopy,
useDashboard,
useDebounceFn,
useNuxtApp,
useUserSorts,
} from '#imports'
const { api, isLoading } = useApi()
@ -19,8 +28,14 @@ const { user: loggedInUser } = useGlobal()
const { copy } = useCopy()
const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortsData } = useUserSorts()
const users = ref<UserType[]>([])
const sortedUsers = computed(() => {
return handleGetSortsData(users.value, sorts.value) as UserType[]
})
const currentPage = ref(1)
const currentLimit = ref(10)
@ -64,6 +79,7 @@ const loadUsers = useDebounceFn(async (page = currentPage.value, limit = current
onMounted(() => {
loadUsers()
loadSorts()
})
const updateRole = async (userId: string, roles: string) => {
@ -176,10 +192,21 @@ const openDeleteModal = (user: UserType) => {
</div>
<div class="w-full rounded-md max-w-250 h-[calc(100%-12rem)] rounded-md overflow-hidden mt-5">
<div class="flex w-full bg-gray-50 border-1 rounded-t-md">
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6" data-rec="true">
{{ $t('labels.email') }}
<div
class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6 flex items-center space-x-2"
data-rec="true"
>
<span>
{{ $t('labels.email') }}
</span>
<LazyAccountUserMenu :direction="sortDirection['email']" field="email" :handle-user-sort="saveOrUpdate" />
</div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start flex items-center space-x-2" data-rec="true">
<span>
{{ $t('objects.role') }}
</span>
<LazyAccountUserMenu :direction="sortDirection['roles']" field="roles" :handle-user-sort="saveOrUpdate" />
</div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start" data-rec="true">{{ $t('objects.role') }}</div>
<div class="flex py-3.5 text-gray-500 font-medium text-3.5 w-28 justify-end mr-4" data-rec="true">
{{ $t('labels.action') }}
</div>
@ -193,7 +220,7 @@ const openDeleteModal = (user: UserType) => {
</div>
<section v-else class="tbody h-[calc(100%-4rem)] nc-scrollbar-md border-t-0 !overflow-auto">
<div
v-for="el of users"
v-for="el of sortedUsers"
:key="el.id"
data-testid="nc-token-list"
class="user flex py-3 justify-around px-1 border-b-1 border-l-1 border-r-1"
@ -261,7 +288,7 @@ const openDeleteModal = (user: UserType) => {
<div
class="flex items-center gap-2"
:class="{
'opacity-0': el.roles?.includes('super'),
'opacity-0 pointer-events-none': el.roles?.includes('super'),
}"
>
<NcDropdown :trigger="['click']">

89
packages/nc-gui/components/account/UserMenu.vue

@ -0,0 +1,89 @@
<script lang="ts" setup>
import { iconMap } from '#imports'
import type { UsersSortType } from '~/lib'
const { field, direction, handleUserSort } = defineProps<{
field: UsersSortType['field']
direction: UsersSortType['direction']
handleUserSort: Function
}>()
const isOpen = ref(false)
const sortUserBy = async (direction?: UsersSortType['direction']) => {
handleUserSort({
field: field,
direction,
})
isOpen.value = false
}
</script>
<template>
<a-dropdown
v-model:visible="isOpen"
:trigger="['click']"
placement="bottomLeft"
overlay-class-name="nc-user-menu-column-operations !border-1 rounded-lg !shadow-xl"
@click.stop="isOpen = !isOpen"
>
<div>
<GeneralIcon
:icon="direction === 'asc' || direction === 'desc' ? 'sortDesc' : 'arrowDown'"
class="text-grey h-full text-grey nc-user-menu-trigger cursor-pointer outline-0 mr-2 transition-none"
:style="{ transform: direction === 'asc' ? 'rotate(180deg)' : undefined }"
/>
</div>
<template #overlay>
<NcMenu class="flex flex-col gap-1 border-gray-200 nc-user-menu-column-options">
<NcMenuItem @click="sortUserBy('asc')">
<div class="nc-column-insert-after nc-user-menu-item">
<component
:is="iconMap.sortDesc"
class="text-gray-700 !rotate-180 !w-4.25 !h-4.25"
:style="{
transform: 'rotate(180deg)',
}"
/>
<!-- Sort Ascending -->
{{ $t('general.sortAsc') }}
</div>
</NcMenuItem>
<NcMenuItem @click="sortUserBy('desc')">
<div class="nc-column-insert-before nc-user-menu-item">
<component :is="iconMap.sortDesc" class="text-gray-700 !w-4.25 !h-4.25 ml-0.5 mr-0.25" />
<!-- Sort Descending -->
{{ $t('general.sortDesc') }}
</div>
</NcMenuItem>
<a-divider class="!my-0" />
<NcMenuItem @click="sortUserBy()">
<div class="nc-column-delete nc-user-menu-item text-gray-700">
<component :is="iconMap.close" />
<!-- Reset -->
{{ $t('general.resetSort') }}
</div>
</NcMenuItem>
</NcMenu>
</template>
</a-dropdown>
</template>
<style scoped>
.nc-user-menu-item {
@apply flex items-center gap-2;
}
.nc-user-menu-column-options {
.nc-icons {
@apply !w-5 !h-5;
}
}
:deep(.ant-dropdown-menu-item) {
@apply !hover:text-black text-gray-700;
}
</style>

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

@ -0,0 +1,133 @@
import rfdc from 'rfdc'
import type { UsersSortType } from '~/lib'
import { useGlobal } from '#imports'
/**
* Hook for managing user sorts and sort configurations.
* @returns An object containing reactive values and functions related to user sorts.
*/
export function useUserSorts() {
const clone = rfdc()
const { user } = useGlobal()
const sorts = ref<UsersSortType[]>([])
// Key for storing user sort configurations in local storage
const userSortConfigKey = 'userSortConfig'
// Default user ID if no user found (fallback)
const defaultUserId = 'default'
/**
* Computed property that returns a record of sort directions based on the current sort configurations.
* @type {ComputedRef<Record<string, UsersSortType['direction']>>}
*/
const sortDirection: ComputedRef<Record<string, UsersSortType['direction']>> = computed(() => {
return sorts.value.reduce((acc, curr) => {
acc = { ...acc, [curr.field]: curr.direction }
return acc
}, {} as Record<string, UsersSortType['direction']>)
})
/**
* Loads user sort configurations from local storage based on the current user ID.
*/
function loadSorts(): void {
try {
// Retrieve sort configuration from local storage
const storedConfig = localStorage.getItem(userSortConfigKey)
const sortConfig = storedConfig ? JSON.parse(storedConfig) : {}
sorts.value = sortConfig
// Load user-specific sort configurations or default configurations
sorts.value = user.value?.id ? sortConfig[user.value.id] || [] : sortConfig[defaultUserId] || []
} catch (error) {
console.error('Error while retrieving sort configuration from local storage:', error)
// Set sorts to an empty array in case of an error
sorts.value = []
}
}
/**
* Saves or updates a user sort configuration and updates local storage.
* @param {UsersSortType} newSortConfig - The new sort configuration to save or update.
*/
function saveOrUpdate(newSortConfig: UsersSortType): void {
try {
const fieldIndex = sorts.value.findIndex((sort) => sort.field === newSortConfig.field)
if (newSortConfig.direction) {
if (fieldIndex !== -1) {
// Update the direction if the field exists
sorts.value = [
...clone(sorts.value).map((sort) => {
if (sort.field === newSortConfig.field) {
sort.direction = newSortConfig.direction
}
return sort
}),
]
} else {
// Add a new sort configuration if the field does not exist
sorts.value = [...clone(sorts.value), newSortConfig]
}
} else {
if (fieldIndex !== -1) {
// Remove the sort configuration if the field exists and direction is not present
sorts.value = [...clone(sorts.value).filter((sort) => sort.field !== newSortConfig.field)]
}
}
// Update local storage with the new sort configurations
const storedConfig = localStorage.getItem(userSortConfigKey)
const sortConfig = storedConfig ? JSON.parse(storedConfig) : {}
if (user.value?.id) {
// Save or delete user-specific sort configurations
if (sorts.value.length) {
sortConfig[user.value.id] = sorts.value
} else {
delete sortConfig[user.value.id]
}
} else {
// Save or delete default user sort configurations
sortConfig[defaultUserId] = sorts.value
}
localStorage.setItem(userSortConfigKey, JSON.stringify(sortConfig))
} catch (error) {
console.error('Error while retrieving sort configuration from local storage:', error)
}
}
/**
* Sorts and returns a deep copy of an array of objects based on the provided sort configurations.
*
* @param data - The array of objects to be sorted.
* @param sortsConfig - The array of sort configurations.
* @returns A new array containing sorted objects.
* @template T - The type of objects in the input array.
*/
function handleGetSortsData<T extends Record<string, any>>(data: T[], sortsConfig: UsersSortType[] = sorts.value): T[] {
const sortedData = clone(data).sort((a, b) => {
let sortCondition = 0
for (const { field, direction } of sortsConfig) {
if (a[field]) continue
if (direction === 'asc') {
sortCondition = sortCondition || a[field]?.localeCompare(b[field])
} else if (direction === 'desc') {
sortCondition = sortCondition || b[field]?.localeCompare(a[field])
}
}
return sortCondition
})
return sortedData
}
return { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortsData }
}

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

@ -189,7 +189,8 @@
"shift": "Shift",
"enter": "Enter",
"seconds": "Seconds",
"paste": "Paste"
"paste": "Paste",
"resetSort": "Reset Sort"
},
"objects": {
"workspace": "Workspace",
@ -1212,7 +1213,6 @@
"thankYou": "Thank you!",
"submittedFormData": "You have successfully submitted the form data.",
"editingSystemKeyNotSupported": "Editing system key not supported"
},
"error": {
"nameRequired": "Name Required",

6
packages/nc-gui/lib/types.ts

@ -176,6 +176,11 @@ interface SidebarTableNode extends TableType {
isViewsLoading?: boolean
}
interface UsersSortType {
field: string
direction?: 'asc' | 'desc'
}
export type {
User,
ProjectMetaInfo,
@ -201,4 +206,5 @@ export type {
ViewPageType,
NcButtonSize,
SidebarTableNode,
UsersSortType,
}

Loading…
Cancel
Save