Skip to content

Commit 3ff5798

Browse files
authored
feat: repository groups editing (#3381)
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
1 parent d55695a commit 3ff5798

13 files changed

Lines changed: 555 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE public."repositoryGroups"
2+
REPLICA IDENTITY DEFAULT;
3+
ALTER PUBLICATION sequin_pub DROP TABLE "repositoryGroups";
4+
5+
DROP INDEX IF EXISTS "ix_repositoryGroups_updatedAt_id";
6+
7+
DROP TABLE IF EXISTS "repositoryGroups";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE TABLE IF NOT EXISTS "repositoryGroups"
2+
(
3+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4+
name VARCHAR(255) NOT NULL,
5+
slug VARCHAR(255) NOT NULL,
6+
"repositories" VARCHAR[] DEFAULT ARRAY []::VARCHAR[],
7+
"insightsProjectId" UUID NOT NULL,
8+
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
9+
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
10+
"deletedAt" TIMESTAMP NULL DEFAULT NULL,
11+
foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade,
12+
UNIQUE (slug, "insightsProjectId", "deletedAt")
13+
);
14+
15+
create index "ix_repositoryGroups_updatedAt_id" on "repositoryGroups" ("updatedAt", id);
16+
17+
ALTER PUBLICATION sequin_pub ADD TABLE "repositoryGroups";
18+
ALTER TABLE public."repositoryGroups" REPLICA IDENTITY FULL;

backend/src/services/collectionService.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { uniq } from 'lodash'
22

33
import { getCleanString } from '@crowd/common'
4+
import { QueryExecutor } from '@crowd/data-access-layer'
45
import { listCategoriesByIds } from '@crowd/data-access-layer/src/categories'
56
import {
67
CollectionField,
@@ -30,6 +31,14 @@ import {
3031
} from '@crowd/data-access-layer/src/integrations'
3132
import { OrganizationField, findOrgById, queryOrgs } from '@crowd/data-access-layer/src/orgs'
3233
import { QueryFilter } from '@crowd/data-access-layer/src/query'
34+
import {
35+
ICreateRepositoryGroup,
36+
IRepositoryGroup,
37+
createRepositoryGroup,
38+
deleteRepositoryGroup,
39+
listRepositoryGroups,
40+
updateRepositoryGroup,
41+
} from '@crowd/data-access-layer/src/repositoryGroups'
3342
import { findSegmentById } from '@crowd/data-access-layer/src/segments'
3443
import { QueryResult } from '@crowd/data-access-layer/src/utils'
3544
import { GithubIntegrationSettings } from '@crowd/integrations'
@@ -241,6 +250,10 @@ export class CollectionService extends LoggerBase {
241250
)
242251
}
243252

253+
if (project.repositoryGroups) {
254+
await this.syncRepositoryGroupsWithDb(qx, createdProject.id, project.repositoryGroups)
255+
}
256+
244257
const txSvc = new CollectionService({ ...this.options, transaction: tx })
245258

246259
return txSvc.findInsightsProjectById(createdProject.id)
@@ -281,6 +294,7 @@ export class CollectionService extends LoggerBase {
281294
fields: Object.values(CollectionField),
282295
})
283296
: []
297+
const repositoryGroups = await listRepositoryGroups(qx, { insightsProjectId: id })
284298

285299
return {
286300
...project,
@@ -296,6 +310,7 @@ export class CollectionService extends LoggerBase {
296310
displayName: organization?.displayName,
297311
logo: organization?.logo,
298312
},
313+
repositoryGroups,
299314
}
300315
})
301316
}
@@ -400,6 +415,9 @@ export class CollectionService extends LoggerBase {
400415
})),
401416
)
402417
}
418+
if (project.repositoryGroups) {
419+
await this.syncRepositoryGroupsWithDb(qx, insightsProjectId, project.repositoryGroups)
420+
}
403421

404422
const txSvc = new CollectionService({
405423
...this.options,
@@ -409,6 +427,76 @@ export class CollectionService extends LoggerBase {
409427
})
410428
}
411429

