forked from trent_larson/crowd-funder-for-time-pwa
fix missing starred projects in gift selection, and highlight filter on home view if set
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user