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
2 changes: 0 additions & 2 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ declare module '@vue/runtime-core' {
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTimeSelect: typeof import('element-plus/es')['ElTimeSelect']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,8 @@ import {
CollectionModel,
CollectionRequest,
} from '../models/collection.model';
import { InsightsProjectsService } from '../../insights-projects/services/insights-projects.service';
import { useInsightsProjectsStore } from '../../insights-projects/pinia';
import { COLLECTIONS_SERVICE } from '../services/collections.service';

const insightsProjectsStore = useInsightsProjectsStore();

const emit = defineEmits<{(e: 'update:modelValue', value: boolean): void;
(e: 'onCollectionEdited'): void;
(e: 'onCollectionCreated'): void;
Expand Down Expand Up @@ -227,9 +223,6 @@ const fillForm = (record?: CollectionModel) => {
};

onMounted(() => {
InsightsProjectsService.list({}).then((response) => {
insightsProjectsStore.setInsightsProjects(response.rows);
});
if (isEditForm.value) {
loading.value = true;
fillForm(props.collection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,19 @@
/>
<span class="ml-2 text-gray-900 text-sm line-clamp-2">{{ project.name }}</span>
</div>
<div
v-if="isFetchingNextPage"
class="text-gray-400 px-3 h-20 flex items-center justify-center"
>
<lf-icon name="circle-notch" class="animate-spin text-gray-400" :size="16" />
<span class="text-tiny ml-1 text-gray-400">Loading projects...</span>
</div>
</div>
</div>
<div v-else-if="isPending" class="text-gray-400 px-3 h-20 flex items-center justify-center">
<lf-icon name="circle-notch" class="animate-spin text-gray-400" :size="16" />
<span class="text-tiny ml-1 text-gray-400">Loading projects...</span>
</div>
<div v-else class="text-gray-400 px-3 h-10 flex items-center">
No projects found
</div>
Expand All @@ -56,9 +67,17 @@
</template>

<script setup lang="ts">
import { h, ref, computed } from 'vue';
import {
h, ref, computed, onMounted, nextTick, watch, onBeforeUnmount,
} from 'vue';
import { InsightsProjectModel } from '@/modules/admin/modules/insights-projects/models/insights-project.model';
import LfAvatar from '@/ui-kit/avatar/Avatar.vue';
import { QueryFunction, useInfiniteQuery } from '@tanstack/vue-query';
import { useDebounce } from '@vueuse/core';
import { Pagination } from '@/shared/types/Pagination';
import { TanstackKey } from '@/shared/types/tanstack';
import Message from '@/shared/message/message';
import { INSIGHTS_PROJECTS_SERVICE } from '../../insights-projects/services/insights-projects.service';
import { useInsightsProjectsStore } from '../../insights-projects/pinia';

const SearchIcon = h(
Expand All @@ -83,15 +102,52 @@ const emit = defineEmits<{(e: 'onAddProject', projectId: string): void }>();
const props = defineProps<{
selectedProjects: InsightsProjectModel[];
}>();
let scrollContainer: HTMLElement | null = null;

const insightsProjectsStore = useInsightsProjectsStore();

const inputRef = ref(null);
const searchQuery = ref('');
const searchValue = useDebounce(searchQuery, 300);
const isPopoverVisible = ref(false);
const displayProjects = computed(() => removeSelectedProject(
insightsProjectsStore.searchInsightsProjects(searchQuery.value),
));
const queryKey = computed(() => [
TanstackKey.ADMIN_INSIGHTS_PROJECTS,
searchValue.value,
]);

const projectGroupsQueryFn = INSIGHTS_PROJECTS_SERVICE.query(() => ({
limit: 20,
offset: 0,
filter: searchValue.value
? {
name: {
like: `%${searchValue.value}%`,
},
}
: {},
})) as QueryFunction<Pagination<InsightsProjectModel>, readonly unknown[], unknown>;

const {
data,
isPending,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
isSuccess,
error,
} = useInfiniteQuery<Pagination<InsightsProjectModel>, Error>({
queryKey,
queryFn: projectGroupsQueryFn,
getNextPageParam: (lastPage) => {
const nextPage = lastPage.offset + lastPage.limit;
const totalRows = lastPage.total!;
return nextPage < totalRows ? nextPage : undefined;
},
initialPageParam: 0,
});

const removeSelectedProject = (projects: InsightsProjectModel[]) => {
const selectedProjectsIds = props.selectedProjects.map(
Expand All @@ -115,6 +171,59 @@ const onOptionClick = (project: InsightsProjectModel) => {

emit('onAddProject', project.id);
};

// Infinite scroll handler
function onScroll(e: Event) {
if (!scrollContainer) return;
const threshold = 20;

const target = e.target as HTMLElement;
if (
!isFetchingNextPage.value
&& hasNextPage.value
&& target.scrollHeight - target.scrollTop - target.clientHeight < threshold
) {
fetchNextPage();
}
}

watch(data, () => {
if (isSuccess.value && data.value) {
let result = data.value.pages.reduce(
(acc, page) => acc.concat(page.rows),
[] as InsightsProjectModel[],
);

result = [...props.selectedProjects, ...result].reduce((acc, item) => {
if (!acc.find((i) => i.id === item.id)) acc.push(item);
return acc;
}, [] as InsightsProjectModel[]);
insightsProjectsStore.setInsightsProjects(result);
}
}, { immediate: true });

watch(error, (err) => {
if (err) {
Message.error('Something went wrong while fetching Insights projects');
}
});

onMounted(() => {
nextTick(() => {
scrollContainer = document.querySelector(
'.insights-projects-select-popper',
);
if (scrollContainer) {
scrollContainer.addEventListener('scroll', onScroll);
}
});
});

onBeforeUnmount(() => {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', onScroll);
}
});
</script>

<script lang="ts">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import authAxios from '@/shared/axios/auth-axios';
import { Pagination } from '@/shared/types/Pagination';
import { QueryFunction } from '@tanstack/vue-query';
import { InsightsProjectModel } from '../models/insights-project.model';

export class InsightsProjectsService {
static async list(query: any) {
Expand All @@ -9,6 +12,17 @@ export class InsightsProjectsService {
return response.data;
}

query(
query: () => Record<string, string | number | object>,
): QueryFunction<Pagination<InsightsProjectModel>> {
return ({ pageParam = 0 }) => authAxios
.post<Pagination<InsightsProjectModel>>('/collections/insights-projects/query', {
...query(),
offset: pageParam,
})
.then((res) => res.data);
}

static async getById(id: string) {
const response = await authAxios.get(
`/collections/insights-projects/${id}`,
Expand Down Expand Up @@ -53,3 +67,4 @@ export class InsightsProjectsService {
return response.data;
}
}
export const INSIGHTS_PROJECTS_SERVICE = new InsightsProjectsService();
1 change: 1 addition & 0 deletions frontend/src/shared/types/Pagination.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface Pagination<T>{
count: number;
total?: number;
limit: number;
offset: number;
rows: T[]
Expand Down
1 change: 1 addition & 0 deletions frontend/src/shared/types/tanstack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum TanstackKey {
ADMIN_PROJECT_GROUPS = 'admin-project-groups',
ADMIN_COLLECTIONS = 'admin-collections',
ADMIN_INSIGHTS_PROJECTS = 'admin-insights-projects',
}
Loading