Skip to content

Commit

Permalink
feat: update user select dropdown (#5246)
Browse files Browse the repository at this point in the history
* chore: update translations

Signed-off-by: NaYeong,Kim <[email protected]>

* feat: create user group reference store

Signed-off-by: NaYeong,Kim <[email protected]>

* feat: update user select dropdown

Signed-off-by: NaYeong,Kim <[email protected]>

* chore: update translations

Signed-off-by: NaYeong,Kim <[email protected]>

* refactor: cleanup code(add type, apply props)

Signed-off-by: NaYeong,Kim <[email protected]>

* refactor: return type change

Signed-off-by: NaYeong,Kim <[email protected]>

---------

Signed-off-by: NaYeong,Kim <[email protected]>
  • Loading branch information
skdud4659 authored Dec 18, 2024
1 parent 9314a27 commit 87f51fb
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 76 deletions.
277 changes: 206 additions & 71 deletions apps/web/src/common/modules/user/UserSelectDropdown.vue
Original file line number Diff line number Diff line change
@@ -1,118 +1,190 @@
<script setup lang="ts">
import type { Ref } from 'vue';
import {
ref, computed, toRef, watch,
computed, reactive, watch,
} from 'vue';
import { isEqual } from 'lodash';
import { PSelectDropdown, getTextHighlightRegex } from '@cloudforet/mirinae';
import type { AutocompleteHandler, SelectDropdownMenuItem } from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type';
import {
getTextHighlightRegex, PAvatar, PSelectDropdown, PTag,
} from '@cloudforet/mirinae';
import type {
AutocompleteHandler,
SelectDropdownMenuItem,
} from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type';
import { i18n } from '@/translations';
import { useAllReferenceStore } from '@/store/reference/all-reference-store';
import type { UserGroupReferenceMap } from '@/store/reference/user-group-reference-store';
import type { UserReferenceMap } from '@/store/reference/user-reference-store';
import { useUserReferenceStore } from '@/store/reference/user-reference-store';
import ErrorHandler from '@/common/composables/error/errorHandler';
import type { SelectedUserDropdownIdsType } from '@/common/modules/user/typte';
import { indigo } from '@/styles/colors';
interface UserDropdownItem extends SelectDropdownMenuItem {
interface DropdownItem extends SelectDropdownMenuItem {
name: string;
label: string;
members?: number;
userName?: string;
}
type DropdownCategoriesType = {
key: string;
title: string;
};
const props = withDefaults(defineProps<{
userId?: string;
userIds?: string[];
selectedId?: SelectedUserDropdownIdsType;
selectedIds?: SelectedUserDropdownIdsType[];
selectionType?: 'single'|'multiple';
useFixedMenuStyle?: boolean;
invalid?: boolean;
disabled?: boolean;
readonly?: boolean;
userPool?: string[];
appearanceType?: 'badge'|'stack';
showUserList?: boolean;
showUserGroupList?: boolean;
showCategoryTitle?: boolean;
}>(), {
userId: '',
userIds: () => [],
selectedId: undefined,
selectedIds: undefined,
selectionType: 'single',
useFixedMenuStyle: false,
invalid: false,
disabled: false,
userPool: undefined,
appearanceType: 'badge',
showUserList: true,
showUserGroupList: true,
showCategoryTitle: true,
});
const emit = defineEmits<{(event: 'update:user-ids', value: string[]): void;
(event: 'update:user-id', value: string): void;
const emit = defineEmits<{(event: 'update:selected-ids', value: SelectedUserDropdownIdsType[]): void;
(event: 'update:selected-id', value?: SelectedUserDropdownIdsType): void;
}>();
const userReferenceStore = useUserReferenceStore();
const loading = computed(() => userReferenceStore.getters.loading);
const userReferenceMap: Ref<Readonly<UserReferenceMap>> = toRef(userReferenceStore.getters, 'userItems');
const allUserItems = computed<UserDropdownItem[]>(() => {
if (props.userPool && props.userPool.length > 0) {
return props.userPool.map((userId) => ({
name: userId,
label: userReferenceMap.value[userId]?.label ?? userId,
const allReferenceStore = useAllReferenceStore();
const allReferenceGetters = allReferenceStore.getters;
const storeState = reactive({
userReferenceMap: computed<UserReferenceMap>(() => allReferenceGetters.user),
userGroupReferenceMap: computed<UserGroupReferenceMap>(() => allReferenceGetters.user_group),
});
const state = reactive({
dropdownCategories: computed<DropdownCategoriesType[]>(() => {
const result: DropdownCategoriesType[] = [];
if (props.showUserList) {
result.push({
key: 'user',
title: i18n.t('COMMON.USER_SELECT_DROPDOWN.USER') as string,
});
}
if (props.showUserGroupList) {
result.push({
key: 'user_group',
title: i18n.t('COMMON.USER_SELECT_DROPDOWN.USER_GROUP') as string,
});
}
return result;
}),
allUserItems: computed<DropdownItem[]>(() => {
if (!props.showUserList) return [];
if (props.userPool && props.userPool.length > 0) {
return props.userPool.map((userId) => ({
name: userId,
label: storeState.userReferenceMap[userId]?.label ?? userId,
}));
}
return Object.values(storeState.userReferenceMap).map((u: UserReferenceMap[string]) => ({
name: u.key,
label: u.key,
userName: u.name,
}));
}
return Object.values(userReferenceMap.value).map((u: UserReferenceMap[string]) => ({
name: u.key,
label: u.label,
}));
}),
allUserGroupItems: computed<DropdownItem[]>(() => {
if (!props.showUserGroupList) return [];
return Object.values(storeState.userGroupReferenceMap).map((u: UserGroupReferenceMap[string]) => ({
name: u.key,
label: u.label,
members: u.data.users?.length,
}));
}),
selectedItems: [] as SelectDropdownMenuItem[],
});
const selectedUserItems = ref<SelectDropdownMenuItem[]>([]);
const userMenuItemsHandler: AutocompleteHandler = async (keyword: string, pageStart = 1, pageLimit = 10) => {
const filteredItems = allUserItems.value.filter((item) => getTextHighlightRegex(keyword).test(item.label));
const menuItemsHandler = (): AutocompleteHandler => async (keyword: string, pageStart = 1, pageLimit = 10, filters, resultIndex) => {
const _totalCount = pageStart - 1 + pageLimit;
const _slicedResults = filteredItems.slice(pageStart - 1, _totalCount);
return {
results: _slicedResults,
more: _totalCount < filteredItems.length,
};
const filterItems = (items: DropdownItem[]) => items.filter((item) => getTextHighlightRegex(keyword).test(item.name)).slice(pageStart - 1, _totalCount);
if (resultIndex === undefined) {
return state.dropdownCategories.map((c, idx) => {
const items = c.key === 'user' ? state.allUserItems : state.allUserGroupItems;
const _slicedItems = filterItems(items);
if (props.showCategoryTitle) {
_slicedItems.unshift({ type: 'header', label: c.title, name: 'header' });
}
if (idx !== 0) {
_slicedItems.unshift({ type: 'divider', label: '', name: '' });
}
return {
results: _slicedItems,
more: _totalCount < items.length,
};
});
}
return state.dropdownCategories.map((c, i) => {
const items = c.key === 'user' ? state.allUserItems : state.allUserGroupItems;
const _slicedItems = filterItems(items);
if (i !== resultIndex) return { results: [], title: c.title };
return {
results: _slicedItems,
more: _totalCount < items.length,
};
});
};
const currentUserIds = computed<string[]>(() => selectedUserItems.value.map((item) => item.name));
const currentUserId = computed<string | undefined>(() => currentUserIds.value[0]);
const currentUserIds = computed<SelectedUserDropdownIdsType[]>(() => state.selectedItems.map((item) => ({ value: item.name, type: checkUserGroup(item.name) ? 'USER_GROUP' : 'USER' })));
const currentUserId = computed<SelectedUserDropdownIdsType|undefined>(() => ({ value: state.selectedItems[0]?.name, type: checkUserGroup(state.selectedItems[0]?.name) ? 'USER_GROUP' : 'USER' }));
const handleUpdateSelectedUserItems = (selectedUsers: SelectDropdownMenuItem[]) => {
if (isEqual(selectedUsers, selectedUserItems.value)) return; // prevent unnecessary update
selectedUserItems.value = selectedUsers; // it updates currentUserId and currentUserIds automatically
if (isEqual(selectedUsers, state.selectedItems)) return; // prevent unnecessary update
state.selectedItems = selectedUsers; // it updates currentUserId and currentUserIds automatically
if (props.selectionType === 'single') {
if (currentUserId.value === props.userId) return; // prevent unnecessary update
emit('update:user-id', selectedUsers[0]?.name ?? '');
if (currentUserId?.value === props.selectedId) return; // prevent unnecessary update
emit('update:selected-id', currentUserId?.value);
} else {
if (isEqual(currentUserIds.value, props.userIds)) return; // prevent unnecessary update
emit('update:user-ids', currentUserIds.value);
if (isEqual(currentUserIds.value, props.selectedIds)) return; // prevent unnecessary update
emit('update:selected-ids', currentUserIds.value);
}
};
const initSingleType = (_userId?: string) => {
if (currentUserId.value === _userId) {
return;
const handleTagDelete = (idx: number) => {
state.selectedItems.splice(idx, 1);
emit('update:selected-ids', currentUserIds.value);
};
const initSingleType = (_userId?: SelectedUserDropdownIdsType) => {
if (currentUserId?.value !== _userId?.value) {
state.selectedItems = _userId?.value
? [{ name: _userId?.value, label: storeState.userReferenceMap[_userId?.value]?.label ?? _userId?.value }]
: [];
}
selectedUserItems.value = _userId ? [{
name: _userId,
label: userReferenceMap.value[_userId]?.label ?? _userId,
}] : [];
};
const initMultipleType = (_userIds?: string[]) => {
try {
if (!Array.isArray(_userIds)) {
throw new Error('userIds should be an array');
}
if (isEqual(currentUserIds.value, _userIds)) {
return;
}
selectedUserItems.value = _userIds.map((userId) => ({
name: userId,
label: userReferenceMap.value[userId]?.label ?? userId,
selected: true,
const initMultipleType = (_userIds?: SelectedUserDropdownIdsType[]) => {
if (!Array.isArray(_userIds)) throw new Error('userIds should be an array');
if (!isEqual(currentUserIds.value, _userIds)) {
state.selectedItems = _userIds.map((userId) => ({
name: userId.value,
label: storeState.userReferenceMap[userId.value]?.label ?? userId.value,
}));
} catch (e) {
ErrorHandler.handleError(e);
}
};
const checkUserGroup = (id: string): boolean => state.allUserGroupItems.some((i) => i.name === id);
watch([loading, () => props.userId, () => props.userIds], ([_loading, newUserId, newUserIds]) => {
if (_loading) return;
watch([() => props.selectedId, () => props.selectedIds], ([newUserId, newUserIds]) => {
if (props.selectionType === 'single') {
if (currentUserId.value === newUserId) return; // prevent infinite loop
if (currentUserId?.value === newUserId) return; // prevent infinite loop
initSingleType(newUserId);
} else {
if (isEqual(currentUserIds.value, newUserIds)) return; // prevent infinite loop
Expand All @@ -123,16 +195,79 @@ watch([loading, () => props.userId, () => props.userIds], ([_loading, newUserId,
<template>
<p-select-dropdown show-select-marker
:selected="selectedUserItems"
:handler="userMenuItemsHandler"
class="user-select-dropdown"
:selected="state.selectedItems"
:handler="menuItemsHandler()"
is-filterable
:invalid="props.invalid"
:disabled="props.disabled"
:readonly="props.readonly"
:use-fixed-menu-style="useFixedMenuStyle"
page-size="10"
:use-fixed-menu-style="props.useFixedMenuStyle"
show-delete-all-button
:multi-selectable="props.selectionType === 'multiple'"
appearance-type="badge"
:appearance-type="props.appearanceType"
@update:selected="handleUpdateSelectedUserItems"
/>
>
<template v-if="props.appearanceType === 'stack'"
#dropdown-button
>
<div class="flex flex-wrap py-1 gap-y-2">
<p-tag v-for="(item, idx) in state.selectedItems"
:key="item.name"
:outline="!checkUserGroup(item.name)"
class="tag border-none"
selected
:deletable="!props.disabled"
@delete="handleTagDelete(idx)"
>
<div class="member-menu-item h-4">
<p-avatar v-if="checkUserGroup(item.name)"
icon="ic_member"
:color="indigo[300]"
size="xs"
/>
<p-avatar v-else
size="xs"
/>
<span class="leading-4">{{ item.label }}</span>
</div>
</p-tag>
</div>
</template>
<template #menu-item--format="{ item }">
<div class="member-menu-item">
<div>
<p-avatar v-if="checkUserGroup(item.name)"
class="menu-icon"
icon="ic_member"
:color="indigo[300]"
size="xs"
/>
<p-avatar v-else
class="menu-icon"
size="xs"
/>
</div>
<span>{{ item.label }}</span>
<span class="text-gray-500">
<span v-if="checkUserGroup(item.name)">({{ item?.members || 0 }} {{ $t('ALERT_MANAGER.ALERTS.MEMBERS') }})</span>
<span v-else-if="item?.userName">({{ item?.userName }})</span>
</span>
</div>
</template>
</p-select-dropdown>
</template>
<style scoped lang="postcss">
.user-select-dropdown {
.member-menu-item {
@apply flex items-center;
gap: 0.25rem;
}
.tag {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
}
</style>
4 changes: 4 additions & 0 deletions apps/web/src/common/modules/user/typte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SelectedUserDropdownIdsType = {
value: string;
type: 'USER'|'USER_GROUP';
};
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import RoleVariableModel from '@/lib/variable-models/managed-model/resource-mode
import SecretVariableModel from '@/lib/variable-models/managed-model/resource-model/secret-variable-model';
import ServiceAccountVariableModel from '@/lib/variable-models/managed-model/resource-model/service-account-variable-model';
import UnifiedCostVariableModel from '@/lib/variable-models/managed-model/resource-model/unified-cost-variable-model';
import UserGroupVariableModel from '@/lib/variable-models/managed-model/resource-model/user-group-variable-model';
import UserVariableModel from '@/lib/variable-models/managed-model/resource-model/user-variable-model';
import WebhookVariableModel from '@/lib/variable-models/managed-model/resource-model/webhook-variable-model';
import WorkspaceGroupVariableModel from '@/lib/variable-models/managed-model/resource-model/workspace-group-variable-model';
Expand All @@ -47,6 +48,7 @@ const RESOURCE_VARIABLE_MODELS = {
secret: SecretVariableModel,
service_account: ServiceAccountVariableModel,
user: UserVariableModel,
user_group: UserGroupVariableModel,
webhook: WebhookVariableModel,
workspace: WorkspaceVariableModel,
workspace_group: WorkspaceGroupVariableModel,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { UserGroupModel } from '@/schema/identity/user-group/model';

import ResourceVariableModel from '@/lib/variable-models/_base/resource-variable-model';
import type { VariableModelConstructorConfig } from '@/lib/variable-models/_base/types';


export default class UserGroupVariableModel extends ResourceVariableModel<UserGroupModel> {
static meta = {
key: 'user_group',
name: 'User Group',
resourceType: 'identity.UserGroup',
idKey: 'user_group_id',
nameKey: 'name',
};

constructor(config: VariableModelConstructorConfig = {}) {
super(config);
this._meta = UserGroupVariableModel.meta;
}
}
Loading

0 comments on commit 87f51fb

Please sign in to comment.