Skip to content

feat: collection project repo#1815

Open
emlimlf wants to merge 6 commits intomainfrom
feat/collection-project-repo
Open

feat: collection project repo#1815
emlimlf wants to merge 6 commits intomainfrom
feat/collection-project-repo

Conversation

@emlimlf
Copy link
Copy Markdown
Collaborator

@emlimlf emlimlf commented Apr 6, 2026

WIP

In this PR

  • Added the ability to include repositories in collections
  • Fix issue with edit collection modal height

Ticket

IN-1074

emlimlf added 3 commits April 6, 2026 11:16
Signed-off-by: Efren Lim <elim@linuxfoundation.org>
Signed-off-by: Efren Lim <elim@linuxfoundation.org>
Signed-off-by: Efren Lim <elim@linuxfoundation.org>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conventional Commits FTW!

@emlimlf emlimlf changed the title Feat/collection project repo feat: collection project repo Apr 6, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the Collections feature to support adding repositories alongside projects, and adjusts the Edit Collection modal layout to better constrain height and allow scrolling.

Changes:

  • Add repository support to collection create/edit flows (form model, payload, UI selection/removal).
  • Switch collection details fetching to a combined project-repos endpoint and update list rendering to handle repo-specific display.
  • UI polish: pluralized counts and edit modal height/scroll behavior.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
frontend/types/search.ts Adds url to SearchRepository to support repository selection payloads.
frontend/types/collection.ts Adds repositoryCount for combined project+repo counts in UI.
frontend/tailwind.config.js Adds bg-neutral-50 to safelist.
frontend/server/api/search.ts Includes repository url in search results for repo selection.
frontend/app/components/shared/components/collection-list-item.vue Updates displayed count (now derived from projects + repositories) and pluralizes.
frontend/app/components/shared/components/collection-card.vue Same count/pluralization update for cards.
frontend/app/components/modules/collection/views/collection-details.vue Updates flat list typing to ProjectInsights for combined project/repo results.
frontend/app/components/modules/collection/services/collections.api.service.ts Updates search return type and switches collection items fetch to /project-repos.
frontend/app/components/modules/collection/config/create-collection.config.ts Extends form model to include repositories.
frontend/app/components/modules/collection/components/edit-modal/edit-modal-projects.vue Enables adding/removing repositories in edit modal selection step.
frontend/app/components/modules/collection/components/edit-modal/edit-collection-modal.vue Makes modal content scrollable + includes repositories in form init/validation/payload.
frontend/app/components/modules/collection/components/details/header.vue Updates count computation (projects + repositories) and pluralizes output.
frontend/app/components/modules/collection/components/details/collection-project-item.vue Enhances item UI to display repository URL details.
frontend/app/components/modules/collection/components/create-modal/steps/step-projects.vue Enables repositories in create flow selection step.
frontend/app/components/modules/collection/components/create-modal/steps/selected-projects-list.vue Renders selected repositories and supports removal actions.
frontend/app/components/modules/collection/components/create-modal/steps/project-search-dropdown.vue Updates dropdown to surface both project and repository search results.
frontend/app/components/modules/collection/components/create-modal/steps/collection-search-results.vue New component to render separate project/repository result sections.
frontend/app/components/modules/collection/components/create-modal/create-collection-modal.vue Cloning/creation now carries repositories through to payload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 16 to 22
<lf-selected-projects-list
v-if="model.projects.length > 0"
v-if="model.projects.length > 0 || model.repositories.length > 0"
:projects="model.projects"
@remove="removeProject"
:repositories="model.repositories"
@remove-project="removeProject"
@remove-repository="removeRepository"
/>
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UI now allows proceeding when either projects or repositories are selected, but this step’s validation still enforces projects minLength(1) and the empty-state copy still says “Add projects…”. This will block creating a collection with only repositories. Update the Vuelidate rules (and messaging) to validate (projects.length + repositories.length) >= 1.

Copilot uses AI. Check for mistakes.