430+
/**
431+
* Synchronizes repository groups with the database by creating, updating, or deleting groups based on the provided input.
432+
*
433+
* @param {QueryExecutor} qx - The query executor used to perform database operations.
434+
* @param {string} insightsProjectId - The ID of the insights project to which the repository groups belong.
435+
* @param {ICreateRepositoryGroup[]} repositoryGroups - The array of repository group objects to be synchronized with the database.
436+
* @return {Promise<IRepositoryGroup[]>} A promise that resolves to the list of repository groups currently in the database after synchronization.
437+
*/
438+
// eslint-disable-next-line class-methods-use-this
439+
async syncRepositoryGroupsWithDb(
440+
qx: QueryExecutor,
441+
insightsProjectId: string,
442+
repositoryGroups: ICreateRepositoryGroup[],
443+
): Promise<IRepositoryGroup[]> {
444+
// Get existing repository groups for the given insights project
445+
const existingRepositoryGroups = await listRepositoryGroups(qx, { insightsProjectId })
446+
447+
// Extract IDs of existing repository groups
448+
const existingIds: string[] = existingRepositoryGroups.map((rg) => rg.id)
449+
450+
// Extract IDs of repository groups to be synchronized
451+
const repositoryGroupIds: string[] = repositoryGroups.map((rg) => rg.id) as string[]
452+
453+
// Find repository groups that need to be updated (exist in both lists)
454+
const toUpdate: ICreateRepositoryGroup[] = repositoryGroups.filter((rg) =>
455+
existingIds.includes(rg.id),
456+
)
457+
458+
// Find repository groups that need to be created (don't exist or have no ID)
459+
const toCreate: ICreateRepositoryGroup[] = repositoryGroups.filter(
460+
(rg) => !rg.id || !existingIds.includes(rg.id),
461+
)
462+
463+
// Find repository groups that need to be deleted (exist but not in new list)
464+
const toDelete: string[] = existingIds.filter((id) => !repositoryGroupIds.includes(id))
465+
466+
// Create new repository groups
467+
if (toCreate.length > 0) {
468+
for (const rg of toCreate) {
469+
const slug = getCleanString(rg.name).replace(/\s+/g, '-')
470+
await createRepositoryGroup(qx, {
471+
...rg,
472+
slug,
473+
insightsProjectId,
474+
})
475+
}
476+
}
477+
478+
// Delete repository groups that are no longer needed
479+
if (toDelete.length > 0) {
480+
for (const id of toDelete) {
481+
await deleteRepositoryGroup(qx, id)
482+
}
483+
}
484+
485+
// Update existing repository groups with new data
486+
if (toUpdate.length > 0) {
487+
for (const rg of toUpdate) {
488+
const slug = getCleanString(rg.name).replace(/\s+/g, '-')
489+
await updateRepositoryGroup(qx, rg.id, {
490+
...rg,
491+
slug,
492+
})
493+
}
494+
}
495+
496+
// Return the updated list of repository groups from the database
497+
return listRepositoryGroups(qx, { insightsProjectId })
498+
}
499+
412500
async findRepositoriesForSegment(segmentId: string) {
413501
return SequelizeRepository.withTx(this.options, async (tx) => {
414502
const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx })
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<template>
2+
<div>
3+
<div v-if="cForm.repositoryGroups.length > 0">
4+
<div class="py-2 px-6 bg-gray-50 border-b border-gray-100 -mx-6">
5+
<lf-button type="primary-ghost" size="small" @click="add()">
6+
<lf-icon name="plus" />
7+
Add repository group
8+
</lf-button>
9+
</div>
10+
<div class="py-3">
11+
<article
12+
v-for="(group, gi) of cForm.repositoryGroups"
13+
:key="gi"
14+
class="flex justify-between items-center border-t first:border-t-0 border-gray-100 py-2.5"
15+
>
16+
<p class="text-medium">
17+
{{ group.name }}
18+
</p>
19+
<div class="flex items-center gap-4">
20+
<lf-badge type="secondary">
21+
<div class="gap-1 flex items-center">
22+
<lf-svg
23+
name="git-repository"
24+
class="w-3.5 h-3.5"
25+
/>
26+
{{ pluralize('repository', group.repositories.length, true) }}
27+
</div>
28+
</lf-badge>
29+
<div class="flex items-center gap-2">
30+
<lf-button :icon-only="true" type="secondary-ghost-light" @click="edit(gi)">
31+
<lf-icon name="edit" />
32+
</lf-button>
33+
<lf-button :icon-only="true" type="secondary-ghost-light" @click="remove(gi)">
34+
<lf-icon name="trash-can" />
35+
</lf-button>
36+
</div>
37+
</div>
38+
</article>
39+
</div>
40+
</div>
41+
<div v-else class="py-20 flex flex-col items-center">
42+
<lf-icon name="list-tree" :size="80" class="text-gray-300" />
43+
<h6 class="text-center text-h6 pt-6 pb-3">
44+
No repository groups yet
45+
</h6>
46+
<p class="text-small text-gray-500 text-center pb-6">
47+
Create the first group of repositories for this project
48+
</p>
49+
<lf-button type="primary-ghost" @click="add()">
50+
<lf-icon name="plus" />
51+
Add repository group
52+
</lf-button>
53+
</div>
54+
</div>
55+
<lf-repository-groups-modal
56+
v-if="isModalOpen"
57+
v-model="isModalOpen"
58+
:repositories="repositories"
59+
:repository-group="editIndex >= 0 ? cForm.repositoryGroups[editIndex] : null"
60+
@add="create"
61+
@edit="update"
62+
/>
63+
</template>
64+
65+
<script setup lang="ts">
66+
import LfIcon from '@/ui-kit/icon/Icon.vue';
67+
import LfButton from '@/ui-kit/button/Button.vue';
68+
import { computed, reactive, ref } from 'vue';
69+
import LfRepositoryGroupsModal
70+
from '@/modules/admin/modules/insights-projects/components/repository-groups/lf-repository-groups-modal.vue';
71+
import LfBadge from '@/ui-kit/badge/Badge.vue';
72+
import pluralize from 'pluralize';
73+
import LfSvg from '@/shared/svg/svg.vue';
74+
import { InsightsProjectAddFormModel } from '../models/insights-project-add-form.model';
75+
76+
interface RepositoryGroup {
77+
id?: string;
78+
name: string;
79+
repositories: string[];
80+
}
81+
82+
const props = defineProps<{
83+
form: InsightsProjectAddFormModel;
84+
}>();
85+
86+
const cForm = reactive(props.form);
87+
88+
const isModalOpen = ref(false);
89+
const editIndex = ref(-1);
90+
91+
const repositories = computed(() => props.form.repositories.filter((r) => r.enabled));
92+
93+
const add = () => {
94+
editIndex.value = -1;
95+
isModalOpen.value = true;
96+
};
97+
const edit = (index: number) => {
98+
editIndex.value = index;
99+
isModalOpen.value = true;
100+
};
101+
102+
const create = (data: RepositoryGroup) => {
103+
cForm.repositoryGroups = [...cForm.repositoryGroups, data];
104+
};
105+
106+
const update = (data: RepositoryGroup) => {
107+
const list = [...cForm.repositoryGroups];
108+
if (editIndex.value >= 0 && editIndex.value < list.length) {
109+
list[editIndex.value] = {
110+
...list[editIndex.value],
111+
...data,
112+
};
113+
cForm.repositoryGroups = list;
114+
}
115+
};
116+
117+
const remove = (index: number) => {
118+
const list = [...cForm.repositoryGroups];
119+
if (index >= 0 && index < list.length) {
120+
list.splice(index, 1);
121+
cForm.repositoryGroups = list;
122+
}
123+
};
124+
</script>
125+
126+
<script lang="ts">
127+
export default {
128+
name: 'LfInsightsProjectAddRepositoryGroups',
129+
};
130+
</script>

frontend/src/modules/admin/modules/insights-projects/components/lf-insights-project-add.vue

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@
6464
<lf-tab name="widgets">
6565
Widgets
6666
</lf-tab>
67+
<lf-tab name="repository-groups">
68+
Repository groups
69+
</lf-tab>
6770
</lf-tabs>
6871
<div class="pt-2.5">
6972
<div class="tab-content">
@@ -84,6 +87,10 @@
8487
:is-loading="isLoadingWidgets"
8588
:form="form"
8689
/>
90+
<lf-insights-project-add-repository-groups
91+
v-else-if="activeTab === 'repository-groups'"
92+
:form="form"
93+
/>
8794
</div>
8895
</div>
8996
</div>
@@ -128,6 +135,8 @@ import cloneDeep from 'lodash/cloneDeep';
128135
import { ToastStore } from '@/shared/message/notification';
129136
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
130137
import { TanstackKey } from '@/shared/types/tanstack';
138+
import LfInsightsProjectAddRepositoryGroups
139+
from '@/modules/admin/modules/insights-projects/components/lf-insights-project-add-repository-groups.vue';
131140
import LfInsightsProjectAddDetailsTab from './lf-insights-project-add-details-tab.vue';
132141
import LfInsightsProjectAddRepositoryTab from './lf-insights-project-add-repository-tab.vue';
133142
import {
@@ -183,6 +192,7 @@ const initialFormState: InsightsProjectAddFormModel = {
183192
twitter: '',
184193
linkedin: '',
185194
repositories: [],
195+
repositoryGroups: [],
186196
keywords: [],
187197
searchKeywords: [],
188198
widgets: Object.fromEntries(
@@ -276,6 +286,7 @@ const onSubmit = () => {
276286
const request = buildRequest({
277287
...form,
278288
});
289+
console.log(request);
279290
if (isEditForm.value) {
280291
updateMutation.mutate({
281292
id: props.insightsProjectId as string,

0 commit comments

Comments
 (0)