Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ALTER TABLE public."repositoryGroups"
REPLICA IDENTITY DEFAULT;
ALTER PUBLICATION sequin_pub DROP TABLE "repositoryGroups";

DROP INDEX IF EXISTS "ix_repositoryGroups_updatedAt_id";

DROP TABLE IF EXISTS "repositoryGroups";
18 changes: 18 additions & 0 deletions backend/src/database/migrations/V1757413130__repositoryGroups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS "repositoryGroups"
(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL,
"repositories" VARCHAR[] DEFAULT ARRAY []::VARCHAR[],
"insightsProjectId" UUID NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"deletedAt" TIMESTAMP NULL DEFAULT NULL,
foreign key ("insightsProjectId") references "insightsProjects" (id) on delete cascade,
UNIQUE (slug, "insightsProjectId", "deletedAt")
);

create index "ix_repositoryGroups_updatedAt_id" on "repositoryGroups" ("updatedAt", id);

ALTER PUBLICATION sequin_pub ADD TABLE "repositoryGroups";
ALTER TABLE public."repositoryGroups" REPLICA IDENTITY FULL;
88 changes: 88 additions & 0 deletions backend/src/services/collectionService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { uniq } from 'lodash'

import { getCleanString } from '@crowd/common'
import { QueryExecutor } from '@crowd/data-access-layer'
import { listCategoriesByIds } from '@crowd/data-access-layer/src/categories'
import {
CollectionField,
Expand Down Expand Up @@ -30,6 +31,14 @@ import {
} from '@crowd/data-access-layer/src/integrations'
import { OrganizationField, findOrgById, queryOrgs } from '@crowd/data-access-layer/src/orgs'
import { QueryFilter } from '@crowd/data-access-layer/src/query'
import {
ICreateRepositoryGroup,
IRepositoryGroup,
createRepositoryGroup,
deleteRepositoryGroup,
listRepositoryGroups,
updateRepositoryGroup,
} from '@crowd/data-access-layer/src/repositoryGroups'
import { findSegmentById } from '@crowd/data-access-layer/src/segments'
import { QueryResult } from '@crowd/data-access-layer/src/utils'
import { GithubIntegrationSettings } from '@crowd/integrations'
Expand Down Expand Up @@ -241,6 +250,10 @@ export class CollectionService extends LoggerBase {
)
}

if (project.repositoryGroups) {
await this.syncRepositoryGroupsWithDb(qx, createdProject.id, project.repositoryGroups)
}

const txSvc = new CollectionService({ ...this.options, transaction: tx })

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

return {
...project,
Expand All @@ -296,6 +310,7 @@ export class CollectionService extends LoggerBase {
displayName: organization?.displayName,
logo: organization?.logo,
},
repositoryGroups,
}
})
}
Expand Down Expand Up @@ -400,6 +415,9 @@ export class CollectionService extends LoggerBase {
})),
)
}
if (project.repositoryGroups) {
await this.syncRepositoryGroupsWithDb(qx, insightsProjectId, project.repositoryGroups)
}

const txSvc = new CollectionService({
...this.options,
Expand All @@ -409,6 +427,76 @@ export class CollectionService extends LoggerBase {
})
}

/**
* Synchronizes repository groups with the database by creating, updating, or deleting groups based on the provided input.
*
* @param {QueryExecutor} qx - The query executor used to perform database operations.
* @param {string} insightsProjectId - The ID of the insights project to which the repository groups belong.
* @param {ICreateRepositoryGroup[]} repositoryGroups - The array of repository group objects to be synchronized with the database.
* @return {Promise<IRepositoryGroup[]>} A promise that resolves to the list of repository groups currently in the database after synchronization.
*/
// eslint-disable-next-line class-methods-use-this
async syncRepositoryGroupsWithDb(
qx: QueryExecutor,
insightsProjectId: string,
repositoryGroups: ICreateRepositoryGroup[],
): Promise<IRepositoryGroup[]> {
// Get existing repository groups for the given insights project
const existingRepositoryGroups = await listRepositoryGroups(qx, { insightsProjectId })

// Extract IDs of existing repository groups
const existingIds: string[] = existingRepositoryGroups.map((rg) => rg.id)

// Extract IDs of repository groups to be synchronized
const repositoryGroupIds: string[] = repositoryGroups.map((rg) => rg.id) as string[]

// Find repository groups that need to be updated (exist in both lists)
const toUpdate: ICreateRepositoryGroup[] = repositoryGroups.filter((rg) =>
existingIds.includes(rg.id),
)

// Find repository groups that need to be created (don't exist or have no ID)
const toCreate: ICreateRepositoryGroup[] = repositoryGroups.filter(
(rg) => !rg.id || !existingIds.includes(rg.id),
)

// Find repository groups that need to be deleted (exist but not in new list)
const toDelete: string[] = existingIds.filter((id) => !repositoryGroupIds.includes(id))

// Create new repository groups
if (toCreate.length > 0) {
for (const rg of toCreate) {
const slug = getCleanString(rg.name).replace(/\s+/g, '-')
await createRepositoryGroup(qx, {
...rg,
slug,
insightsProjectId,
})
}
}

// Delete repository groups that are no longer needed
if (toDelete.length > 0) {
for (const id of toDelete) {
await deleteRepositoryGroup(qx, id)
}
}

// Update existing repository groups with new data
if (toUpdate.length > 0) {
for (const rg of toUpdate) {
const slug = getCleanString(rg.name).replace(/\s+/g, '-')
await updateRepositoryGroup(qx, rg.id, {
...rg,
slug,
})
}
}

// Return the updated list of repository groups from the database
return listRepositoryGroups(qx, { insightsProjectId })
}