// @ts-expect-error - TanStack Query type inference issue with Vue
const flatData = computed(() => data.value?.pages.flatMap((page: Pagination<Project>) => page.data) || []);
const flatData = computed(() => data.value?.pages.flatMap((page: Pagination<ProjectInsights>) => page.data) || []);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pagination and ProjectInsights are referenced in this computed, but they are not imported anywhere in this file (the previous Project import was removed). This will cause a TypeScript error; add the missing type imports (e.g. Pagination from ~~/types/shared/pagination and ProjectInsights from ~~/types/project).

Suggested change
const flatData = computed(() => data.value?.pages.flatMap((page: Pagination<ProjectInsights>) => page.data) || []);
const flatData = computed(() => data.value?.pages.flatMap((
page: import('~~/types/shared/pagination').Pagination<import('~~/types/project').ProjectInsights>,
) => page.data) || []);

Copilot uses AI. Check for mistakes.
import LfxBadgeDetails from '~/components/modules/collection/components/details/badge-details.vue';
import LfxPopover from '~/components/uikit/popover/popover.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import { getRepoNameFromUrl } from '~~/server/helpers/repository.helpers';
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component imports getRepoNameFromUrl from ~~/server/helpers/repository.helpers, which is server-only code. Importing from ~~/server in a Vue component that renders on the client can break the client bundle in Nuxt. Use the existing client-side helper (~/components/modules/repository/utils/repository.helpers) or move the shared logic to a non-server module.

Suggested change
import { getRepoNameFromUrl } from '~~/server/helpers/repository.helpers';
import { getRepoNameFromUrl } from '~/components/modules/repository/utils/repository.helpers';

Copilot uses AI. Check for mistakes.
CollectionProject,
CollectionRepository,
} from '~/components/modules/collection/config/create-collection.config';
import { getRepoNameFromUrl } from '~~/server/helpers/repository.helpers';
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component imports getRepoNameFromUrl from ~~/server/helpers/repository.helpers, which is server-only code. Since this list renders client-side, this can break the client bundle in Nuxt. Use the existing client-side helper (~/components/modules/repository/utils/repository.helpers) or relocate the helper to a shared (non-server/) module.

Suggested change
import { getRepoNameFromUrl } from '~~/server/helpers/repository.helpers';
import { getRepoNameFromUrl } from '~/components/modules/repository/utils/repository.helpers';

Copilot uses AI. Check for mistakes.
removeRepository: [slug: string];
}>();

