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>
|
</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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user