async findRepositoriesForSegment(segmentId: string) {
return SequelizeRepository.withTx(this.options, async (tx) => {
const qx = SequelizeRepository.getQueryExecutor({ ...this.options, transaction: tx })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<template>
<div>
<div v-if="cForm.repositoryGroups.length > 0">
<div class="py-2 px-6 bg-gray-50 border-b border-gray-100 -mx-6">
<lf-button type="primary-ghost" size="small" @click="add()">
<lf-icon name="plus" />
Add repository group
</lf-button>
</div>
<div class="py-3">
<article
v-for="(group, gi) of cForm.repositoryGroups"
:key="gi"
class="flex justify-between items-center border-t first:border-t-0 border-gray-100 py-2.5"
>
<p class="text-medium">
{{ group.name }}
</p>
<div class="flex items-center gap-4">
<lf-badge type="secondary">
<div class="gap-1 flex items-center">
<lf-svg
name="git-repository"
class="w-3.5 h-3.5"
/>
{{ pluralize('repository', group.repositories.length, true) }}
</div>
</lf-badge>
<div class="flex items-center gap-2">
<lf-button :icon-only="true" type="secondary-ghost-light" @click="edit(gi)">
<lf-icon name="edit" />
</lf-button>
<lf-button :icon-only="true" type="secondary-ghost-light" @click="remove(gi)">
<lf-icon name="trash-can" />
</lf-button>
</div>
</div>
</article>
</div>
</div>
<div v-else class="py-20 flex flex-col items-center">
<lf-icon name="list-tree" :size="80" class="text-gray-300" />
<h6 class="text-center text-h6 pt-6 pb-3">
No repository groups yet
</h6>
<p class="text-small text-gray-500 text-center pb-6">
Create the first group of repositories for this project
</p>
<lf-button type="primary-ghost" @click="add()">
<lf-icon name="plus" />
Add repository group
</lf-button>
</div>
</div>
<lf-repository-groups-modal
v-if="isModalOpen"
v-model="isModalOpen"
:repositories="repositories"
:repository-group="editIndex >= 0 ? cForm.repositoryGroups[editIndex] : null"
@add="create"
@edit="update"
/>
</template>

<script setup lang="ts">
import LfIcon from '@/ui-kit/icon/Icon.vue';
import LfButton from '@/ui-kit/button/Button.vue';
import { computed, reactive, ref } from 'vue';
import LfRepositoryGroupsModal
from '@/modules/admin/modules/insights-projects/components/repository-groups/lf-repository-groups-modal.vue';
import LfBadge from '@/ui-kit/badge/Badge.vue';
import pluralize from 'pluralize';
import LfSvg from '@/shared/svg/svg.vue';
import { InsightsProjectAddFormModel } from '../models/insights-project-add-form.model';

interface RepositoryGroup {
id?: string;
name: string;
repositories: string[];
}

const props = defineProps<{
form: InsightsProjectAddFormModel;
}>();

const cForm = reactive(props.form);

const isModalOpen = ref(false);
const editIndex = ref(-1);

const repositories = computed(() => props.form.repositories.filter((r) => r.enabled));

const add = () => {
editIndex.value = -1;
isModalOpen.value = true;
};
const edit = (index: number) => {
editIndex.value = index;
isModalOpen.value = true;
};

const create = (data: RepositoryGroup) => {
cForm.repositoryGroups = [...cForm.repositoryGroups, data];
};

const update = (data: RepositoryGroup) => {
const list = [...cForm.repositoryGroups];
if (editIndex.value >= 0 && editIndex.value < list.length) {
list[editIndex.value] = {
...list[editIndex.value],
...data,
};
cForm.repositoryGroups = list;
}
};

const remove = (index: number) => {
const list = [...cForm.repositoryGroups];
if (index >= 0 && index < list.length) {
list.splice(index, 1);
cForm.repositoryGroups = list;
}
};
</script>

<script lang="ts">
export default {
name: 'LfInsightsProjectAddRepositoryGroups',
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
<lf-tab name="widgets">
Widgets
</lf-tab>
<lf-tab name="repository-groups">
Repository groups
</lf-tab>
</lf-tabs>
<div class="pt-2.5">
<div class="tab-content">
Expand All @@ -84,6 +87,10 @@
:is-loading="isLoadingWidgets"
:form="form"
/>
<lf-insights-project-add-repository-groups
v-else-if="activeTab === 'repository-groups'"
:form="form"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -128,6 +135,8 @@ import cloneDeep from 'lodash/cloneDeep';
import { ToastStore } from '@/shared/message/notification';
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query';
import { TanstackKey } from '@/shared/types/tanstack';
import LfInsightsProjectAddRepositoryGroups
from '@/modules/admin/modules/insights-projects/components/lf-insights-project-add-repository-groups.vue';
import LfInsightsProjectAddDetailsTab from './lf-insights-project-add-details-tab.vue';
import LfInsightsProjectAddRepositoryTab from './lf-insights-project-add-repository-tab.vue';
import {
Expand Down Expand Up @@ -183,6 +192,7 @@ const initialFormState: InsightsProjectAddFormModel = {
twitter: '',
linkedin: '',
repositories: [],
repositoryGroups: [],
keywords: [],
searchKeywords: [],
widgets: Object.fromEntries(
Expand Down Expand Up @@ -276,6 +286,7 @@ const onSubmit = () => {
const request = buildRequest({
...form,
});
console.log(request);
if (isEditForm.value) {
updateMutation.mutate({
id: props.insightsProjectId as string,
Expand Down
Loading
Loading