feat: enhance EntityGrid with function props and improve code formatting

- 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.
This commit is contained in:
Matthew Raymer
2025-07-18 06:16:35 +00:00
parent 45e9bba80a
commit 73a472d8b7
16 changed files with 383 additions and 69 deletions

View File

@@ -63,7 +63,7 @@
<div
v-if="record.image"
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${transformImageUrlForCors(record.image)});`"
:style="`background-image: url(${record.image});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@@ -71,7 +71,7 @@
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="transformImageUrlForCors(record.image)"
:src="record.image"
alt="Activity image"
@load="cacheImage(record.image)"
/>
@@ -253,8 +253,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import {
isGiveClaimType,
notifyWhyCannotConfirm,
transformImageUrlForCors,
notifyWhyCannotConfirm
} from "../libs/util";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
@@ -357,9 +356,5 @@ export default class ActivityListItem extends Vue {
day: "numeric",
});
}
transformImageUrlForCors(imageUrl: string): string {
return transformImageUrlForCors(imageUrl);
}
}
</script>

View File

@@ -96,6 +96,7 @@ import { NotificationIface } from "../constants/app";
* - 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: {
@@ -158,6 +159,40 @@ export default class EntityGrid extends Vue {
@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
*/
@@ -179,9 +214,18 @@ export default class EntityGrid extends Vue {
}
/**
* Computed entities to display (limited by maxItems)
* 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);
}

View File

@@ -15,7 +15,6 @@ import { createAvatar, StyleOptions } from "@dicebear/core";
import { avataaars } from "@dicebear/collection";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { Contact } from "../db/tables/contacts";
import { transformImageUrlForCors } from "../libs/util";
import blankSquareSvg from "../assets/blank-square.svg";
/**
@@ -57,7 +56,7 @@ export default class EntityIcon extends Vue {
// Check for profile image URL (highest priority)
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
if (imageUrl) {
return `<img src="${transformImageUrlForCors(imageUrl)}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
}
// Check for identifier for avatar generation

View File

@@ -41,7 +41,6 @@
import { Component, Vue, Prop } from "vue-facing-decorator";
import { UAParser } from "ua-parser-js";
import { logger } from "../utils/logger";
import { transformImageUrlForCors } from "../libs/util";
@Component({ emits: ["update:isOpen"] })
export default class ImageViewer extends Vue {
@@ -80,10 +79,6 @@ export default class ImageViewer extends Vue {
window.open(this.imageUrl, "_blank");
}
}
get transformedImageUrl() {
return transformImageUrlForCors(this.imageUrl);
}
}
</script>

View File

@@ -13,7 +13,6 @@
<script lang="ts">
import { toSvg } from "jdenticon";
import { Vue, Component, Prop } from "vue-facing-decorator";
import { transformImageUrlForCors } from "../libs/util";
const BLANK_CONFIG = {
lightness: {
@@ -36,7 +35,7 @@ export default class ProjectIcon extends Vue {
generateIcon() {
if (this.imageUrl) {
return `<img src="${transformImageUrlForCors(this.imageUrl)}" class="w-full h-full object-contain" />`;
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
} else {
const config = this.entityId ? undefined : BLANK_CONFIG;
const svgString = toSvg(this.entityId, this.iconSize, config);

View File

@@ -64,8 +64,8 @@
Number(imageLimits?.doneImagesThisWeek || 0) === 1 ? "" : "s"
}}</b
>
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this
week. Your image counter resets at
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this week.
Your image counter resets at
<b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime || "")
}}</b>
@@ -82,13 +82,13 @@
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@Component({
@Component({
name: "UsageLimitsSection",
components: {
FontAwesome: FontAwesomeIcon
}
FontAwesome: FontAwesomeIcon,
},
})
export default class UsageLimitsSection extends Vue {
@Prop({ required: true }) loadingLimits!: boolean;