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
Expand Up @@ -209,7 +209,7 @@ class CollectionsApiService {
async searchProjectsAndRepositories(query: string): Promise<SearchResults> {
const sanitizedQuery = sanitizeSearchQuery(query);
if (!sanitizedQuery) {
return { projects: [], repositories: [], collections: [] };
return { projects: [], repositories: [], collections: [], organizations: [] };
}

const res = await $fetch<SearchResults>('/api/search', {
Expand Down
14 changes: 12 additions & 2 deletions frontend/app/components/shared/layout/search/search-modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SPDX-License-Identifier: MIT
v-model="search"
type="text"
class="!outline-none !shadow-none flex-grow text-sm text-neutral-900 leading-5"
placeholder="Search projects, repositories, or collections..."
placeholder="Search projects, repositories, collections, or organizations..."
@input="triggerSearch"
/>
<lfx-icon
Expand Down Expand Up @@ -49,6 +49,7 @@ SPDX-License-Identifier: MIT
:projects="projects"
:repositories="repositories"
:collections="collections"
:organizations="organizations"
/>
</div>
</div>
Expand All @@ -58,7 +59,13 @@ SPDX-License-Identifier: MIT
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { debounce } from 'lodash-es';
import type { SearchCollection, SearchProject, SearchRepository, SearchResults } from '~~/types/search';
import type {
SearchCollection,
SearchOrganization,
SearchProject,
SearchRepository,
SearchResults,
} from '~~/types/search';
import LfxModal from '~/components/uikit/modal/modal.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import LfxSearchResult from '~/components/shared/layout/search/search-result.vue';
Expand All @@ -79,6 +86,7 @@ const loading = ref<boolean>(false);
const projects = ref<SearchProject[]>([]);
const repositories = ref<SearchRepository[]>([]);
const collections = ref<SearchCollection[]>([]);
const organizations = ref<SearchOrganization[]>([]);

const fetchSearchResults = () => {
searchQuery.value = search.value;
Expand All @@ -95,11 +103,13 @@ const fetchSearchResults = () => {
projects.value = res.projects;
repositories.value = res.repositories;
collections.value = res.collections;
organizations.value = res.organizations;
})
.catch(() => {
projects.value = [];
repositories.value = [];
collections.value = [];
organizations.value = [];
})
.finally(() => {
loading.value = false;
Expand Down
53 changes: 51 additions & 2 deletions frontend/app/components/shared/layout/search/search-result.vue
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,47 @@ SPDX-License-Identifier: MIT
</p>
</section>

<!-- Organizations -->
<div
v-if="tab === 'all' && props.organizations.length > 0"
class="pt-3 px-3 text-xs font-semibold leading-5 text-neutral-400"
>
Organizations
</div>
<section
v-if="tab === 'all' || (tab === 'organizations' && props.organizations.length > 0)"
class="flex flex-col gap-1"
>
<nuxt-link
v-for="organization of props.organizations"
:key="organization.slug"
:to="{ name: LfxRoutes.ORGANIZATION, params: { orgSlug: organization.slug } }"
class="px-3 py-2 rounded-md transition-all hover:bg-neutral-50 flex items-center gap-2 cursor-pointer text-sm text-neutral-900"
>
<lfx-avatar
:src="organization.logo || ''"
size="xsmall"
type="organization"
class="!rounded-sm !outline-1"
:aria-label="organization.logo && organization.name"
/>
{{ organization.name }}
</nuxt-link>
</section>
<section
v-if="tab === 'organizations' && props.organizations.length === 0"
class="px-3 py-12 flex flex-col items-center"
>
<lfx-icon
name="building"
:size="40"
class="text-neutral-300"
/>
<p class="pt-5 text-sm leading-5 text-neutral-500 text-center">
We couldn’t find any organizations with that term
</p>
</section>

<!-- No results -->
<section
v-if="tab === 'all' && noResult"
Expand All @@ -174,7 +215,7 @@ SPDX-License-Identifier: MIT

<script setup lang="ts">
import { useRoute } from 'nuxt/app';
import type { SearchCollection, SearchProject, SearchRepository } from '~~/types/search';
import type { SearchCollection, SearchOrganization, SearchProject, SearchRepository } from '~~/types/search';
import LfxTabs from '~/components/uikit/tabs/tabs.vue';
import LfxAvatar from '~/components/uikit/avatar/avatar.vue';
import LfxIcon from '~/components/uikit/icon/icon.vue';
Expand All @@ -186,13 +227,17 @@ const props = defineProps<{
projects: SearchProject[];
repositories: SearchRepository[];
collections: SearchCollection[];
organizations: SearchOrganization[];
}>();

const route = useRoute();

const tab = ref('all');

const noResult = computed(() => !props.projects.length && !props.repositories.length && !props.collections.length);
const noResult = computed(
() =>
!props.projects.length && !props.repositories.length && !props.collections.length && !props.organizations.length,
);

const repositories = computed(() =>
props.repositories.map((repository) => ({
Expand Down Expand Up @@ -223,6 +268,10 @@ const tabs = [
value: 'collections',
label: 'Collections',
},
{
value: 'organizations',
label: 'Organizations',
},
];
</script>

Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/shared/layout/search/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ SPDX-License-Identifier: MIT
class="text-neutral-400 font-normal"
:size="14"
/>
<p class="text-body-1 text-neutral-400 truncate">Search projects, repositories, or collections</p>
<p class="text-body-1 text-neutral-400 truncate">Search projects, repositories, collections, or organizations</p>
<div class="flex-grow" />
<lfx-chip
v-if="!isMobile"
Expand Down
25 changes: 20 additions & 5 deletions frontend/server/api/search.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
// Copyright (c) 2025 The Linux Foundation and each contributor.
// SPDX-License-Identifier: MIT
import { fetchFromTinybird } from '~~/server/data/tinybird/tinybird';
import type { SearchCollection, SearchProject, SearchRepository } from '~~/types/search';
import type {
SearchCollection,
SearchOrganization,
SearchProject,
SearchRepository,
} from '~~/types/search';
import { getRepoNameFromUrl, getRepoSlugFromName } from '~~/server/helpers/repository.helpers';

export interface SearchResponse {
id: string;
type: 'project' | 'repository' | 'collection';
type: 'project' | 'repository' | 'collection' | 'organization';
slug: string;
logo: string | null;
projectSlug: string | null;
Expand All @@ -20,19 +25,20 @@ export interface SearchResponse {
/**
* API Endpoint: /api/search
* Method: GET
* Description: Searches for collections, projects, and repositories based on the provided query.
* Description: Searches for collections, projects, repositories, and organizations based on the provided query.
*
* Query Parameters:
* - query (string, optional): The search term to query collections, projects, and repositories.
* - query (string, optional): The search term to query collections, projects, repositories, and organizations.
* - limit (number, optional): The maximum number of results to return (default: 10).
*
* Response:
* - projects (Array<SearchResponse>): A list of search results of type "project".
* - repositories (Array<SearchResponse>): A list of search results of type "repository".
* - collections (Array<SearchResponse>): A list of search results of type "collection".
* - organizations (Array<SearchResponse>): A list of search results of type "organization".
*
* Search Response Object (SearchResponse):
* - type (string): The type identifier for the search result (e.g., "project", "repository", "collection").
* - type (string): The type identifier for the search result (e.g., "project", "repository", "collection", "organization").
* - slug (string): A unique slug identifier for the search result.
* - logo (string | null): The logo URL associated with the search result, or null if unavailable.
* - projectSlug (string | null): The project slug associated with the result, or null if absent.
Expand All @@ -48,6 +54,7 @@ export default defineEventHandler(async (event) => {
const projects: SearchProject[] = [];
const repositories: SearchRepository[] = [];
const collections: SearchCollection[] = [];
const organizations: SearchOrganization[] = [];

const limit: number = 10;

Expand Down Expand Up @@ -86,6 +93,12 @@ export default defineEventHandler(async (event) => {
slug: item.slug,
name: item.name || '',
});
} else if (item.type === 'organization') {
organizations.push({
slug: item.slug,
name: item.name || '',
logo: item.logo,
});
}
});
}
Expand All @@ -94,13 +107,15 @@ export default defineEventHandler(async (event) => {
projects,
repositories,
collections,
organizations,
};
} catch (error) {
console.error('Error fetching search results:', error);
return {
projects,
repositories,
collections,
organizations,
};
}
});
7 changes: 7 additions & 0 deletions frontend/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ export interface SearchRepository {
url: string;
}

export interface SearchOrganization {
name: string;
slug: string;
logo: string | null;
}

export interface SearchResults {
projects: SearchProject[];
repositories: SearchRepository[];
collections: SearchCollection[];
organizations: SearchOrganization[];
}
Loading