forked from trent_larson/crowd-funder-for-time-pwa
feat: add pagination support for project lists in dialogs
Add server-side pagination to EntityGrid component for projects, enabling infinite scrolling to load all available projects instead of stopping after the initial batch. Changes: - EntityGrid: Add loadMoreCallback prop to trigger server-side loading when scroll reaches end of loaded projects - OnboardMeetingSetupView: Update loadProjects() to support pagination with beforeId parameter and add handleLoadMoreProjects() callback - MeetingProjectDialog: Accept and pass through loadMoreCallback to EntityGrid - GiftedDialog: Add pagination support to loadProjects() and handleLoadMoreProjects() callback - EntitySelectionStep: Accept and pass through loadMoreCallback prop to EntityGrid when showing projects This ensures users can access all projects in MeetingProjectDialog and GiftedDialog by automatically loading more as they scroll, matching the behavior already present in DiscoverView. All project uses of EntityGrid now use pagination by default.
This commit is contained in:
@@ -207,6 +207,7 @@ export default class EntityGrid extends Vue {
|
||||
displayedCount = INITIAL_BATCH_SIZE;
|
||||
infiniteScrollReset?: () => void;
|
||||
scrollContainer?: HTMLElement;
|
||||
isLoadingMore = false; // Prevent duplicate callback calls
|
||||
|
||||
/**
|
||||
* Array of entities to display
|
||||
@@ -286,6 +287,23 @@ export default class EntityGrid extends Vue {
|
||||
entityType: "people" | "projects",
|
||||
) => Contact[] | PlanData[];
|
||||
|
||||
/**
|
||||
* Optional callback function to load more entities from server
|
||||
* Called when infinite scroll reaches end and more data is available
|
||||
* Required for projects when using server-side pagination
|
||||
*
|
||||
* @param entities - Current array of entities
|
||||
* @returns Promise that resolves when more entities are loaded
|
||||
*
|
||||
* @example
|
||||
* :load-more-callback="async (entities) => {
|
||||
* const lastEntity = entities[entities.length - 1];
|
||||
* await loadMoreFromServer(lastEntity.rowId);
|
||||
* }"
|
||||
*/
|
||||
@Prop({ default: null })
|
||||
loadMoreCallback?: (entities: Contact[] | PlanData[]) => Promise<void>;
|
||||
|
||||
/**
|
||||
* CSS classes for the empty state message
|
||||
*/
|
||||
@@ -540,7 +558,15 @@ export default class EntityGrid extends Vue {
|
||||
}
|
||||
|
||||
if (this.entityType === "projects") {
|
||||
// Projects: check if more available
|
||||
// Projects: if we've shown all loaded entities, callback handles server-side availability
|
||||
// If callback exists and we've reached the end, assume more might be available
|
||||
if (
|
||||
this.displayedCount >= this.entities.length &&
|
||||
this.loadMoreCallback
|
||||
) {
|
||||
return !this.isLoadingMore; // Only return true if not already loading
|
||||
}
|
||||
// Otherwise, check if more in memory
|
||||
return this.displayedCount < this.entities.length;
|
||||
}
|
||||
|
||||
@@ -560,9 +586,30 @@ export default class EntityGrid extends Vue {
|
||||
if (container) {
|
||||
const { reset } = useInfiniteScroll(
|
||||
container,
|
||||
() => {
|
||||
// Load more: increment displayedCount
|
||||
this.displayedCount += INCREMENT_SIZE;
|
||||
async () => {
|
||||
// For projects: if we've shown all entities and callback exists, call it
|
||||
if (
|
||||
this.entityType === "projects" &&
|
||||
this.displayedCount >= this.entities.length &&
|
||||
this.loadMoreCallback &&
|
||||
!this.isLoadingMore
|
||||
) {
|
||||
this.isLoadingMore = true;
|
||||
try {
|
||||
await this.loadMoreCallback(this.entities);
|
||||
// After callback, entities prop will update via Vue reactivity
|
||||
// Reset scroll state to allow further loading
|
||||
this.infiniteScrollReset?.();
|
||||
} catch (error) {
|
||||
// Error handling is up to the callback, but we should reset loading state
|
||||
console.error("Error in loadMoreCallback:", error);
|
||||
} finally {
|
||||
this.isLoadingMore = false;
|
||||
}
|
||||
} else {
|
||||
// Normal case: increment displayedCount to show more from memory
|
||||
this.displayedCount += INCREMENT_SIZE;
|
||||
}
|
||||
},
|
||||
{
|
||||
distance: 50, // pixels from bottom
|
||||
|
||||
@@ -23,6 +23,7 @@ properties * * @author Matthew Raymer */
|
||||
:you-selectable="youSelectable"
|
||||
:notify="notify"
|
||||
:conflict-context="conflictContext"
|
||||
:load-more-callback="shouldShowProjects ? loadMoreCallback : undefined"
|
||||
@entity-selected="handleEntitySelected"
|
||||
/>
|
||||
|
||||
@@ -148,6 +149,10 @@ export default class EntitySelectionStep extends Vue {
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/** Callback function to load more projects from server */
|
||||
@Prop()
|
||||
loadMoreCallback?: (entities: PlanData[]) => Promise<void>;
|
||||
|
||||
/**
|
||||
* CSS classes for the cancel button
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,11 @@
|
||||
:unit-code="unitCode"
|
||||
:offer-id="offerId"
|
||||
:notify="$notify"
|
||||
:load-more-callback="
|
||||
giverEntityType === 'project' || recipientEntityType === 'project'
|
||||
? handleLoadMoreProjects
|
||||
: undefined
|
||||
"
|
||||
@entity-selected="handleEntitySelected"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
@@ -489,9 +494,17 @@ export default class GiftedDialog extends Vue {
|
||||
this.firstStep = false;
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
/**
|
||||
* Load projects from the API
|
||||
* @param beforeId - Optional rowId for pagination (loads projects before this ID)
|
||||
*/
|
||||
async loadProjects(beforeId?: string) {
|
||||
try {
|
||||
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
|
||||
let url = this.apiServer + "/api/v2/report/plans";
|
||||
if (beforeId) {
|
||||
url += `?beforeId=${encodeURIComponent(beforeId)}`;
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: await getHeaders(this.activeDid),
|
||||
});
|
||||
@@ -502,14 +515,56 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
const results = await response.json();
|
||||
if (results.data) {
|
||||
this.projects = results.data;
|
||||
// Ensure rowId is included in project data
|
||||
const newProjects = results.data.map(
|
||||
(plan: PlanData & { rowId?: string }) => ({
|
||||
...plan,
|
||||
rowId: plan.rowId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (beforeId) {
|
||||
// Pagination: append new projects
|
||||
this.projects.push(...newProjects);
|
||||
} else {
|
||||
// Initial load: replace array
|
||||
this.projects = newProjects;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading projects:", error);
|
||||
this.safeNotify.error("Failed to load projects", TIMEOUTS.STANDARD);
|
||||
// Don't clear existing projects if this was a pagination request
|
||||
if (!beforeId) {
|
||||
this.projects = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle loading more projects when EntityGrid reaches the end
|
||||
* Called by EntitySelectionStep via loadMoreCallback
|
||||
* @param entities - Current array of projects
|
||||
*/
|
||||
async handleLoadMoreProjects(
|
||||
entities: Array<{
|
||||
handleId: string;
|
||||
rowId?: string;
|
||||
}>,
|
||||
): Promise<void> {
|
||||
if (entities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastProject = entities[entities.length - 1];
|
||||
if (!lastProject.rowId) {
|
||||
// No rowId means we can't paginate - likely end of data
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadProjects(lastProject.rowId);
|
||||
}
|
||||
|
||||
selectProject(project: PlanData) {
|
||||
this.giver = {
|
||||
did: project.handleId,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
:show-unnamed-entity="false"
|
||||
:notify="notify"
|
||||
:conflict-context="'project'"
|
||||
:load-more-callback="loadMoreCallback"
|
||||
@entity-selected="handleEntitySelected"
|
||||
/>
|
||||
|
||||
@@ -77,6 +78,10 @@ export default class MeetingProjectDialog extends Vue {
|
||||
@Prop()
|
||||
notify?: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/** Callback function to load more projects from server */
|
||||
@Prop()
|
||||
loadMoreCallback?: (entities: PlanData[]) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Handle entity selection from EntityGrid
|
||||
* Immediately assigns the selected project and closes the dialog
|
||||
|
||||
@@ -274,6 +274,7 @@
|
||||
:all-my-dids="allMyDids"
|
||||
:all-contacts="allContacts"
|
||||
:notify="$notify"
|
||||
:load-more-callback="handleLoadMoreProjects"
|
||||
@assign="handleProjectLinkAssigned"
|
||||
/>
|
||||
|
||||
@@ -785,40 +786,80 @@ export default class OnboardMeetingView extends Vue {
|
||||
|
||||
/**
|
||||
* Load projects from the API
|
||||
* @param beforeId - Optional rowId for pagination (loads projects before this ID)
|
||||
*/
|
||||
async loadProjects() {
|
||||
async loadProjects(beforeId?: string) {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const url = `${this.apiServer}/api/v2/report/plans`;
|
||||
let url = `${this.apiServer}/api/v2/report/plans`;
|
||||
if (beforeId) {
|
||||
url += `?beforeId=${encodeURIComponent(beforeId)}`;
|
||||
}
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
this.allProjects = resp.data.data.map(
|
||||
const newProjects = resp.data.data.map(
|
||||
(plan: {
|
||||
name: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
rowId?: string;
|
||||
}) => ({
|
||||
name: plan.name,
|
||||
description: plan.description,
|
||||
image: plan.image,
|
||||
handleId: plan.handleId,
|
||||
issuerDid: plan.issuerDid,
|
||||
rowId: plan.rowId,
|
||||
}),
|
||||
);
|
||||
|
||||
if (beforeId) {
|
||||
// Pagination: append new projects
|
||||
this.allProjects.push(...newProjects);
|
||||
} else {
|
||||
// Initial load: replace array
|
||||
this.allProjects = newProjects;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$logAndConsole(
|
||||
"Error loading projects: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
// Don't show error to user - just leave projects empty
|
||||
this.allProjects = [];
|
||||
// Don't show error to user - just leave projects empty (or keep existing if pagination)
|
||||
if (!beforeId) {
|
||||
this.allProjects = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle loading more projects when EntityGrid reaches the end
|
||||
* Called by MeetingProjectDialog via loadMoreCallback
|
||||
* @param entities - Current array of projects
|
||||
*/
|
||||
async handleLoadMoreProjects(
|
||||
entities: Array<{
|
||||
handleId: string;
|
||||
rowId?: string;
|
||||
}>,
|
||||
): Promise<void> {
|
||||
if (entities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastProject = entities[entities.length - 1];
|
||||
if (!lastProject.rowId) {
|
||||
// No rowId means we can't paginate - likely end of data
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadProjects(lastProject.rowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed property for selected project
|
||||
* Derives the project from projectLink by finding it in allProjects
|
||||
|
||||
Reference in New Issue
Block a user