refactor: consolidate project loading into EntityGrid component

Unify project loading and searching logic in EntityGrid.vue to eliminate
duplication. Make entities prop optional for projects, add internal
project state, and auto-load projects when needed.

- EntityGrid: Combine search/load into fetchProjects(), add internal
  allProjects state, handle pagination internally for both search and
  load modes
- OnboardMeetingSetupView: Remove project loading methods
- MeetingProjectDialog: Remove project props
- GiftedDialog: Remove project loading logic
- EntitySelectionStep: Make projects prop optional

Reduces code duplication by ~150 lines and simplifies component APIs.
All project selection now uses EntityGrid's internal loading.
This commit is contained in:
Jose Olarte III
2025-11-17 19:49:17 +08:00
parent acf104eaa7
commit cb75b25529
5 changed files with 265 additions and 266 deletions

View File

@@ -76,7 +76,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
</template> </template>
<!-- Empty state message --> <!-- Empty state message -->
<li v-if="entities.length === 0" :class="emptyStateClasses"> <li v-if="hasNoEntities" :class="emptyStateClasses">
{{ emptyStateMessage }} {{ emptyStateMessage }}
</li> </li>
@@ -213,6 +213,11 @@ export default class EntityGrid extends Vue {
// API server for project searches // API server for project searches
apiServer = ""; apiServer = "";
// Internal project state (when entities prop not provided for projects)
allProjects: PlanData[] = [];
loadBeforeId: string | undefined = undefined;
isLoadingProjects = false;
// Infinite scroll state // Infinite scroll state
displayedCount = INITIAL_BATCH_SIZE; displayedCount = INITIAL_BATCH_SIZE;
infiniteScrollReset?: () => void; infiniteScrollReset?: () => void;
@@ -222,18 +227,17 @@ export default class EntityGrid extends Vue {
/** /**
* Array of entities to display * Array of entities to display
* *
* For contacts: Must be a COMPLETE list from local database. * For contacts (entityType === 'people'): REQUIRED - Must be a COMPLETE list from local database.
* Use $contactsByDateAdded() to ensure all contacts are included. * Use $contactsByDateAdded() to ensure all contacts are included.
* Client-side filtering assumes the complete list is available. * Client-side filtering assumes the complete list is available.
* IMPORTANT: When passing Contact[] arrays, they must be sorted by date added * IMPORTANT: When passing Contact[] arrays, they must be sorted by date added
* (newest first) for the "Recently Added" section to display correctly. * (newest first) for the "Recently Added" section to display correctly.
* *
* For projects: Can be partial list (pagination supported). * For projects (entityType === 'projects'): OPTIONAL - If not provided, EntityGrid loads
* Server-side search will fetch matching results with pagination, * projects internally from the API server. If provided, uses the provided list.
* regardless of what's in this prop.
*/ */
@Prop({ required: true }) @Prop({ required: false })
entities!: Contact[] | PlanData[]; entities?: Contact[] | PlanData[];
/** Active user's DID */ /** Active user's DID */
@Prop({ required: true }) @Prop({ required: true })
@@ -322,6 +326,33 @@ export default class EntityGrid extends Vue {
return "text-xs text-slate-500 italic col-span-full"; return "text-xs text-slate-500 italic col-span-full";
} }
/**
* Check if there are no entities to display
*/
get hasNoEntities(): boolean {
if (this.entityType === "projects") {
// For projects: check internal state if no entities prop, otherwise check prop
const projectsToCheck = this.entities || this.allProjects;
return projectsToCheck.length === 0;
} else {
// For people: entities prop is required
return !this.entities || this.entities.length === 0;
}
}
/**
* Get the entities array to use (prop or internal state)
*/
get entitiesToUse(): Contact[] | PlanData[] {
if (this.entityType === "projects") {
// For projects: use prop if provided, otherwise use internal state
return this.entities || this.allProjects;
} else {
// For people: entities prop is required
return this.entities || [];
}
}
/** /**
* Computed entities to display - uses function prop if provided, otherwise uses infinite scroll * Computed entities to display - uses function prop if provided, otherwise uses infinite scroll
* When searching, returns filtered results with infinite scroll applied * When searching, returns filtered results with infinite scroll applied
@@ -334,12 +365,12 @@ export default class EntityGrid extends Vue {
// If custom function provided, use it (disables infinite scroll) // If custom function provided, use it (disables infinite scroll)
if (this.displayEntitiesFunction) { if (this.displayEntitiesFunction) {
return this.displayEntitiesFunction(this.entities, this.entityType); return this.displayEntitiesFunction(this.entitiesToUse, this.entityType);
} }
// Default: projects use infinite scroll // Default: projects use infinite scroll
if (this.entityType === "projects") { if (this.entityType === "projects") {
return (this.entities as PlanData[]).slice(0, this.displayedCount); return (this.entitiesToUse as PlanData[]).slice(0, this.displayedCount);
} }
// People: handled by recentContacts + alphabeticalContacts (both use displayedCount) // People: handled by recentContacts + alphabeticalContacts (both use displayedCount)
@@ -353,7 +384,11 @@ export default class EntityGrid extends Vue {
* See the entities prop documentation for details on using $contactsByDateAdded(). * See the entities prop documentation for details on using $contactsByDateAdded().
*/ */
get recentContacts(): Contact[] { get recentContacts(): Contact[] {
if (this.entityType !== "people" || this.searchTerm.trim()) { if (
this.entityType !== "people" ||
this.searchTerm.trim() ||
!this.entities
) {
return []; return [];
} }
// Entities are already sorted by date added (newest first) // Entities are already sorted by date added (newest first)
@@ -365,7 +400,11 @@ export default class EntityGrid extends Vue {
* Uses infinite scroll to control how many are displayed * Uses infinite scroll to control how many are displayed
*/ */
get alphabeticalContacts(): Contact[] { get alphabeticalContacts(): Contact[] {
if (this.entityType !== "people" || this.searchTerm.trim()) { if (
this.entityType !== "people" ||
this.searchTerm.trim() ||
!this.entities
) {
return []; return [];
} }
// Skip the first few (recent contacts) and sort the rest alphabetically // Skip the first few (recent contacts) and sort the rest alphabetically
@@ -503,7 +542,8 @@ export default class EntityGrid extends Vue {
try { try {
if (this.entityType === "projects") { if (this.entityType === "projects") {
// Server-side search for projects (initial load, no beforeId) // Server-side search for projects (initial load, no beforeId)
await this.performProjectSearch(); const searchLower = this.searchTerm.toLowerCase().trim();
await this.fetchProjects(undefined, searchLower);
} else { } else {
// Client-side filtering for contacts (complete list) // Client-side filtering for contacts (complete list)
await this.performContactSearch(); await this.performContactSearch();
@@ -518,15 +558,24 @@ export default class EntityGrid extends Vue {
} }
/** /**
* Perform server-side project search with optional pagination * Fetch projects from API server
* Uses claimContents parameter for search and beforeId for pagination. * Unified method for both loading all projects and searching projects.
* Results are appended when paginating, replaced on initial search. * If claimContents is provided, performs search and updates filteredEntities.
* If claimContents is not provided, loads all projects and updates allProjects.
* *
* @param beforeId - Optional rowId for pagination (loads projects before this ID) * @param beforeId - Optional rowId for pagination (loads projects before this ID)
* @param claimContents - Optional search term (if provided, performs search; if not, loads all)
*/ */
async performProjectSearch(beforeId?: string): Promise<void> { async fetchProjects(
beforeId?: string,
claimContents?: string,
): Promise<void> {
if (!this.apiServer) { if (!this.apiServer) {
this.filteredEntities = []; if (claimContents) {
this.filteredEntities = [];
} else {
this.allProjects = [];
}
if (this.notify) { if (this.notify) {
this.notify( this.notify(
{ {
@@ -541,11 +590,21 @@ export default class EntityGrid extends Vue {
return; return;
} }
const searchLower = this.searchTerm.toLowerCase().trim(); const isSearch = !!claimContents;
let url = `${this.apiServer}/api/v2/report/plans?claimContents=${encodeURIComponent(searchLower)}`; let url = `${this.apiServer}/api/v2/report/plans`;
// Build query parameters
const params: string[] = [];
if (claimContents) {
params.push(
`claimContents=${encodeURIComponent(claimContents.toLowerCase().trim())}`,
);
}
if (beforeId) { if (beforeId) {
url += `&beforeId=${encodeURIComponent(beforeId)}`; params.push(`beforeId=${encodeURIComponent(beforeId)}`);
}
if (params.length > 0) {
url += `?${params.join("&")}`;
} }
try { try {
@@ -555,7 +614,9 @@ export default class EntityGrid extends Vue {
}); });
if (response.status !== 200) { if (response.status !== 200) {
throw new Error("Failed to search projects"); throw new Error(
isSearch ? "Failed to search projects" : "Failed to load projects",
);
} }
const results = await response.json(); const results = await response.json();
@@ -567,44 +628,84 @@ export default class EntityGrid extends Vue {
}), }),
); );
if (beforeId) { if (isSearch) {
// Pagination: append new projects to existing search results // Search mode: update filteredEntities
this.filteredEntities.push(...newProjects); if (beforeId) {
} else { // Pagination: append new projects to existing search results
// Initial search: replace array this.filteredEntities.push(...newProjects);
this.filteredEntities = newProjects; } else {
} // Initial search: replace array
this.filteredEntities = newProjects;
}
// Update searchBeforeId for next pagination // Update searchBeforeId for next pagination
// Use the last project's rowId, or undefined if no more results if (newProjects.length > 0) {
if (newProjects.length > 0) { const lastProject = newProjects[newProjects.length - 1];
const lastProject = newProjects[newProjects.length - 1]; this.searchBeforeId = lastProject.rowId || undefined;
// Only set searchBeforeId if rowId exists (indicates more results available) } else {
this.searchBeforeId = lastProject.rowId || undefined; this.searchBeforeId = undefined; // No more results
}
} else { } else {
this.searchBeforeId = undefined; // No more results // Load mode: update allProjects
if (beforeId) {
// Pagination: append new projects
this.allProjects.push(...newProjects);
} else {
// Initial load: replace array
this.allProjects = newProjects;
}
// Update loadBeforeId for next pagination
if (newProjects.length > 0) {
const lastProject = newProjects[newProjects.length - 1];
this.loadBeforeId = lastProject.rowId || undefined;
} else {
this.loadBeforeId = undefined; // No more results
}
} }
} else { } else {
// No data in response
if (isSearch) {
if (!beforeId) {
// Only clear on initial search, not pagination
this.filteredEntities = [];
}
this.searchBeforeId = undefined;
} else {
if (!beforeId) {
// Only clear on initial load, not pagination
this.allProjects = [];
}
this.loadBeforeId = undefined;
}
}
} catch (error) {
logger.error(
`Error ${isSearch ? "searching" : "loading"} projects:`,
error,
);
if (isSearch) {
if (!beforeId) { if (!beforeId) {
// Only clear on initial search, not pagination // Only clear on initial search error, not pagination error
this.filteredEntities = []; this.filteredEntities = [];
} }
this.searchBeforeId = undefined; this.searchBeforeId = undefined;
} else {
if (!beforeId) {
// Only clear on initial load error, not pagination error
this.allProjects = [];
}
this.loadBeforeId = undefined;
} }
} catch (error) {
logger.error("Error searching projects:", error);
if (!beforeId) {
// Only clear on initial search error, not pagination error
this.filteredEntities = [];
}
this.searchBeforeId = undefined;
if (this.notify) { if (this.notify) {
this.notify( this.notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "Failed to search projects. Please try again.", text: isSearch
? "Failed to search projects. Please try again."
: "Failed to load projects. Please try again.",
}, },
TIMEOUTS.STANDARD, TIMEOUTS.STANDARD,
); );
@@ -617,6 +718,11 @@ export default class EntityGrid extends Vue {
* Assumes entities prop contains complete contact list from local database * Assumes entities prop contains complete contact list from local database
*/ */
async performContactSearch(): Promise<void> { async performContactSearch(): Promise<void> {
if (!this.entities) {
this.filteredEntities = [];
return;
}
// Simulate async (for consistency with project search) // Simulate async (for consistency with project search)
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
@@ -685,21 +791,39 @@ export default class EntityGrid extends Vue {
} }
} }
// Non-search mode: existing logic // Non-search mode
if (this.entityType === "projects") { if (this.entityType === "projects") {
// Projects: if we've shown all loaded entities and callback exists, callback handles server-side availability // Projects: check internal state or prop
const projectsToCheck = this.entities || this.allProjects;
const beforeId = this.entities ? undefined : this.loadBeforeId;
// Can load more if:
// 1. We have more already-loaded results to show, OR
// 2. We've shown all loaded results AND there's a beforeId to load more (and not using entities prop)
const hasMoreLoaded = this.displayedCount < projectsToCheck.length;
const canLoadMoreFromServer =
!this.entities &&
this.displayedCount >= projectsToCheck.length &&
!!beforeId &&
!this.isLoadingProjects;
// Also check if loadMoreCallback is provided (for backward compatibility)
if ( if (
this.entities &&
this.displayedCount >= this.entities.length && this.displayedCount >= this.entities.length &&
this.loadMoreCallback this.loadMoreCallback
) { ) {
return !this.isLoadingMore; // Only return true if not already loading return !this.isLoadingMore;
} }
// Otherwise, check if more in memory
return this.displayedCount < this.entities.length; return hasMoreLoaded || canLoadMoreFromServer;
} }
// People: check if more alphabetical contacts available // People: check if more alphabetical contacts available
// Total available = recent + all alphabetical // Total available = recent + all alphabetical
if (!this.entities) {
return false;
}
const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length; const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length;
return this.displayedCount < totalAvailable; return this.displayedCount < totalAvailable;
} }
@@ -708,10 +832,40 @@ export default class EntityGrid extends Vue {
* Initialize infinite scroll on mount * Initialize infinite scroll on mount
*/ */
async mounted(): Promise<void> { async mounted(): Promise<void> {
// Load apiServer for project searches // Load apiServer for project searches/loads
if (this.entityType === "projects") { if (this.entityType === "projects") {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
// Load projects on mount if entities prop not provided
if (!this.entities && this.apiServer) {
this.isLoadingProjects = true;
try {
await this.fetchProjects();
} catch (error) {
logger.error("Error loading projects on mount:", error);
} finally {
this.isLoadingProjects = false;
}
}
}
// Validate entities prop for people
if (this.entityType === "people" && !this.entities) {
logger.error(
"EntityGrid: entities prop is required when entityType is 'people'",
);
if (this.notify) {
this.notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Contacts data is required but not provided.",
},
TIMEOUTS.SHORT,
);
}
} }
this.$nextTick(() => { this.$nextTick(() => {
@@ -732,12 +886,13 @@ export default class EntityGrid extends Vue {
) { ) {
this.isLoadingSearchMore = true; this.isLoadingSearchMore = true;
try { try {
await this.performProjectSearch(this.searchBeforeId); const searchLower = this.searchTerm.toLowerCase().trim();
await this.fetchProjects(this.searchBeforeId, searchLower);
// After loading more, reset scroll state to allow further loading // After loading more, reset scroll state to allow further loading
this.infiniteScrollReset?.(); this.infiniteScrollReset?.();
} catch (error) { } catch (error) {
logger.error("Error loading more search results:", error); logger.error("Error loading more search results:", error);
// Error already handled in performProjectSearch // Error already handled in fetchProjects
} finally { } finally {
this.isLoadingSearchMore = false; this.isLoadingSearchMore = false;
} }
@@ -750,28 +905,54 @@ export default class EntityGrid extends Vue {
this.displayedCount += INCREMENT_SIZE; this.displayedCount += INCREMENT_SIZE;
} }
} else { } else {
// Non-search mode: existing logic // Non-search mode
// For projects: if we've shown all entities and callback exists, call it if (this.entityType === "projects") {
if ( const projectsToCheck = this.entities || this.allProjects;
this.entityType === "projects" && const beforeId = this.entities ? undefined : this.loadBeforeId;
this.displayedCount >= this.entities.length &&
this.loadMoreCallback && // If using internal state and need to load more from server
!this.isLoadingMore if (
) { !this.entities &&
this.isLoadingMore = true; this.displayedCount >= projectsToCheck.length &&
try { beforeId &&
await this.loadMoreCallback(this.entities); !this.isLoadingProjects
// After callback, entities prop will update via Vue reactivity ) {
// Reset scroll state to allow further loading this.isLoadingProjects = true;
this.infiniteScrollReset?.(); try {
} catch (error) { await this.fetchProjects(beforeId);
// Error handling is up to the callback, but we should reset loading state // After loading more, reset scroll state to allow further loading
console.error("Error in loadMoreCallback:", error); this.infiniteScrollReset?.();
} finally { } catch (error) {
this.isLoadingMore = false; logger.error("Error loading more projects:", error);
// Error already handled in fetchProjects
} finally {
this.isLoadingProjects = false;
}
} else if (
this.entities &&
this.displayedCount >= this.entities.length &&
this.loadMoreCallback &&
!this.isLoadingMore
) {
// Backward compatibility: use loadMoreCallback if provided
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
logger.error("Error in loadMoreCallback:", error);
} finally {
this.isLoadingMore = false;
}
} else {
// Normal case: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE;
} }
} else { } else {
// Normal case: increment displayedCount to show more from memory // People: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE; this.displayedCount += INCREMENT_SIZE;
} }
} }
@@ -823,6 +1004,12 @@ export default class EntityGrid extends Vue {
} }
this.displayedCount = INITIAL_BATCH_SIZE; this.displayedCount = INITIAL_BATCH_SIZE;
this.infiniteScrollReset?.(); this.infiniteScrollReset?.();
// For projects: if entities prop is provided, clear internal state
if (this.entityType === "projects" && this.entities) {
this.allProjects = [];
this.loadBeforeId = undefined;
}
} }
/** /**

View File

@@ -14,7 +14,7 @@ properties * * @author Matthew Raymer */
<EntityGrid <EntityGrid
:entity-type="shouldShowProjects ? 'projects' : 'people'" :entity-type="shouldShowProjects ? 'projects' : 'people'"
:entities="shouldShowProjects ? projects : allContacts" :entities="shouldShowProjects ? projects || undefined : allContacts"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
@@ -23,7 +23,6 @@ properties * * @author Matthew Raymer */
:you-selectable="youSelectable" :you-selectable="youSelectable"
:notify="notify" :notify="notify"
:conflict-context="conflictContext" :conflict-context="conflictContext"
:load-more-callback="shouldShowProjects ? loadMoreCallback : undefined"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
/> />
@@ -95,9 +94,9 @@ export default class EntitySelectionStep extends Vue {
@Prop({ default: false }) @Prop({ default: false })
isFromProjectView!: boolean; isFromProjectView!: boolean;
/** Array of available projects */ /** Array of available projects (optional - EntityGrid loads internally if not provided) */
@Prop({ required: true }) @Prop({ required: false })
projects!: PlanData[]; projects?: PlanData[];
/** Array of available contacts */ /** Array of available contacts */
@Prop({ required: true }) @Prop({ required: true })
@@ -149,10 +148,6 @@ export default class EntitySelectionStep extends Vue {
@Prop() @Prop()
notify?: (notification: NotificationIface, timeout?: number) => void; 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 * CSS classes for the cancel button
*/ */

View File

@@ -15,7 +15,6 @@
giverEntityType === 'project' || recipientEntityType === 'project' giverEntityType === 'project' || recipientEntityType === 'project'
" "
:is-from-project-view="isFromProjectView" :is-from-project-view="isFromProjectView"
:projects="projects"
:all-contacts="allContacts" :all-contacts="allContacts"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
@@ -29,11 +28,6 @@
:unit-code="unitCode" :unit-code="unitCode"
:offer-id="offerId" :offer-id="offerId"
:notify="$notify" :notify="$notify"
:load-more-callback="
giverEntityType === 'project' || recipientEntityType === 'project'
? handleLoadMoreProjects
: undefined
"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
@cancel="cancel" @cancel="cancel"
/> />
@@ -73,7 +67,6 @@ import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
serverMessageForUser, serverMessageForUser,
getHeaders,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
@@ -139,7 +132,6 @@ export default class GiftedDialog extends Vue {
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description) firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
offerId = ""; offerId = "";
projects: PlanData[] = [];
prompt = ""; prompt = "";
receiver?: libsUtil.GiverReceiverInputInfo; receiver?: libsUtil.GiverReceiverInputInfo;
stepType = "giver"; stepType = "giver";
@@ -239,16 +231,6 @@ export default class GiftedDialog extends Vue {
this.allContacts = await this.$contactsByDateAdded(); this.allContacts = await this.$contactsByDateAdded();
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: unknown) { } catch (err: unknown) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.safeNotify.error( this.safeNotify.error(
@@ -494,77 +476,6 @@ export default class GiftedDialog extends Vue {
this.firstStep = false; this.firstStep = false;
} }
/**
* Load projects from the API
* @param beforeId - Optional rowId for pagination (loads projects before this ID)
*/
async loadProjects(beforeId?: string) {
try {
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),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (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) { selectProject(project: PlanData) {
this.giver = { this.giver = {
did: project.handleId, did: project.handleId,

View File

@@ -7,7 +7,6 @@
<!-- EntityGrid for projects --> <!-- EntityGrid for projects -->
<EntityGrid <EntityGrid
:entity-type="'projects'" :entity-type="'projects'"
:entities="allProjects"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
@@ -16,7 +15,6 @@
:show-unnamed-entity="false" :show-unnamed-entity="false"
:notify="notify" :notify="notify"
:conflict-context="'project'" :conflict-context="'project'"
:load-more-callback="loadMoreCallback"
@entity-selected="handleEntitySelected" @entity-selected="handleEntitySelected"
/> />
@@ -58,10 +56,6 @@ export default class MeetingProjectDialog extends Vue {
/** Whether the dialog is visible */ /** Whether the dialog is visible */
visible = false; visible = false;
/** Array of available projects */
@Prop({ required: true })
allProjects!: PlanData[];
/** Active user's DID */ /** Active user's DID */
@Prop({ required: true }) @Prop({ required: true })
activeDid!: string; activeDid!: string;
@@ -78,10 +72,6 @@ export default class MeetingProjectDialog extends Vue {
@Prop() @Prop()
notify?: (notification: NotificationIface, timeout?: number) => void; 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 * Handle entity selection from EntityGrid
* Immediately assigns the selected project and closes the dialog * Immediately assigns the selected project and closes the dialog

View File

@@ -269,12 +269,10 @@
<MeetingProjectDialog <MeetingProjectDialog
ref="meetingProjectDialog" ref="meetingProjectDialog"
:all-projects="allProjects"
:active-did="activeDid" :active-did="activeDid"
:all-my-dids="allMyDids" :all-my-dids="allMyDids"
:all-contacts="allContacts" :all-contacts="allContacts"
:notify="$notify" :notify="$notify"
:load-more-callback="handleLoadMoreProjects"
@assign="handleProjectLinkAssigned" @assign="handleProjectLinkAssigned"
@open="handleDialogOpen" @open="handleDialogOpen"
@close="handleDialogClose" @close="handleDialogClose"
@@ -418,7 +416,6 @@ export default class OnboardMeetingView extends Vue {
isRegistered = false; isRegistered = false;
showDeleteConfirm = false; showDeleteConfirm = false;
fullName = ""; fullName = "";
allProjects: PlanData[] = [];
allContacts: Contact[] = []; allContacts: Contact[] = [];
allMyDids: string[] = []; allMyDids: string[] = [];
selectedProjectData: PlanData | null = null; selectedProjectData: PlanData | null = null;
@@ -847,82 +844,6 @@ export default class OnboardMeetingView extends Vue {
} }
} }
/**
* Load projects from the API
* @param beforeId - Optional rowId for pagination (loads projects before this ID)
*/
async loadProjects(beforeId?: string) {
try {
const headers = await getHeaders(this.activeDid);
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) {
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 (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 * Computed property for selected project
* Returns the separately stored selected project data * Returns the separately stored selected project data
@@ -977,18 +898,13 @@ export default class OnboardMeetingView extends Vue {
} }
/** /**
* Handle dialog open event - stop auto-refresh in MembersList and load projects * Handle dialog open event - stop auto-refresh in MembersList
*/ */
async handleDialogOpen(): Promise<void> { handleDialogOpen(): void {
const membersList = this.$refs.membersList as MembersList; const membersList = this.$refs.membersList as MembersList;
if (membersList) { if (membersList) {
membersList.stopAutoRefresh(); membersList.stopAutoRefresh();
} }
// Load projects when dialog opens (if not already loaded)
if (this.allProjects.length === 0) {
await this.loadProjects();
}
} }
/** /**