fix missing starred projects in gift selection, and highlight filter on home view if set

This commit is contained in:
2026-01-19 16:57:13 -07:00
parent 9a6e78ee9d
commit 29b2d9927d
5 changed files with 205 additions and 219 deletions

View File

@@ -139,17 +139,17 @@ projects, and special entities with selection. * * @author Matthew Raymer */
</template>
<template v-else-if="entityType === 'projects'">
<!-- When showing projects without search: split into recently bookmarked and rest -->
<!-- When showing projects without search: split into recently starred and rest -->
<template v-if="!searchTerm.trim()">
<!-- Recently Bookmarked Section -->
<template v-if="recentBookmarkedProjects.length > 0">
<!-- Recently Starred Section -->
<template v-if="recentStarredProjectsToShow.length > 0">
<li
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
>
Recently Bookmarked
Recently Starred
</li>
<ProjectCard
v-for="project in recentBookmarkedProjects"
v-for="project in recentStarredProjectsToShow"
:key="project.handleId"
:project="project"
:active-did="activeDid"
@@ -164,7 +164,7 @@ projects, and special entities with selection. * * @author Matthew Raymer */
<!-- Rest of Projects Section -->
<li
v-if="recentBookmarkedProjects.length > 0"
v-if="remainingProjects.length > 0"
class="text-xs font-semibold text-slate-500 uppercase pt-5 pb-1.5 px-2 border-b border-slate-300"
>
All Projects
@@ -223,7 +223,7 @@ import { TIMEOUTS } from "@/utils/notify";
const INITIAL_BATCH_SIZE = 20;
const INCREMENT_SIZE = 20;
const RECENT_CONTACTS_COUNT = 3;
const RECENT_BOOKMARKED_PROJECTS_COUNT = 10;
const RECENT_STARRED_PROJECTS_COUNT = 10;
/**
* EntityGrid - Unified grid layout for displaying people or projects
@@ -272,8 +272,9 @@ export default class EntityGrid extends Vue {
infiniteScrollReset?: () => void;
scrollContainer?: HTMLElement;
// Starred projects state (for showing recently bookmarked projects)
// Starred projects state (for showing recently starred projects)
starredPlanHandleIds: string[] = [];
recentStarredProjects: PlanData[] = [];
/**
* Array of entities to display
@@ -425,40 +426,19 @@ export default class EntityGrid extends Vue {
}
/**
* Get the 3 most recently bookmarked projects (when showing projects and not searching)
* The starredPlanHandleIds array order represents bookmark order (newest at the end)
* Get the 3 most recently starred projects (when showing projects and not searching)
* Returns the cached member field
*/
get recentBookmarkedProjects(): PlanData[] {
if (
this.entityType !== "projects" ||
this.searchTerm.trim() ||
this.starredPlanHandleIds.length === 0
) {
get recentStarredProjectsToShow(): PlanData[] {
if (this.entityType !== "projects" || this.searchTerm.trim()) {
return [];
}
const projects = this.entitiesToUse as PlanData[];
if (projects.length === 0) {
return [];
}
// Get the last 3 starred IDs (most recently bookmarked)
const recentStarredIds = this.starredPlanHandleIds.slice(
-RECENT_BOOKMARKED_PROJECTS_COUNT,
);
// Find projects matching those IDs, sorting with newest first
const recentProjects = recentStarredIds
.map((id) => projects.find((p) => p.handleId === id))
.filter((p): p is PlanData => p !== undefined)
.reverse();
return recentProjects;
return this.recentStarredProjects;
}
/**
* Get all projects (when showing projects and not searching)
* Includes projects shown in "Recently Bookmarked" section as well
* Includes projects shown in "Recently Starred" section as well
* Uses infinite scroll to control how many are displayed
*/
get remainingProjects(): PlanData[] {
@@ -520,6 +500,124 @@ export default class EntityGrid extends Vue {
return UNNAMED_ENTITY_NAME;
}
/**
* Initialize infinite scroll on mount
*/
async mounted(): Promise<void> {
if (this.entityType === "projects") {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Load starred project IDs for showing recently starred projects
this.starredPlanHandleIds = settings.starredPlanHandleIds || [];
// Load projects on mount if entities prop not provided
this.isLoadingProjects = true;
if (!this.entities) {
await this.loadProjects();
}
await this.loadRecentStarredProjects();
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(() => {
const container = this.$refs.scrollContainer as HTMLElement;
if (container) {
const { reset } = useInfiniteScroll(
container,
async () => {
// Search mode: handle search pagination
if (this.searchTerm.trim()) {
if (this.entityType === "projects") {
// Projects: load more search results if available
if (
this.displayedCount >= this.filteredEntities.length &&
this.searchBeforeId &&
!this.isLoadingSearchMore
) {
this.isLoadingSearchMore = true;
try {
const searchLower = this.searchTerm.toLowerCase().trim();
await this.loadProjects(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 loadProjects
} finally {
this.isLoadingSearchMore = false;
}
} else {
// Show more from already-loaded search results
this.displayedCount += INCREMENT_SIZE;
}
} else {
// Contacts: show more from already-filtered results
this.displayedCount += INCREMENT_SIZE;
}
} else {
// 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.loadProjects(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 loadProjects
} finally {
this.isLoadingProjects = false;
}
} else {
// Normal case: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE;
}
} else {
// People: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE;
}
}
},
{
distance: 50, // pixels from bottom
canLoadMore: () => this.canLoadMore(),
},
);
this.infiniteScrollReset = reset;
}
});
}
/**
* Check if a person DID is conflicted
*/
@@ -604,7 +702,7 @@ export default class EntityGrid extends Vue {
if (this.entityType === "projects") {
// Server-side search for projects (initial load, no beforeId)
const searchLower = this.searchTerm.toLowerCase().trim();
await this.fetchProjects(undefined, searchLower);
await this.loadProjects(undefined, searchLower);
} else {
// Client-side filtering for contacts (complete list)
await this.performContactSearch();
@@ -618,6 +716,57 @@ export default class EntityGrid extends Vue {
}
}
/**
* Load the most recently starred projects
* The starredPlanHandleIds array order represents starred order (newest at the end)
*/
async loadRecentStarredProjects(): Promise<void> {
if (
this.entityType !== "projects" ||
this.searchTerm.trim() ||
this.starredPlanHandleIds.length === 0
) {
this.recentStarredProjects = [];
return;
}
// Get the last 3 starred IDs (most recently starred)
const recentStarredIds = this.starredPlanHandleIds.slice(
-RECENT_STARRED_PROJECTS_COUNT,
);
// Find projects matching those IDs, sorting with newest first
const projects = this.entitiesToUse as PlanData[];
const recentProjects = recentStarredIds
.map((id) => projects.find((p) => p.handleId === id))
.filter((p): p is PlanData => p !== undefined)
.reverse();
// If any projects are not found, fetch them from the API server
if (recentProjects.length < recentStarredIds.length) {
const missingIds = recentStarredIds.filter(
(id) => !recentProjects.some((p) => p.handleId === id),
);
const missingProjects = await this.fetchProjectsByIds(missingIds);
recentProjects.push(...missingProjects);
}
this.recentStarredProjects = recentProjects;
}
async fetchProjectsByIds(ids: string[]): Promise<PlanData[]> {
const idsString = encodeURIComponent(JSON.stringify(ids));
const url = `${this.apiServer}/api/v2/report/plans?planHandleIds=${idsString}`;
const response = await fetch(url, {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to fetch projects");
}
const results = await response.json();
return results.data;
}
/**
* Fetch projects from API server
* Unified method for both loading all projects and searching projects.
@@ -627,10 +776,7 @@ export default class EntityGrid extends Vue {
* @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 fetchProjects(
beforeId?: string,
claimContents?: string,
): Promise<void> {
async loadProjects(beforeId?: string, claimContents?: string): Promise<void> {
if (!this.apiServer) {
if (claimContents) {
this.filteredEntities = [];
@@ -874,129 +1020,6 @@ export default class EntityGrid extends Vue {
return this.displayedCount < this.entities.length;
}
/**
* Initialize infinite scroll on mount
*/
async mounted(): Promise<void> {
// Load apiServer for project searches/loads
if (this.entityType === "projects") {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
// Load starred project IDs for showing recently bookmarked projects
this.starredPlanHandleIds = settings.starredPlanHandleIds || [];
// 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(() => {
const container = this.$refs.scrollContainer as HTMLElement;
if (container) {
const { reset } = useInfiniteScroll(
container,
async () => {
// Search mode: handle search pagination
if (this.searchTerm.trim()) {
if (this.entityType === "projects") {
// Projects: load more search results if available
if (
this.displayedCount >= this.filteredEntities.length &&
this.searchBeforeId &&
!this.isLoadingSearchMore
) {
this.isLoadingSearchMore = true;
try {
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 fetchProjects
} finally {
this.isLoadingSearchMore = false;
}
} else {
// Show more from already-loaded search results
this.displayedCount += INCREMENT_SIZE;
}
} else {
// Contacts: show more from already-filtered results
this.displayedCount += INCREMENT_SIZE;
}
} else {
// 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 {
// Normal case: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE;
}
} else {
// People: increment displayedCount to show more from memory
this.displayedCount += INCREMENT_SIZE;
}
}
},
{
distance: 50, // pixels from bottom
canLoadMore: () => this.canLoadMore(),
},
);
this.infiniteScrollReset = reset;
}
});
}
// Emit methods using @Emit decorator
@Emit("entity-selected")
@@ -1024,28 +1047,17 @@ export default class EntityGrid extends Vue {
// When switching to projects, load them if not provided via entities prop
if (newType === "projects" && !this.entities) {
// Ensure apiServer is loaded
if (!this.apiServer) {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
this.starredPlanHandleIds = settings.starredPlanHandleIds || [];
}
// Load projects if we have an API server
if (this.apiServer && this.allProjects.length === 0) {
if (this.allProjects.length === 0) {
this.isLoadingProjects = true;
try {
await this.fetchProjects();
} catch (error) {
logger.error(
"Error loading projects when switching to projects:",
error,
);
} finally {
await this.loadProjects();
await this.loadRecentStarredProjects();
this.isLoadingProjects = false;
}
}
}
// Clear project state when switching away from projects
if (newType === "people") {

View File

@@ -24,7 +24,7 @@ properties * * @author Matthew Raymer */
<EntityGrid
:entity-type="shouldShowProjects ? 'projects' : 'people'"
:entities="shouldShowProjects ? projects || undefined : allContacts"
:entities="shouldShowProjects ? undefined : allContacts"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
@@ -87,10 +87,6 @@ export default class EntitySelectionStep extends Vue {
@Prop({ required: true })
stepType!: "giver" | "recipient";
/** Array of available projects (optional - EntityGrid loads internally if not provided) */
@Prop({ required: false })
projects?: PlanData[];
/** Array of available contacts */
@Prop({ required: true })
allContacts!: Contact[];

View File

@@ -11,10 +11,6 @@
:step-type="stepType"
:giver-entity-type="currentGiverEntityType"
:recipient-entity-type="currentRecipientEntityType"
:show-projects="
currentGiverEntityType === 'project' ||
currentRecipientEntityType === 'project'
"
:all-contacts="allContacts"
:active-did="activeDid"
:all-my-dids="allMyDids"

View File

@@ -114,8 +114,7 @@ export interface DatabaseExport {
const _memoryLogs: string[] = [];
/**
* Enhanced mixin that provides cached platform service access and utility methods
* with smart caching layer for ultimate performance optimization
* Enhanced mixin that provides platform utility methods
*/
export const PlatformServiceMixin = {
data() {
@@ -1011,7 +1010,7 @@ export const PlatformServiceMixin = {
},
/**
* Load settings with optional defaults WITHOUT caching - $settings()
* Load settings with optional defaults - $settings()
* Settings are loaded fresh every time for immediate consistency
* @param defaults Optional default values
* @returns Fresh settings object from database
@@ -1034,11 +1033,11 @@ export const PlatformServiceMixin = {
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
return settings; // Return fresh data without caching
return settings;
},
/**
* Load account-specific settings WITHOUT caching - $accountSettings()
* Load account-specific settings - $accountSettings()
* Settings are loaded fresh every time for immediate consistency
* @param did DID identifier (optional, uses current active DID)
* @param defaults Optional default values
@@ -2242,7 +2241,7 @@ export const PlatformServiceMixin = {
// =================================================
/**
* Enhanced interface with caching utility methods
* Enhanced interface
*/
export interface IPlatformServiceMixin {
platformService: PlatformService;

View File

@@ -167,18 +167,12 @@ Raymer * @version 1.0.0 */
<div class="flex gap-2 items-center mb-3">
<h2 class="font-bold">Latest Activity</h2>
<button
v-if="resultsAreFiltered()"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
<button
v-else
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
class="block ms-auto text-sm text-center text-white p-1.5 rounded-full shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)]"
:class="
isAnyFeedFilterOn
? 'bg-gradient-to-b from-blue-400 to-blue-700'
: 'bg-gradient-to-b from-slate-400 to-slate-700'
"
@click="openFeedFilters()"
>
<font-awesome
@@ -971,17 +965,6 @@ export default class HomeView extends Vue {
);
}
/**
* Checks if feed results are being filtered
*
* @public
* Used in template for filter button display
* @returns true if visible or nearby filters are active
*/
resultsAreFiltered() {
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
}
/**
* Checks if browser notifications are supported
*