From e03edfe966f6ba67dee6e0d33c27c01103205122 Mon Sep 17 00:00:00 2001 From: rgermain Date: Wed, 10 Dec 2025 13:23:59 +0100 Subject: [PATCH 01/15] dynamic module --- src/api/groups.service.ts | 6 +- src/components/group/GroupPageInnerV2.vue | 547 ++++++++---------- src/models/invitation.model.ts | 4 + .../GroupPageV2/Tabs/GroupSnapshotTab.vue | 187 ++---- src/pages/GroupPageV2/Tabs/GroupTabs.vue | 128 ---- .../Tabs/previews/BaseGroupPreview.vue | 23 + .../Tabs/previews/GroupProjectsPreview.vue | 69 +++ .../Tabs/previews/GroupUserPreview.vue | 59 ++ 8 files changed, 427 insertions(+), 596 deletions(-) delete mode 100644 src/pages/GroupPageV2/Tabs/GroupTabs.vue create mode 100644 src/pages/GroupPageV2/Tabs/previews/BaseGroupPreview.vue create mode 100644 src/pages/GroupPageV2/Tabs/previews/GroupProjectsPreview.vue create mode 100644 src/pages/GroupPageV2/Tabs/previews/GroupUserPreview.vue diff --git a/src/api/groups.service.ts b/src/api/groups.service.ts index 97715d03..ad091243 100644 --- a/src/api/groups.service.ts +++ b/src/api/groups.service.ts @@ -52,7 +52,7 @@ export async function addParentGroup( return await useAPI(`organization/${orgId}/people-group/${groupId}/`, { body, method: 'PATCH' }) //.data.value } -export async function getGroup(org: string, groupId: string, noError: boolean = false) { +export async function getGroup(org: string, groupId: number, noError: boolean = false) { return await useAPI( `/organization/${org}/people-group/${groupId}/`, { noError: noError } // TODO nuxt check error silenced @@ -72,7 +72,7 @@ export async function deleteGroup(org_code, group_id) { // GROUP MEMBERS -export async function getGroupMember(org: string, groupId: string, noError: boolean = false) { +export async function getGroupMember(org: string, groupId: number, noError: boolean = false) { return ( // TODO nuxt check error silenced await useAPI(`organization/${org}/people-group/${groupId}/member/`, { noError: noError }) @@ -104,7 +104,7 @@ export async function removeGroupMember( // GROUP PROJECTS -export async function getGroupProject(org: string, groupId: string, noError: boolean = false) { +export async function getGroupProject(org: string, groupId: number, noError: boolean = false) { return await useAPI(`organization/${org}/people-group/${groupId}/project/`, { noError: noError, }) //.data.value diff --git a/src/components/group/GroupPageInnerV2.vue b/src/components/group/GroupPageInnerV2.vue index 11c4f788..503527c3 100644 --- a/src/components/group/GroupPageInnerV2.vue +++ b/src/components/group/GroupPageInnerV2.vue @@ -38,351 +38,274 @@ - - diff --git a/src/models/invitation.model.ts b/src/models/invitation.model.ts index 084c34c1..d20ef359 100644 --- a/src/models/invitation.model.ts +++ b/src/models/invitation.model.ts @@ -6,6 +6,10 @@ export interface PeopleGroupModel { email: string type: string header_image: string + modules: { + members: number + projects: number + } } export interface ProfilePictureVariationsModel { diff --git a/src/pages/GroupPageV2/Tabs/GroupSnapshotTab.vue b/src/pages/GroupPageV2/Tabs/GroupSnapshotTab.vue index 989748f1..863e2aca 100644 --- a/src/pages/GroupPageV2/Tabs/GroupSnapshotTab.vue +++ b/src/pages/GroupPageV2/Tabs/GroupSnapshotTab.vue @@ -1,17 +1,21 @@ - diff --git a/src/pages/GroupPageV2/Tabs/GroupTabs.vue b/src/pages/GroupPageV2/Tabs/GroupTabs.vue deleted file mode 100644 index 238399ad..00000000 --- a/src/pages/GroupPageV2/Tabs/GroupTabs.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - diff --git a/src/pages/GroupPageV2/Tabs/previews/BaseGroupPreview.vue b/src/pages/GroupPageV2/Tabs/previews/BaseGroupPreview.vue new file mode 100644 index 00000000..0fe229da --- /dev/null +++ b/src/pages/GroupPageV2/Tabs/previews/BaseGroupPreview.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/pages/GroupPageV2/Tabs/previews/GroupProjectsPreview.vue b/src/pages/GroupPageV2/Tabs/previews/GroupProjectsPreview.vue new file mode 100644 index 00000000..9ee51a15 --- /dev/null +++ b/src/pages/GroupPageV2/Tabs/previews/GroupProjectsPreview.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/pages/GroupPageV2/Tabs/previews/GroupUserPreview.vue b/src/pages/GroupPageV2/Tabs/previews/GroupUserPreview.vue new file mode 100644 index 00000000..22e29743 --- /dev/null +++ b/src/pages/GroupPageV2/Tabs/previews/GroupUserPreview.vue @@ -0,0 +1,59 @@ + + + From 60d20f92972f1df054894b4fad2cc57d1e839f71 Mon Sep 17 00:00:00 2001 From: rgermain Date: Tue, 16 Dec 2025 10:49:48 +0100 Subject: [PATCH 02/15] pre-add --- src/api/groups.service.ts | 29 ++-- .../group/GroupForm/GroupTeamSection.vue | 1 + src/components/group/GroupPageInnerV2.vue | 104 ++----------- .../GroupTeamDrawer/GroupTeamDrawer.vue | 2 +- src/composables/useAPI.ts | 9 +- src/composables/useAPI2.ts | 4 +- src/composables/useAsyncAPI.ts | 124 +++++++++++++++ src/composables/useLoadingFromStatus.ts | 16 ++ src/composables/usePagination.ts | 2 +- src/models/group.model.ts | 6 +- src/models/invitation.model.ts | 13 +- .../CreateEditGroupPage.vue | 7 +- .../GroupPageV2/Tabs/GroupMembersTab.vue | 147 ++++-------------- .../GroupPageV2/Tabs/GroupSnapshotTab.vue | 77 +++------ .../Tabs/previews/GroupProjectsPreview.vue | 76 +++------ .../Tabs/previews/GroupUserPreview.vue | 85 +++++----- 16 files changed, 323 insertions(+), 379 deletions(-) create mode 100644 src/composables/useAsyncAPI.ts create mode 100644 src/composables/useLoadingFromStatus.ts diff --git a/src/api/groups.service.ts b/src/api/groups.service.ts index ad091243..d9af0165 100644 --- a/src/api/groups.service.ts +++ b/src/api/groups.service.ts @@ -7,11 +7,14 @@ import type { PostGroupProjects, RemoveGroupProject, AddParentGroupModelInput, + GroupMember, + GroupModel, } from '@/models/group.model' // import type { HierarchyGroupModel } from '@/models/group.model' // import type { APIResponseList } from '@/api/types' import { _adaptParamsToGetQuery } from '@/api/utils.service' import useAPI from '@/composables/useAPI' +import { Project } from '@playwright/test' // HIERARCHY @@ -52,14 +55,11 @@ export async function addParentGroup( return await useAPI(`organization/${orgId}/people-group/${groupId}/`, { body, method: 'PATCH' }) //.data.value } -export async function getGroup(org: string, groupId: number, noError: boolean = false) { - return await useAPI( - `/organization/${org}/people-group/${groupId}/`, - { noError: noError } // TODO nuxt check error silenced - ) //.data.value +export function getGroup(organizationCode: string, groupId: string) { + return useAPI2(`organization/${organizationCode}/people-group/${groupId}/`) } -export async function patchGroup(org: string, group_id: number, groupData: Partial) { +export async function patchGroup(org: string, group_id: string, groupData: Partial) { return await useAPI(`organization/${org}/people-group/${group_id}/`, { body: groupData, method: 'PATCH', @@ -72,12 +72,8 @@ export async function deleteGroup(org_code, group_id) { // GROUP MEMBERS -export async function getGroupMember(org: string, groupId: number, noError: boolean = false) { - return ( - // TODO nuxt check error silenced - await useAPI(`organization/${org}/people-group/${groupId}/member/`, { noError: noError }) - //.data.value - ) +export function getGroupMember(org: string, groupId: number, config = {}) { + return useAPI(`organization/${org}/people-group/${groupId}/member/`, config) } export async function postGroupMembers( @@ -104,10 +100,11 @@ export async function removeGroupMember( // GROUP PROJECTS -export async function getGroupProject(org: string, groupId: number, noError: boolean = false) { - return await useAPI(`organization/${org}/people-group/${groupId}/project/`, { - noError: noError, - }) //.data.value +export function getGroupProject(org: string, groupId: number, config = {}) { + return useAPI2>( + `organization/${org}/people-group/${groupId}/project/`, + config + ) } export async function postGroupProjects( diff --git a/src/components/group/GroupForm/GroupTeamSection.vue b/src/components/group/GroupForm/GroupTeamSection.vue index 33ca36e7..47fdbf11 100644 --- a/src/components/group/GroupForm/GroupTeamSection.vue +++ b/src/components/group/GroupForm/GroupTeamSection.vue @@ -153,6 +153,7 @@ export default { return null }, openDrawer(mode) { + console.log(this.teamModalMode) this.teamModalMode = mode this.teamModalVisible = true }, diff --git a/src/components/group/GroupPageInnerV2.vue b/src/components/group/GroupPageInnerV2.vue index 503527c3..3b342525 100644 --- a/src/components/group/GroupPageInnerV2.vue +++ b/src/components/group/GroupPageInnerV2.vue @@ -31,8 +31,8 @@ /> @@ -42,43 +42,29 @@ diff --git a/src/composables/useAPI.ts b/src/composables/useAPI.ts index e8847fa9..1b453515 100644 --- a/src/composables/useAPI.ts +++ b/src/composables/useAPI.ts @@ -16,11 +16,10 @@ export const defaultOptions = () => { method: 'GET', headers, onRequest({ options }) { - // if (import.meta.client) { - const accessToken = usersStore.accessToken // localStorage?.getItem('ACCESS_TOKEN') - console.log(accessToken) - if (accessToken) options.headers.set('Authorization', `Bearer ${accessToken}`) - // } + if (import.meta.client) { + const accessToken = usersStore.accessToken // localStorage?.getItem('ACCESS_TOKEN') + if (accessToken) options.headers.set('Authorization', `Bearer ${accessToken}`) + } }, onRequestError() { // Handle the request errors diff --git a/src/composables/useAsyncAPI.ts b/src/composables/useAsyncAPI.ts index 47928245..23c557ee 100644 --- a/src/composables/useAsyncAPI.ts +++ b/src/composables/useAsyncAPI.ts @@ -67,7 +67,7 @@ type asyncPaginationAPI = Omit< 'data' > & { pagination: Pagination - data: Result | Ref + data: Result extends undefined ? Ref : Result } /** @@ -79,17 +79,15 @@ type asyncPaginationAPI = Omit< * @kind variable * @exports */ -export const useAsyncPaginationAPI = ( +export function useAsyncPaginationAPI( ...params: AsyncPaginationParameters -): asyncPaginationAPI => { +): asyncPaginationAPI { const paginationData = useState() const pagination = usePagination(paginationData, params[2]?.paginationConfig) const config = defaultOptions() - // pass all arguements, but override the transform data const { data, ...rest } = useAsyncAPI, DataT[]>( params[0], - // override handler to add pagination query (ctx) => { return params[1]({ ctx, @@ -100,19 +98,14 @@ export const useAsyncPaginationAPI = ( }) }, { - // add all params without transform ...((omit(params[2] ?? {}, ['transform']) ?? {}) as AsyncDataConfig< PaginationResult, DataT[] >), - // add page watcher watch: [...(params[2]?.watch || []), pagination.current], - default() { - // return by defaults a emptys arrays return params[2]?.default?.() || [] }, - transform(dataApi) { paginationData.value = dataApi const results = dataApi.results @@ -121,12 +114,12 @@ export const useAsyncPaginationAPI = ( } ) - // translate results - const dataWrapped = params[2]?.translate?.(data) || data + const dataWrapped = (params[2]?.translate?.(data) as Result) || (data as Ref) return { ...rest, pagination, + // @ts-expect-error dynamic type from translate function or raw data data: dataWrapped, } } diff --git a/src/composables/useAutoTranslate.ts b/src/composables/useAutoTranslate.ts index 62b2a030..8ca35e50 100644 --- a/src/composables/useAutoTranslate.ts +++ b/src/composables/useAutoTranslate.ts @@ -117,9 +117,10 @@ export default function useAutoTranslate() { // -------------------- // People - const translateUser = (user) => - translateEntity(user, ['description', 'short_description', 'job']) - const translateUsers = (users) => translateEntities(users, translateUser) + const translateUser = (user) => + translateEntity(user, ['description', 'short_description', 'job']) + const translateUsers = (users) => + translateEntities(users, translateUser) const translateTeam = (team) => computed(() => { diff --git a/src/composables/useForm.ts b/src/composables/useForm.ts index a7898e31..0e7180f2 100644 --- a/src/composables/useForm.ts +++ b/src/composables/useForm.ts @@ -29,13 +29,12 @@ const onClean = (d) => d const useForm = ( options: OptionsForm = { onClean } ): UseFormResult => { - const form = ref({ ...(options.default ?? {}) } as T) + const form = ref({ ...(options.default ?? {}) } as T) as Ref const _onClean = options.onClean ?? onClean const isValid = ref(false) const v$ = useValidate(options.rules ?? {}, form) - // debounce validate to optimize check const validate = () => v$.value.$validate().then((v) => (isValid.value = v)) const debounceValidate = debounce(validate, options.validateTimeout ?? 200) watch(form, () => debounceValidate(), { deep: true, immediate: true }) @@ -50,7 +49,6 @@ const useForm = ( return err }) - // clean data (before send to backend) const cleanedData = computed(() => { if (!isValid.value) { return null diff --git a/src/composables/useGroupProjectsUpdate.ts b/src/composables/useGroupProjectsUpdate.ts index 8a66abe3..790cfeae 100644 --- a/src/composables/useGroupProjectsUpdate.ts +++ b/src/composables/useGroupProjectsUpdate.ts @@ -47,6 +47,11 @@ export default function useGroupProjectsUpdate(orgCode, groupId, form) { await removeGroupProject(orgCode.value, groupId, payloadProjects as any) } isSaving.value = false + + return { + removed: projectsToRemove.length, + added: projectsToAdd.length, + } } return { groupProjectData, isSaving, setProjectsData, updateGroupProjects } diff --git a/src/composables/useLoadingFromStatus.ts b/src/composables/useLoadingFromStatus.ts index f137e098..4753e5e9 100644 --- a/src/composables/useLoadingFromStatus.ts +++ b/src/composables/useLoadingFromStatus.ts @@ -7,8 +7,8 @@ * @param {any} status * @returns {globalThis.ComputedRef} */ -const useLoadingFromStatus = (status) => { - const loading = computed(() => ['idle', 'pending'].includes(status)) +const useLoadingFromStatus = (status: Ref<'pending' | 'error' | 'success' | 'idle'>) => { + const loading = computed(() => ['idle', 'pending'].includes(status.value)) return loading } diff --git a/src/composables/useLpiHead.ts b/src/composables/useLpiHead.ts index 572ad511..6faa2e67 100644 --- a/src/composables/useLpiHead.ts +++ b/src/composables/useLpiHead.ts @@ -1,6 +1,7 @@ import useNuxtI18n from '@/composables/useNuxtI18n' +import { ImageModel } from '@/models/image.model' -export default (url, _title, _description, image, dimensions = null) => { +const useLpiHead = (url, _title, _description, image, dimensions = null) => { const runtimeConfig = useRuntimeConfig() const { locale } = useNuxtI18n() @@ -115,3 +116,37 @@ export default (url, _title, _description, image, dimensions = null) => { watch(() => [locale.value], setHead) } + +export default useLpiHead + +type OptionsHead = { + url?: string + title?: string + image?: ImageModel + description?: string +} + +/** + * default value for useLpiHead + * + * @function + * @name useLpiHead2 + * @kind variable + * @param {OptionsHead} options + * @returns {void} + * @exports + */ +export const useLpiHead2 = (options: OptionsHead) => { + const url = options.url ?? useRequestURL().toString() + const title = options.title ?? '' + const description = options.description ?? '' + + let [image, dimensions] = [null, null] + if (typeof options.image === 'object') { + const tmp = useImageAndDimension(options.image, 'medium') + image = tmp.image + dimensions = tmp.dimensions + } + + useLpiHead(url, title, description, image, dimensions) +} diff --git a/src/composables/usePagination.ts b/src/composables/usePagination.ts index dcb37404..ca049ead 100644 --- a/src/composables/usePagination.ts +++ b/src/composables/usePagination.ts @@ -37,7 +37,7 @@ export type PaginationQuery = { offset?: number } -const DEFAULT_Pagination_LIMIT = 100 +const DEFAULT_PAGINATION_LIMIT = 10 export type paginationConfig = { limit?: number offset?: number @@ -68,7 +68,7 @@ export const usePagination = ( const current = useState(() => 1) const total = useState(() => 0) const count = useState(() => 0) - const limit = paginationConfig.limit ?? DEFAULT_Pagination_LIMIT + const limit = paginationConfig.limit ?? DEFAULT_PAGINATION_LIMIT watch( results, diff --git a/src/form/group.ts b/src/form/group.ts deleted file mode 100644 index 7b3e0ea5..00000000 --- a/src/form/group.ts +++ /dev/null @@ -1,17 +0,0 @@ -import useForm from '@/composables/useForm' -import { clone } from 'es-toolkit' - -const DEFAULT_FORM = { - featured_projects: [], -} - -const RULES = {} - -export const useGroupProjectForm = (...options) => { - const onClean = (data) => { - return { - featured_projects: data.featured_projects.map((el) => el.id), - } - } - return useForm({ default: clone(DEFAULT_FORM), rules: clone(RULES), onClean, ...options }) -} diff --git a/src/models/group.model.ts b/src/models/group.model.ts index a3b2cdaf..2693d70e 100644 --- a/src/models/group.model.ts +++ b/src/models/group.model.ts @@ -1,3 +1,4 @@ +import { TranslatedPeopleGroupModel } from '@/models/invitation.model' import { UserFromJWTModel, UserModel } from '@/models/user.model' /** @@ -16,6 +17,8 @@ export type GroupMember = UserModel & { is_manager: boolean } +export type TranslatedGroupMember = TranslatedPeopleGroupModel & GroupMember + export interface HierarchyGroupModel { id: number name: string diff --git a/src/pages/GroupPageV2/GroupPage.vue b/src/pages/GroupPageV2/GroupPage.vue index 4d6557c8..177c2c7e 100644 --- a/src/pages/GroupPageV2/GroupPage.vue +++ b/src/pages/GroupPageV2/GroupPage.vue @@ -1,27 +1,266 @@ - - + diff --git a/src/pages/GroupPageV2/Tabs/GroupMembersEditTab.vue b/src/pages/GroupPageV2/Tabs/GroupMembersEditTab.vue index c128205a..bf69cd83 100644 --- a/src/pages/GroupPageV2/Tabs/GroupMembersEditTab.vue +++ b/src/pages/GroupPageV2/Tabs/GroupMembersEditTab.vue @@ -4,12 +4,9 @@ import { TranslatedPeopleGroupModel } from '@/models/invitation.model' const props = defineProps<{ group: TranslatedPeopleGroupModel }>() const router = useRouter() -const organizationCode = useOrganizationCode() -const orgCode = computed(() => { - // use group's org code if availabe - // to allow edition of groups on the meta portal (PROJ-1032) - return props.group?.organization || organizationCode -}) +// use group's org code if availabe +// to allow edition of groups on the meta portal (PROJ-1032) +const orgCode = computed(() => props.group.organization || useOrganizationCode()) const form = ref({ members: [], @@ -23,36 +20,19 @@ const { setMembersData, updateGroupMembers, isSaving, groupMemberData } = useGro const { startEditWatcher, stopEditWatcher } = useEditWatcher(form) -const redirectMember = () => { - router.push({ - name: 'groupMembers', - params: { groupId: props.group.id }, - }) -} - -const redirectHome = () => { +const redirect = (numberMembers) => { router.push({ - name: 'groupSnapshot', + name: numberMembers ? 'groupMembers' : 'groupSnapshot', params: { groupId: props.group.id }, }) } -const redirect = (numberMembers) => { - if (numberMembers) { - redirectMember() - } else { - redirectHome() - } -} - const save = async () => { try { const { removed, added } = await updateGroupMembers() - console.log(removed, added, groupMemberData.value.length) - const actualMembersCount = groupMemberData.value.length - removed + added stopEditWatcher() await refreshNuxtData(`people-group::${props.group.id}`) - redirect(actualMembersCount) + redirect(groupMemberData.value.length - removed + added) } catch (e) { console.error(e) } diff --git a/src/pages/GroupPageV2/Tabs/GroupMembersTab.vue b/src/pages/GroupPageV2/Tabs/GroupMembersTab.vue index 60272d01..9aad4a42 100644 --- a/src/pages/GroupPageV2/Tabs/GroupMembersTab.vue +++ b/src/pages/GroupPageV2/Tabs/GroupMembersTab.vue @@ -7,7 +7,7 @@ ( {{ count }} ) - + () +const limitSkeletons = computed(() => Math.min(props.group.modules?.members ?? 10, 10)) + const { translateUsers } = useAutoTranslate() const organizationCode = useOrganizationCode() const key = computed(() => `group-${props.group.id}-members-tabs`) diff --git a/src/pages/GroupPageV2/Tabs/GroupProjectsEditTab.vue b/src/pages/GroupPageV2/Tabs/GroupProjectsEditTab.vue index fdc3435c..57d57bba 100644 --- a/src/pages/GroupPageV2/Tabs/GroupProjectsEditTab.vue +++ b/src/pages/GroupPageV2/Tabs/GroupProjectsEditTab.vue @@ -1,77 +1,51 @@