feat: add starred projects at the top of the list to choose

This commit is contained in:
2025-12-17 18:18:37 -07:00
parent 4b118b0b91
commit 306e221479
2 changed files with 134 additions and 40 deletions

View File

@@ -139,17 +139,62 @@ projects, and special entities with selection. * * @author Matthew Raymer */
</template>
<template v-else-if="entityType === 'projects'">
<ProjectCard
v-for="project in displayedEntities as PlanData[]"
:key="project.handleId"
:project="project"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:notify="notify"
:conflict-context="conflictContext"
@project-selected="handleProjectSelected"
/>
<!-- When showing projects without search: split into recently bookmarked and rest -->
<template v-if="!searchTerm.trim()">
<!-- Recently Bookmarked Section -->
<template v-if="recentBookmarkedProjects.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
</li>
<ProjectCard
v-for="project in recentBookmarkedProjects"
:key="project.handleId"
:project="project"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:notify="notify"
:conflict-context="conflictContext"
@project-selected="handleProjectSelected"
/>
</template>
<!-- Rest of Projects Section -->
<li
v-if="recentBookmarkedProjects.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
</li>
<ProjectCard
v-for="project in remainingProjects"
:key="project.handleId"
:project="project"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:notify="notify"
:conflict-context="conflictContext"
@project-selected="handleProjectSelected"
/>
</template>
<!-- When searching: show filtered results normally -->
<template v-else>
<ProjectCard
v-for="project in displayedEntities as PlanData[]"
:key="project.handleId"
:project="project"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:notify="notify"
:conflict-context="conflictContext"
@project-selected="handleProjectSelected"
/>
</template>
</template>
</ul>
</template>
@@ -175,6 +220,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 = 3;
/**
* EntityGrid - Unified grid layout for displaying people or projects
@@ -223,6 +269,9 @@ export default class EntityGrid extends Vue {
infiniteScrollReset?: () => void;
scrollContainer?: HTMLElement;
// Starred projects state (for showing recently bookmarked projects)
starredPlanHandleIds: string[] = [];
/**
* Array of entities to display
*
@@ -378,7 +427,8 @@ export default class EntityGrid extends Vue {
}
/**
* Get the remaining contacts sorted alphabetically (when showing contacts and not searching)
* Get all contacts sorted alphabetically (when showing contacts and not searching)
* Includes contacts shown in "Recently Added" section as well
* Uses infinite scroll to control how many are displayed
*/
get alphabeticalContacts(): Contact[] {
@@ -389,18 +439,68 @@ export default class EntityGrid extends Vue {
) {
return [];
}
// Skip the first few (recent contacts) and sort the rest alphabetically
// Sort all contacts alphabetically (including recent ones)
// Create a copy to avoid mutating the original array
const remaining = this.entities as Contact[];
const sorted = [...remaining].sort((a: Contact, b: Contact) => {
// Sort alphabetically by name, falling back to DID if name is missing
const nameA = (a.name || a.did).toLowerCase();
const nameB = (b.name || b.did).toLowerCase();
return nameA.localeCompare(nameB);
});
// Apply infinite scroll: show based on displayedCount (minus the recent contacts)
const toShow = Math.max(0, this.displayedCount - RECENT_CONTACTS_COUNT);
return sorted.slice(0, toShow);
const sorted = [...(this.entities as Contact[])].sort(
(a: Contact, b: Contact) => {
// Sort alphabetically by name, falling back to DID if name is missing
const nameA = (a.name || a.did).toLowerCase();
const nameB = (b.name || b.did).toLowerCase();
return nameA.localeCompare(nameB);
},
);
// Apply infinite scroll: show based on displayedCount
return sorted.slice(0, this.displayedCount);
}
/**
* 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 recentBookmarkedProjects(): PlanData[] {
if (
this.entityType !== "projects" ||
this.searchTerm.trim() ||
this.starredPlanHandleIds.length === 0
) {
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, preserving the order
const recentProjects = recentStarredIds
.map((id) => projects.find((p) => p.handleId === id))
.filter((p): p is PlanData => p !== undefined);
return recentProjects;
}
/**
* Get all projects (when showing projects and not searching)
* Includes projects shown in "Recently Bookmarked" section as well
* Uses infinite scroll to control how many are displayed
*/
get remainingProjects(): PlanData[] {
if (this.entityType !== "projects" || this.searchTerm.trim()) {
return [];
}
const projects = this.entitiesToUse as PlanData[];
if (projects.length === 0) {
return [];
}
// Apply infinite scroll: show based on displayedCount
return projects.slice(0, this.displayedCount);
}
/**
@@ -793,12 +893,11 @@ export default class EntityGrid extends Vue {
}
// People: check if more alphabetical contacts available
// Total available = recent + all alphabetical
// All contacts are shown alphabetically (recent ones appear in both sections)
if (!this.entities) {
return false;
}
const totalAvailable = RECENT_CONTACTS_COUNT + this.entities.length;
return this.displayedCount < totalAvailable;
return this.displayedCount < this.entities.length;
}
/**
@@ -810,6 +909,9 @@ export default class EntityGrid extends Vue {
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;
@@ -959,7 +1061,7 @@ export default class EntityGrid extends Vue {
this.displayedCount = INITIAL_BATCH_SIZE;
this.infiniteScrollReset?.();
// For projects: if entities prop is provided, clear internal state
// For projects: clear internal state if entities prop is provided
if (this.entityType === "projects" && this.entities) {
this.allProjects = [];
this.loadBeforeId = undefined;