forked from trent_larson/crowd-funder-for-time-pwa
- Add configurable entity display logic via function props to EntityGrid - Implement comprehensive test suite for EntityGrid function props in TestView - Apply consistent code formatting across 15 components and views - Fix linting issues with trailing commas and line breaks - Add new EntityGridFunctionPropTest.vue for component testing - Update endorserServer with improved error handling and logging - Streamline PlatformServiceMixin with better cache management - Enhance component documentation and type safety Changes span 15 files with 159 additions and 69 deletions, focusing on component flexibility, code quality, and testing infrastructure.
340 lines
9.3 KiB
Vue
340 lines
9.3 KiB
Vue
/** * EntityGrid.vue - Unified entity grid layout component * * Extracted from
|
|
GiftedDialog.vue to provide a reusable grid layout * for displaying people,
|
|
projects, and special entities with selection. * * @author Matthew Raymer */
|
|
<template>
|
|
<ul :class="gridClasses">
|
|
<!-- Special entities (You, Unnamed) for people grids -->
|
|
<template v-if="entityType === 'people'">
|
|
<!-- "You" entity -->
|
|
<SpecialEntityCard
|
|
v-if="showYouEntity"
|
|
entity-type="you"
|
|
label="You"
|
|
icon="hand"
|
|
:selectable="youSelectable"
|
|
:conflicted="youConflicted"
|
|
:entity-data="youEntityData"
|
|
:notify="notify"
|
|
:conflict-context="conflictContext"
|
|
@entity-selected="handleEntitySelected"
|
|
/>
|
|
|
|
<!-- "Unnamed" entity -->
|
|
<SpecialEntityCard
|
|
entity-type="unnamed"
|
|
label="Unnamed"
|
|
icon="circle-question"
|
|
:entity-data="unnamedEntityData"
|
|
:notify="notify"
|
|
:conflict-context="conflictContext"
|
|
@entity-selected="handleEntitySelected"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Empty state message -->
|
|
<li v-if="entities.length === 0" :class="emptyStateClasses">
|
|
{{ emptyStateMessage }}
|
|
</li>
|
|
|
|
<!-- Entity cards (people or projects) -->
|
|
<template v-if="entityType === 'people'">
|
|
<PersonCard
|
|
v-for="person in displayedEntities as Contact[]"
|
|
:key="person.did"
|
|
:person="person"
|
|
:conflicted="isPersonConflicted(person.did)"
|
|
:show-time-icon="true"
|
|
:notify="notify"
|
|
:conflict-context="conflictContext"
|
|
@person-selected="handlePersonSelected"
|
|
/>
|
|
</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"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Show All navigation -->
|
|
<ShowAllCard
|
|
v-if="shouldShowAll"
|
|
:entity-type="entityType"
|
|
:route-name="showAllRoute"
|
|
:query-params="showAllQueryParams"
|
|
/>
|
|
</ul>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|
import PersonCard from "./PersonCard.vue";
|
|
import ProjectCard from "./ProjectCard.vue";
|
|
import SpecialEntityCard from "./SpecialEntityCard.vue";
|
|
import ShowAllCard from "./ShowAllCard.vue";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { PlanData } from "../interfaces/records";
|
|
import { NotificationIface } from "../constants/app";
|
|
|
|
/**
|
|
* EntityGrid - Unified grid layout for displaying people or projects
|
|
*
|
|
* Features:
|
|
* - Responsive grid layout for people/projects
|
|
* - Special entity integration (You, Unnamed)
|
|
* - Conflict detection integration
|
|
* - Empty state messaging
|
|
* - Show All navigation
|
|
* - Event delegation for entity selection
|
|
* - Warning notifications for conflicted entities
|
|
* - Template streamlined with computed CSS properties
|
|
* - Configurable entity display logic via function props
|
|
*/
|
|
@Component({
|
|
components: {
|
|
PersonCard,
|
|
ProjectCard,
|
|
SpecialEntityCard,
|
|
ShowAllCard,
|
|
},
|
|
})
|
|
export default class EntityGrid extends Vue {
|
|
/** Type of entities to display */
|
|
@Prop({ required: true })
|
|
entityType!: "people" | "projects";
|
|
|
|
/** Array of entities to display */
|
|
@Prop({ required: true })
|
|
entities!: Contact[] | PlanData[];
|
|
|
|
/** Maximum number of entities to display */
|
|
@Prop({ default: 10 })
|
|
maxItems!: number;
|
|
|
|
/** Active user's DID */
|
|
@Prop({ required: true })
|
|
activeDid!: string;
|
|
|
|
/** All user's DIDs */
|
|
@Prop({ required: true })
|
|
allMyDids!: string[];
|
|
|
|
/** All contacts */
|
|
@Prop({ required: true })
|
|
allContacts!: Contact[];
|
|
|
|
/** Function to check if a person DID would create a conflict */
|
|
@Prop({ required: true })
|
|
conflictChecker!: (did: string) => boolean;
|
|
|
|
/** Whether to show the "You" entity for people grids */
|
|
@Prop({ default: true })
|
|
showYouEntity!: boolean;
|
|
|
|
/** Whether the "You" entity is selectable */
|
|
@Prop({ default: true })
|
|
youSelectable!: boolean;
|
|
|
|
/** Route name for "Show All" navigation */
|
|
@Prop({ default: "" })
|
|
showAllRoute!: string;
|
|
|
|
/** Query parameters for "Show All" navigation */
|
|
@Prop({ default: () => ({}) })
|
|
showAllQueryParams!: Record<string, string>;
|
|
|
|
/** Notification function from parent component */
|
|
@Prop()
|
|
notify?: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
/** Context for conflict messages (e.g., "giver", "recipient") */
|
|
@Prop({ default: "other party" })
|
|
conflictContext!: string;
|
|
|
|
/**
|
|
* Function to determine which entities to display (allows parent control)
|
|
*
|
|
* This function prop allows parent components to customize which entities
|
|
* are displayed in the grid, enabling advanced filtering, sorting, and
|
|
* display logic beyond the default simple slice behavior.
|
|
*
|
|
* @param entities - The full array of entities (Contact[] or PlanData[])
|
|
* @param entityType - The type of entities being displayed ("people" or "projects")
|
|
* @param maxItems - The maximum number of items to display (from maxItems prop)
|
|
* @returns Filtered/sorted array of entities to display
|
|
*
|
|
* @example
|
|
* // Custom filtering: only show contacts with profile images
|
|
* :display-entities-function="(entities, type, max) =>
|
|
* entities.filter(e => e.profileImageUrl).slice(0, max)"
|
|
*
|
|
* @example
|
|
* // Custom sorting: sort projects by name
|
|
* :display-entities-function="(entities, type, max) =>
|
|
* entities.sort((a, b) => a.name.localeCompare(b.name)).slice(0, max)"
|
|
*
|
|
* @example
|
|
* // Advanced logic: different limits for different entity types
|
|
* :display-entities-function="(entities, type, max) =>
|
|
* type === 'projects' ? entities.slice(0, 5) : entities.slice(0, max)"
|
|
*/
|
|
@Prop({ default: null })
|
|
displayEntitiesFunction?: (
|
|
entities: Contact[] | PlanData[],
|
|
entityType: "people" | "projects",
|
|
maxItems: number,
|
|
) => Contact[] | PlanData[];
|
|
|
|
/**
|
|
* CSS classes for the empty state message
|
|
*/
|
|
get emptyStateClasses(): string {
|
|
return "text-xs text-slate-500 italic col-span-full";
|
|
}
|
|
|
|
/**
|
|
* Computed CSS classes for the grid layout
|
|
*/
|
|
get gridClasses(): string {
|
|
const baseClasses = "grid gap-x-2 gap-y-4 text-center mb-4";
|
|
|
|
if (this.entityType === "projects") {
|
|
return `${baseClasses} grid-cols-3 md:grid-cols-4`;
|
|
} else {
|
|
return `${baseClasses} grid-cols-4 sm:grid-cols-5 md:grid-cols-6`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computed entities to display - uses function prop if provided, otherwise defaults
|
|
*/
|
|
get displayedEntities(): Contact[] | PlanData[] {
|
|
if (this.displayEntitiesFunction) {
|
|
return this.displayEntitiesFunction(
|
|
this.entities,
|
|
this.entityType,
|
|
this.maxItems,
|
|
);
|
|
}
|
|
|
|
// Default implementation for backward compatibility
|
|
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems;
|
|
return this.entities.slice(0, maxDisplay);
|
|
}
|
|
|
|
/**
|
|
* Computed empty state message based on entity type
|
|
*/
|
|
get emptyStateMessage(): string {
|
|
if (this.entityType === "projects") {
|
|
return "(No projects found.)";
|
|
} else {
|
|
return "(Add friends to see more people worthy of recognition.)";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether to show the "Show All" navigation
|
|
*/
|
|
get shouldShowAll(): boolean {
|
|
return this.entities.length > 0 && this.showAllRoute !== "";
|
|
}
|
|
|
|
/**
|
|
* Whether the "You" entity is conflicted
|
|
*/
|
|
get youConflicted(): boolean {
|
|
return this.conflictChecker(this.activeDid);
|
|
}
|
|
|
|
/**
|
|
* Entity data for the "You" special entity
|
|
*/
|
|
get youEntityData(): { did: string; name: string } {
|
|
return {
|
|
did: this.activeDid,
|
|
name: "You",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Entity data for the "Unnamed" special entity
|
|
*/
|
|
get unnamedEntityData(): { did: string; name: string } {
|
|
return {
|
|
did: "",
|
|
name: "Unnamed",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a person DID is conflicted
|
|
*/
|
|
isPersonConflicted(did: string): boolean {
|
|
return this.conflictChecker(did);
|
|
}
|
|
|
|
/**
|
|
* Handle person selection from PersonCard
|
|
*/
|
|
handlePersonSelected(person: Contact): void {
|
|
this.emitEntitySelected({
|
|
type: "person",
|
|
data: person,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle project selection from ProjectCard
|
|
*/
|
|
handleProjectSelected(project: PlanData): void {
|
|
this.emitEntitySelected({
|
|
type: "project",
|
|
data: project,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle special entity selection from SpecialEntityCard
|
|
*/
|
|
handleEntitySelected(event: {
|
|
type: string;
|
|
entityType: string;
|
|
data: { did?: string; name: string };
|
|
}): void {
|
|
this.emitEntitySelected({
|
|
type: "special",
|
|
entityType: event.entityType,
|
|
data: event.data,
|
|
});
|
|
}
|
|
|
|
// Emit methods using @Emit decorator
|
|
|
|
@Emit("entity-selected")
|
|
emitEntitySelected(data: {
|
|
type: "person" | "project" | "special";
|
|
entityType?: string;
|
|
data: Contact | PlanData | { did?: string; name: string };
|
|
}): {
|
|
type: "person" | "project" | "special";
|
|
entityType?: string;
|
|
data: Contact | PlanData | { did?: string; name: string };
|
|
} {
|
|
return data;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Grid-specific styles if needed */
|
|
</style>
|