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:
@@ -76,7 +76,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
|
||||
</template>
|
||||
|
||||
<!-- Empty state message -->
|
||||
<li v-if="entities.length === 0" :class="emptyStateClasses">
|
||||
<li v-if="hasNoEntities" :class="emptyStateClasses">
|
||||
{{ emptyStateMessage }}
|
||||
</li>
|
||||
|
||||
@@ -213,6 +213,11 @@ export default class EntityGrid extends Vue {
|
||||
// API server for project searches
|
||||
apiServer = "";
|
||||
|
||||
// Internal project state (when entities prop not provided for projects)
|
||||
allProjects: PlanData[] = [];
|
||||
loadBeforeId: string | undefined = undefined;
|
||||
isLoadingProjects = false;
|
||||
|
||||
// Infinite scroll state
|
||||
displayedCount = INITIAL_BATCH_SIZE;
|
||||
infiniteScrollReset?: () => void;
|
||||
@@ -222,18 +227,17 @@ export default class EntityGrid extends Vue {
|
||||
/**
|
||||
* 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.
|
||||
* Client-side filtering assumes the complete list is available.
|
||||
* IMPORTANT: When passing Contact[] arrays, they must be sorted by date added
|
||||
* (newest first) for the "Recently Added" section to display correctly.
|
||||
*
|
||||
* For projects: Can be partial list (pagination supported).
|
||||
* Server-side search will fetch matching results with pagination,
|
||||
* regardless of what's in this prop.
|
||||
* For projects (entityType === 'projects'): OPTIONAL - If not provided, EntityGrid loads
|
||||
* projects internally from the API server. If provided, uses the provided list.
|
||||
*/
|
||||
@Prop({ required: true })
|
||||
entities!: Contact[] | PlanData[];
|
||||
@Prop({ required: false })
|
||||
entities?: Contact[] | PlanData[];
|
||||
|
||||
/** Active user's DID */
|
||||
@Prop({ required: true })
|
||||
@@ -322,6 +326,33 @@ export default class EntityGrid extends Vue {
|
||||
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
|
||||
* 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 (this.displayEntitiesFunction) {
|
||||
return this.displayEntitiesFunction(this.entities, this.entityType);
|
||||
return this.displayEntitiesFunction(this.entitiesToUse, this.entityType);
|
||||
}
|
||||
|
||||
// Default: projects use infinite scroll
|
||||
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)
|
||||
@@ -353,7 +384,11 @@ export default class EntityGrid extends Vue {
|
||||
* See the entities prop documentation for details on using $contactsByDateAdded().
|
||||
*/
|
||||
get recentContacts(): Contact[] {
|
||||
if (this.entityType !== "people" || this.searchTerm.trim()) {
|
||||
if (
|
||||
this.entityType !== "people" ||
|
||||
this.searchTerm.trim() ||
|
||||
!this.entities
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// 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
|
||||
*/
|
||||
get alphabeticalContacts(): Contact[] {
|
||||
if (this.entityType !== "people" || this.searchTerm.trim()) {
|
||||
if (
|
||||
this.entityType !== "people" ||
|
||||
this.searchTerm.trim() ||
|
||||
!this.entities
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// Skip the first few (recent contacts) and sort the rest alphabetically
|
||||
@@ -503,7 +542,8 @@ export default class EntityGrid extends Vue {
|
||||
try {
|
||||
if (this.entityType === "projects") {
|
||||
// Server-side search for projects (initial load, no beforeId)
|
||||
await this.performProjectSearch();
|
||||
const searchLower = this.searchTerm.toLowerCase().trim();
|
||||
await this.fetchProjects(undefined, searchLower);
|
||||
} else {
|
||||
// Client-side filtering for contacts (complete list)
|
||||
await this.performContactSearch();
|
||||
@@ -518,15 +558,24 @@ export default class EntityGrid extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform server-side project search with optional pagination
|
||||
* Uses claimContents parameter for search and beforeId for pagination.
|
||||
* Results are appended when paginating, replaced on initial search.
|
||||
* Fetch projects from API server
|
||||
* Unified method for both loading all projects and searching projects.
|
||||
* 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 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) {
|
||||
this.filteredEntities = [];
|
||||
if (claimContents) {
|
||||
this.filteredEntities = [];
|
||||
} else {
|
||||
this.allProjects = [];
|
||||
}
|
||||
if (this.notify) {
|
||||
this.notify(
|
||||
{
|
||||
@@ -541,11 +590,21 @@ export default class EntityGrid extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchLower = this.searchTerm.toLowerCase().trim();
|
||||
let url = `${this.apiServer}/api/v2/report/plans?claimContents=${encodeURIComponent(searchLower)}`;
|
||||
const isSearch = !!claimContents;
|
||||
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) {
|
||||
url += `&beforeId=${encodeURIComponent(beforeId)}`;
|
||||
params.push(`beforeId=${encodeURIComponent(beforeId)}`);
|
||||
}
|
||||
if (params.length > 0) {
|
||||
url += `?${params.join("&")}`;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -555,7 +614,9 @@ export default class EntityGrid extends Vue {
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -567,44 +628,84 @@ export default class EntityGrid extends Vue {
|
||||
}),
|
||||
);
|
||||
|
||||
if (beforeId) {
|
||||
// Pagination: append new projects to existing search results
|
||||
this.filteredEntities.push(...newProjects);
|
||||
} else {
|
||||
// Initial search: replace array
|
||||
this.filteredEntities = newProjects;
|
||||
}
|
||||
if (isSearch) {
|
||||
// Search mode: update filteredEntities
|
||||
if (beforeId) {
|
||||
// Pagination: append new projects to existing search results
|
||||
this.filteredEntities.push(...newProjects);
|
||||
} else {
|
||||
// Initial search: replace array
|
||||
this.filteredEntities = newProjects;
|
||||
}
|
||||
|
||||
// Update searchBeforeId for next pagination
|
||||
// Use the last project's rowId, or undefined if no more results
|
||||
if (newProjects.length > 0) {
|
||||
const lastProject = newProjects[newProjects.length - 1];
|
||||
// Only set searchBeforeId if rowId exists (indicates more results available)
|
||||
this.searchBeforeId = lastProject.rowId || undefined;
|
||||
// Update searchBeforeId for next pagination
|
||||
if (newProjects.length > 0) {
|
||||
const lastProject = newProjects[newProjects.length - 1];
|
||||
this.searchBeforeId = lastProject.rowId || undefined;
|
||||
} else {
|
||||
this.searchBeforeId = undefined; // No more results
|
||||
}
|
||||
} 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 {
|
||||
// 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) {
|
||||
// Only clear on initial search, not pagination
|
||||
// Only clear on initial search error, not pagination error
|
||||
this.filteredEntities = [];
|
||||
}
|
||||
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) {
|
||||
this.notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
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,
|
||||
);
|
||||
@@ -617,6 +718,11 @@ export default class EntityGrid extends Vue {
|
||||
* Assumes entities prop contains complete contact list from local database
|
||||
*/
|
||||
async performContactSearch(): Promise<void> {
|
||||
if (!this.entities) {
|
||||
this.filteredEntities = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate async (for consistency with project search)
|
||||
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") {
|
||||
// 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 (
|
||||
this.entities &&
|
||||
this.displayedCount >= this.entities.length &&
|
||||
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
|
||||
// Total available = recent + all alphabetical
|
||||
if (!this.entities) {
|
||||
return false;
|
||||
}
|
||||
const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length;
|
||||
return this.displayedCount < totalAvailable;
|
||||
}
|
||||
@@ -708,10 +832,40 @@ export default class EntityGrid extends Vue {
|
||||
* Initialize infinite scroll on mount
|
||||
*/
|
||||
async mounted(): Promise<void> {
|
||||
// Load apiServer for project searches
|
||||
// Load apiServer for project searches/loads
|
||||
if (this.entityType === "projects") {
|
||||
const settings = await this.$accountSettings();
|
||||
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(() => {
|
||||
@@ -732,12 +886,13 @@ export default class EntityGrid extends Vue {
|
||||
) {
|
||||
this.isLoadingSearchMore = true;
|
||||
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
|
||||
this.infiniteScrollReset?.();
|
||||
} catch (error) {
|
||||
logger.error("Error loading more search results:", error);
|
||||
// Error already handled in performProjectSearch
|
||||
// Error already handled in fetchProjects
|
||||
} finally {
|
||||
this.isLoadingSearchMore = false;
|
||||
}
|
||||
@@ -750,28 +905,54 @@ export default class EntityGrid extends Vue {
|
||||
this.displayedCount += INCREMENT_SIZE;
|
||||
}
|
||||
} else {
|
||||
// Non-search mode: existing logic
|
||||
// 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;
|
||||
// Non-search mode
|
||||
if (this.entityType === "projects") {
|
||||
const projectsToCheck = this.entities || this.allProjects;
|
||||
const beforeId = this.entities ? undefined : this.loadBeforeId;
|
||||
|
||||
// If using internal state and need to load more from server
|
||||
if (
|
||||
!this.entities &&
|
||||
this.displayedCount >= projectsToCheck.length &&
|
||||
beforeId &&
|
||||
!this.isLoadingProjects
|
||||
) {
|
||||
this.isLoadingProjects = true;
|
||||
try {
|
||||
await this.fetchProjects(beforeId);
|
||||
// After loading more, reset scroll state to allow further loading
|
||||
this.infiniteScrollReset?.();
|
||||
} catch (error) {
|
||||
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 {
|
||||
// Normal case: increment displayedCount to show more from memory
|
||||
// People: increment displayedCount to show more from memory
|
||||
this.displayedCount += INCREMENT_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -823,6 +1004,12 @@ export default class EntityGrid extends Vue {
|
||||
}
|
||||
this.displayedCount = INITIAL_BATCH_SIZE;
|
||||
this.infiniteScrollReset?.();
|
||||
|
||||
// For projects: if entities prop is provided, clear internal state
|
||||
if (this.entityType === "projects" && this.entities) {
|
||||
this.allProjects = [];
|
||||
this.loadBeforeId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user