You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

348 lines
9.5 KiB

/** * 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="unnamedEntityName"
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";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
/**
* 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;
/** Whether to hide the "Show All" navigation */
@Prop({ default: false })
hideShowAll!: boolean;
/**
* 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.hideShowAll && 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_ENTITY_NAME,
};
}
/**
* Get the unnamed entity name constant
*/
get unnamedEntityName(): string {
return UNNAMED_ENTITY_NAME;
}
/**
* 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
* Treat "You" and "Unnamed" as person entities
*/
handleEntitySelected(event: { data: { did?: string; name: string } }): void {
// Convert special entities to person entities since they represent people
this.emitEntitySelected({
type: "person",
data: event.data as Contact,
});
}
// Emit methods using @Emit decorator
@Emit("entity-selected")
emitEntitySelected(data: {
type: "person" | "project";
data: Contact | PlanData;
}): {
type: "person" | "project";
data: Contact | PlanData;
} {
return data;
}
}
</script>
<style scoped>
/* Grid-specific styles if needed */
</style>