forked from trent_larson/crowd-funder-for-time-pwa
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:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user