const shorRepoUrl = (url: string) => {
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in helper name: shorRepoUrl should be shortRepoUrl (and update the template call sites) to keep naming clear and avoid propagating the misspelling.

Suggested change
const shorRepoUrl = (url: string) => {
const shortRepoUrl = (url: string) => {

Copilot uses AI. Check for mistakes.
projects.value = results.projects;
repositories.value = results.repositories;
} catch {
showToast('Error searching projects', ToastTypesEnum.negative);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error toast message still says “Error searching projects”, but this dropdown now searches repositories as well. Update the message to reflect both (or a generic “Error searching”).

Suggested change
showToast('Error searching projects', ToastTypesEnum.negative);
showToast('Error searching', ToastTypesEnum.negative);

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +110
<!-- Projects header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Projects</p>
</div>

<!-- Project items -->
<div class="flex flex-col gap-1">
<div
v-for="project in projects"
:key="project.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addProject', project)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<img
v-if="project.logo"
:src="project.logo"
:alt="project.name"
class="size-full object-contain"
/>
<lfx-icon
v-else
name="folder"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ project.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!isSelected(project.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addProject', project)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>

<!-- Repositories header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Repositories</p>
</div>

<!-- Repository items -->
<div class="flex flex-col gap-1">
<div
v-for="repository in repositories"
:key="repository.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addRepository', repository)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<lfx-icon
name="book"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ repository.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!isSelected(repository.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addRepository', repository)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSelected currently treats a slug as selected if it’s in either the project or repository selection. If a project slug matches a repository slug, this will incorrectly disable the “Add” button / show the checkmark for the other type. Consider checking selection per-section (projects only against selectedProjectsSlugs, repositories only against selectedRepositoriesSlugs) and rendering the “Projects/Repositories” headers only when that section has results.

Suggested change
<!-- Projects header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Projects</p>
</div>
<!-- Project items -->
<div class="flex flex-col gap-1">
<div
v-for="project in projects"
:key="project.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addProject', project)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<img
v-if="project.logo"
:src="project.logo"
:alt="project.name"
class="size-full object-contain"
/>
<lfx-icon
v-else
name="folder"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ project.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!isSelected(project.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addProject', project)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>
<!-- Repositories header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Repositories</p>
</div>
<!-- Repository items -->
<div class="flex flex-col gap-1">
<div
v-for="repository in repositories"
:key="repository.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addRepository', repository)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<lfx-icon
name="book"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ repository.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!isSelected(repository.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addRepository', repository)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>
<template v-if="projects.length">
<!-- Projects header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Projects</p>
</div>
<!-- Project items -->
<div class="flex flex-col gap-1">
<div
v-for="project in projects"
:key="project.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addProject', project)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<img
v-if="project.logo"
:src="project.logo"
:alt="project.name"
class="size-full object-contain"
/>
<lfx-icon
v-else
name="folder"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ project.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!selectedProjectsSlugs.includes(project.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addProject', project)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>
</template>
<template v-if="repositories.length">
<!-- Repositories header -->
<div class="px-3 pt-2 pb-1">
<p class="text-xs font-semibold text-neutral-400 leading-5">Repositories</p>
</div>
<!-- Repository items -->
<div class="flex flex-col gap-1">
<div
v-for="repository in repositories"
:key="repository.slug"
class="flex items-center justify-between px-3 py-2 rounded-md mx-1 cursor-pointer transition-colors hover:bg-neutral-50"
@click="emit('addRepository', repository)"
>
<div class="flex items-center gap-2">
<div
class="size-5 rounded-sm border border-neutral-200 bg-white overflow-hidden flex items-center justify-center"
>
<lfx-icon
name="book"
:size="16"
class="text-neutral-400"
/>
</div>
<span class="text-sm font-normal text-neutral-900 leading-5">{{ repository.name }}</span>
</div>
<div class="flex items-center">
<lfx-button
v-if="!selectedRepositoriesSlugs.includes(repository.slug)"
type="ghost"
size="small"
class="!font-medium !p-0"
@click.stop="emit('addRepository', repository)"
>
<lfx-icon
name="plus"
:size="12"
/>
Add
</lfx-button>
<lfx-icon
v-else
name="check"
:size="14"
class="text-positive-500"
/>
</div>
</div>
</div>
</template>

Copilot uses AI. Check for mistakes.
class="text-sm leading-5 text-neutral-600"
>
{{ projectCount }} projects
{{ pluralize('project', projectCount, true) }}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectCount is now computed as projectCount + repositoryCount, but the label still pluralizes “project”. This will display e.g. “10 projects” even when some/all of the items are repositories. Consider changing the label to something type-agnostic (e.g. “items”) or explicitly “projects & repositories”.

Suggested change
{{ pluralize('project', projectCount, true) }}
{{ pluralize('item', projectCount, true) }}

Copilot uses AI. Check for mistakes.
class="text-xs leading-4 text-neutral-500"
>
{{ props.collection.projectCount }} projects
{{ pluralize('project', projectCount, true) }}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectCount includes both projects and repositories, but the UI label still pluralizes “project”, which can be misleading. Consider updating the label to “items” (or “projects & repositories”) to match what’s counted.

Suggested change
{{ pluralize('project', projectCount, true) }}
{{ pluralize('item', projectCount, true) }}

Copilot uses AI. Check for mistakes.
/>
<span class="text-sm text-neutral-500">
{{ props.collection.projectCount }} projects ・ Updated
{{ pluralize('project', projectCount, true) }} ・ Updated
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

projectCount is calculated as projects + repositories, but the text still renders as “X projects”. Consider using a type-agnostic label (e.g. “items”) or “projects & repositories” so the count matches the wording.

Suggested change
{{ pluralize('project', projectCount, true) }} ・ Updated
{{ pluralize('item', projectCount, true) }} ・ Updated

Copilot uses AI. Check for mistakes.
Signed-off-by: Efren Lim <elim@linuxfoundation.org>
gaspergrom

This comment was marked as low quality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants