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
|
<div
|
||||||
v-if="record.image"
|
v-if="record.image"
|
||||||
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
|
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
|
<a
|
||||||
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
|
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
|
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"
|
alt="Activity image"
|
||||||
@load="cacheImage(record.image)"
|
@load="cacheImage(record.image)"
|
||||||
/>
|
/>
|
||||||
@@ -253,8 +253,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
|
|||||||
import EntityIcon from "./EntityIcon.vue";
|
import EntityIcon from "./EntityIcon.vue";
|
||||||
import {
|
import {
|
||||||
isGiveClaimType,
|
isGiveClaimType,
|
||||||
notifyWhyCannotConfirm,
|
notifyWhyCannotConfirm
|
||||||
transformImageUrlForCors,
|
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
import ProjectIcon from "./ProjectIcon.vue";
|
||||||
@@ -357,9 +356,5 @@ export default class ActivityListItem extends Vue {
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
transformImageUrlForCors(imageUrl: string): string {
|
|
||||||
return transformImageUrlForCors(imageUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ import { NotificationIface } from "../constants/app";
|
|||||||
* - Event delegation for entity selection
|
* - Event delegation for entity selection
|
||||||
* - Warning notifications for conflicted entities
|
* - Warning notifications for conflicted entities
|
||||||
* - Template streamlined with computed CSS properties
|
* - Template streamlined with computed CSS properties
|
||||||
|
* - Configurable entity display logic via function props
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -158,6 +159,40 @@ export default class EntityGrid extends Vue {
|
|||||||
@Prop({ default: "other party" })
|
@Prop({ default: "other party" })
|
||||||
conflictContext!: string;
|
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
|
* 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[] {
|
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;
|
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems;
|
||||||
return this.entities.slice(0, maxDisplay);
|
return this.entities.slice(0, maxDisplay);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { createAvatar, StyleOptions } from "@dicebear/core";
|
|||||||
import { avataaars } from "@dicebear/collection";
|
import { avataaars } from "@dicebear/collection";
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { transformImageUrlForCors } from "../libs/util";
|
|
||||||
import blankSquareSvg from "../assets/blank-square.svg";
|
import blankSquareSvg from "../assets/blank-square.svg";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +56,7 @@ export default class EntityIcon extends Vue {
|
|||||||
// Check for profile image URL (highest priority)
|
// Check for profile image URL (highest priority)
|
||||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
||||||
if (imageUrl) {
|
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
|
// Check for identifier for avatar generation
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
import { UAParser } from "ua-parser-js";
|
import { UAParser } from "ua-parser-js";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { transformImageUrlForCors } from "../libs/util";
|
|
||||||
|
|
||||||
@Component({ emits: ["update:isOpen"] })
|
@Component({ emits: ["update:isOpen"] })
|
||||||
export default class ImageViewer extends Vue {
|
export default class ImageViewer extends Vue {
|
||||||
@@ -80,10 +79,6 @@ export default class ImageViewer extends Vue {
|
|||||||
window.open(this.imageUrl, "_blank");
|
window.open(this.imageUrl, "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get transformedImageUrl() {
|
|
||||||
return transformImageUrlForCors(this.imageUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toSvg } from "jdenticon";
|
import { toSvg } from "jdenticon";
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { transformImageUrlForCors } from "../libs/util";
|
|
||||||
|
|
||||||
const BLANK_CONFIG = {
|
const BLANK_CONFIG = {
|
||||||
lightness: {
|
lightness: {
|
||||||
@@ -36,7 +35,7 @@ export default class ProjectIcon extends Vue {
|
|||||||
|
|
||||||
generateIcon() {
|
generateIcon() {
|
||||||
if (this.imageUrl) {
|
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 {
|
} else {
|
||||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||||
|
|||||||
@@ -64,8 +64,8 @@
|
|||||||
Number(imageLimits?.doneImagesThisWeek || 0) === 1 ? "" : "s"
|
Number(imageLimits?.doneImagesThisWeek || 0) === 1 ? "" : "s"
|
||||||
}}</b
|
}}</b
|
||||||
>
|
>
|
||||||
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this
|
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this week.
|
||||||
week. Your image counter resets at
|
Your image counter resets at
|
||||||
<b class="whitespace-nowrap">{{
|
<b class="whitespace-nowrap">{{
|
||||||
readableDate(imageLimits?.nextWeekBeginDateTime || "")
|
readableDate(imageLimits?.nextWeekBeginDateTime || "")
|
||||||
}}</b>
|
}}</b>
|
||||||
@@ -82,13 +82,13 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: "UsageLimitsSection",
|
name: "UsageLimitsSection",
|
||||||
components: {
|
components: {
|
||||||
FontAwesome: FontAwesomeIcon
|
FontAwesome: FontAwesomeIcon,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
export default class UsageLimitsSection extends Vue {
|
export default class UsageLimitsSection extends Vue {
|
||||||
@Prop({ required: true }) loadingLimits!: boolean;
|
@Prop({ required: true }) loadingLimits!: boolean;
|
||||||
|
|||||||
@@ -425,10 +425,14 @@ export async function getHeaders(
|
|||||||
) {
|
) {
|
||||||
// there's an active current passkey token
|
// there's an active current passkey token
|
||||||
token = passkeyAccessToken;
|
token = passkeyAccessToken;
|
||||||
logger.debug(`[getHeaders] Using cached passkey token for DID ${did}`);
|
logger.debug(
|
||||||
|
`[getHeaders] Using cached passkey token for DID ${did}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// there's no current passkey token or it's expired
|
// there's no current passkey token or it's expired
|
||||||
logger.debug(`[getHeaders] Generating new access token for DID ${did}`);
|
logger.debug(
|
||||||
|
`[getHeaders] Generating new access token for DID ${did}`,
|
||||||
|
);
|
||||||
token = await accessToken(did);
|
token = await accessToken(did);
|
||||||
|
|
||||||
passkeyAccessToken = token;
|
passkeyAccessToken = token;
|
||||||
@@ -437,11 +441,15 @@ export async function getHeaders(
|
|||||||
Date.now() / 1000 + passkeyExpirationSeconds;
|
Date.now() / 1000 + passkeyExpirationSeconds;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`[getHeaders] No passkey, generating access token for DID ${did}`);
|
logger.debug(
|
||||||
|
`[getHeaders] No passkey, generating access token for DID ${did}`,
|
||||||
|
);
|
||||||
token = await accessToken(did);
|
token = await accessToken(did);
|
||||||
}
|
}
|
||||||
headers["Authorization"] = "Bearer " + token;
|
headers["Authorization"] = "Bearer " + token;
|
||||||
logger.debug(`[getHeaders] Successfully generated headers for DID ${did}`);
|
logger.debug(
|
||||||
|
`[getHeaders] Successfully generated headers for DID ${did}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// This rarely happens: we've seen it when they have account info but the
|
// This rarely happens: we've seen it when they have account info but the
|
||||||
// encryption secret got lost. But in most cases we want users to at
|
// encryption secret got lost. But in most cases we want users to at
|
||||||
@@ -465,7 +473,9 @@ export async function getHeaders(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// it's usually OK to request without auth; we assume we're only here when allowed
|
// it's usually OK to request without auth; we assume we're only here when allowed
|
||||||
logger.debug(`[getHeaders] No DID provided, proceeding without authentication`);
|
logger.debug(
|
||||||
|
`[getHeaders] No DID provided, proceeding without authentication`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1503,10 @@ export async function fetchEndorserRateLimits(
|
|||||||
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
const response = await axios.get(url, { headers } as AxiosRequestConfig);
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`, errorStringForLog(error));
|
logger.error(
|
||||||
|
`[fetchEndorserRateLimits] Error for DID ${issuerDid}:`,
|
||||||
|
errorStringForLog(error),
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
224
src/test/EntityGridFunctionPropTest.vue
Normal file
224
src/test/EntityGridFunctionPropTest.vue
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>EntityGrid Function Prop Test</h2>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<button @click="toggleCustomFunction">
|
||||||
|
{{ useCustomFunction ? "Use Default" : "Use Custom Function" }}
|
||||||
|
</button>
|
||||||
|
<span class="ml-2"
|
||||||
|
>Current: {{ useCustomFunction ? "Custom" : "Default" }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>
|
||||||
|
People Grid ({{ people.length }} total,
|
||||||
|
{{ displayedPeopleCount }} shown)
|
||||||
|
</h3>
|
||||||
|
<EntityGrid
|
||||||
|
entity-type="people"
|
||||||
|
:entities="people"
|
||||||
|
:max-items="5"
|
||||||
|
:active-did="activeDid"
|
||||||
|
:all-my-dids="allMyDids"
|
||||||
|
:all-contacts="people"
|
||||||
|
:conflict-checker="conflictChecker"
|
||||||
|
:display-entities-function="
|
||||||
|
useCustomFunction ? customPeopleFunction : undefined
|
||||||
|
"
|
||||||
|
@entity-selected="handleEntitySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3>
|
||||||
|
Projects Grid ({{ projects.length }} total,
|
||||||
|
{{ displayedProjectsCount }} shown)
|
||||||
|
</h3>
|
||||||
|
<EntityGrid
|
||||||
|
entity-type="projects"
|
||||||
|
:entities="projects"
|
||||||
|
:max-items="3"
|
||||||
|
:active-did="activeDid"
|
||||||
|
:all-my-dids="allMyDids"
|
||||||
|
:all-contacts="people"
|
||||||
|
:conflict-checker="conflictChecker"
|
||||||
|
:display-entities-function="
|
||||||
|
useCustomFunction ? customProjectsFunction : undefined
|
||||||
|
"
|
||||||
|
@entity-selected="handleEntitySelected"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3>Selected Entity:</h3>
|
||||||
|
<pre>{{ selectedEntity }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import EntityGrid from "../components/EntityGrid.vue";
|
||||||
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import { PlanData } from "../interfaces/records";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test component to demonstrate EntityGrid's new displayEntitiesFunction prop
|
||||||
|
*
|
||||||
|
* Shows how parent components can control which entities are displayed
|
||||||
|
* through function props instead of relying on computed properties.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
EntityGrid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class EntityGridFunctionPropTest extends Vue {
|
||||||
|
useCustomFunction = false;
|
||||||
|
selectedEntity: {
|
||||||
|
type: "person" | "project" | "special";
|
||||||
|
entityType?: string;
|
||||||
|
data: Contact | PlanData | { did?: string; name: string };
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
activeDid = "did:example:123";
|
||||||
|
allMyDids = ["did:example:123"];
|
||||||
|
|
||||||
|
people: Contact[] = [
|
||||||
|
{
|
||||||
|
did: "did:example:1",
|
||||||
|
name: "Alice",
|
||||||
|
profileImageUrl: "https://example.com/alice.jpg",
|
||||||
|
},
|
||||||
|
{ did: "did:example:2", name: "Bob", profileImageUrl: "" },
|
||||||
|
{
|
||||||
|
did: "did:example:3",
|
||||||
|
name: "Charlie",
|
||||||
|
profileImageUrl: "https://example.com/charlie.jpg",
|
||||||
|
},
|
||||||
|
{ did: "did:example:4", name: "Diana", profileImageUrl: "" },
|
||||||
|
{
|
||||||
|
did: "did:example:5",
|
||||||
|
name: "Eve",
|
||||||
|
profileImageUrl: "https://example.com/eve.jpg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
did: "did:example:6",
|
||||||
|
name: "Frank",
|
||||||
|
profileImageUrl: "https://example.com/frank.jpg",
|
||||||
|
},
|
||||||
|
{ did: "did:example:7", name: "Grace", profileImageUrl: "" },
|
||||||
|
];
|
||||||
|
|
||||||
|
projects: PlanData[] = [
|
||||||
|
{
|
||||||
|
handleId: "proj1",
|
||||||
|
name: "Zebra Project",
|
||||||
|
description: "A project about zebras",
|
||||||
|
issuerDid: "did:example:1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handleId: "proj2",
|
||||||
|
name: "Alpha Project",
|
||||||
|
description: "The first project",
|
||||||
|
issuerDid: "did:example:2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handleId: "proj3",
|
||||||
|
name: "Beta Project",
|
||||||
|
description: "The second project",
|
||||||
|
issuerDid: "did:example:3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handleId: "proj4",
|
||||||
|
name: "Gamma Project",
|
||||||
|
description: "The third project",
|
||||||
|
issuerDid: "did:example:4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handleId: "proj5",
|
||||||
|
name: "Delta Project",
|
||||||
|
description: "The fourth project",
|
||||||
|
issuerDid: "did:example:5",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom function for people: only show those with profile images
|
||||||
|
*/
|
||||||
|
customPeopleFunction = (
|
||||||
|
entities: Contact[],
|
||||||
|
_entityType: string,
|
||||||
|
maxItems: number,
|
||||||
|
): Contact[] => {
|
||||||
|
return entities
|
||||||
|
.filter((person) => person.profileImageUrl)
|
||||||
|
.slice(0, maxItems);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom function for projects: sort by name and limit to 3
|
||||||
|
*/
|
||||||
|
customProjectsFunction = (
|
||||||
|
entities: PlanData[],
|
||||||
|
_entityType: string,
|
||||||
|
_maxItems: number,
|
||||||
|
): PlanData[] => {
|
||||||
|
return entities.sort((a, b) => a.name.localeCompare(b.name)).slice(0, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple conflict checker for testing
|
||||||
|
*/
|
||||||
|
conflictChecker = (did: string): boolean => {
|
||||||
|
return did === this.activeDid;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle between custom and default display functions
|
||||||
|
*/
|
||||||
|
toggleCustomFunction(): void {
|
||||||
|
this.useCustomFunction = !this.useCustomFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle entity selection
|
||||||
|
*/
|
||||||
|
handleEntitySelected(event: {
|
||||||
|
type: "person" | "project" | "special";
|
||||||
|
entityType?: string;
|
||||||
|
data: Contact | PlanData | { did?: string; name: string };
|
||||||
|
}): void {
|
||||||
|
this.selectedEntity = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed properties to show display counts
|
||||||
|
*/
|
||||||
|
get displayedPeopleCount(): number {
|
||||||
|
if (this.useCustomFunction) {
|
||||||
|
return this.customPeopleFunction(this.people, "people", 5).length;
|
||||||
|
}
|
||||||
|
return Math.min(5, this.people.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayedProjectsCount(): number {
|
||||||
|
if (this.useCustomFunction) {
|
||||||
|
return this.customProjectsFunction(this.projects, "projects", 3).length;
|
||||||
|
}
|
||||||
|
return Math.min(7, this.projects.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
pre {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Options, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
|
||||||
@Options({
|
@Component({
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
export default class PlatformServiceMixinTest extends Vue {
|
export default class PlatformServiceMixinTest extends Vue {
|
||||||
|
|||||||
@@ -175,13 +175,15 @@ export const PlatformServiceMixin = {
|
|||||||
currentActiveDid: {
|
currentActiveDid: {
|
||||||
handler(newDid: string | null, oldDid: string | null) {
|
handler(newDid: string | null, oldDid: string | null) {
|
||||||
if (newDid !== oldDid) {
|
if (newDid !== oldDid) {
|
||||||
logger.debug(`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`);
|
logger.debug(
|
||||||
|
`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`,
|
||||||
|
);
|
||||||
// Clear caches that might be affected by the change
|
// Clear caches that might be affected by the change
|
||||||
(this as any).$clearAllCaches();
|
(this as any).$clearAllCaches();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
immediate: true
|
immediate: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -196,9 +198,11 @@ export const PlatformServiceMixin = {
|
|||||||
async $updateActiveDid(newDid: string | null): Promise<void> {
|
async $updateActiveDid(newDid: string | null): Promise<void> {
|
||||||
const oldDid = (this as any)._currentActiveDid;
|
const oldDid = (this as any)._currentActiveDid;
|
||||||
(this as any)._currentActiveDid = newDid;
|
(this as any)._currentActiveDid = newDid;
|
||||||
|
|
||||||
if (newDid !== oldDid) {
|
if (newDid !== oldDid) {
|
||||||
logger.debug(`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`);
|
logger.debug(
|
||||||
|
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
|
||||||
|
);
|
||||||
// Clear caches that might be affected by the change
|
// Clear caches that might be affected by the change
|
||||||
this.$clearAllCaches();
|
this.$clearAllCaches();
|
||||||
}
|
}
|
||||||
@@ -800,7 +804,7 @@ export const PlatformServiceMixin = {
|
|||||||
|
|
||||||
params.push(did);
|
params.push(did);
|
||||||
const sql = `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`;
|
const sql = `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`;
|
||||||
|
|
||||||
await this.$dbExec(sql, params);
|
await this.$dbExec(sql, params);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -821,7 +825,7 @@ export const PlatformServiceMixin = {
|
|||||||
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
|
async $saveMySettings(changes: Partial<Settings>): Promise<boolean> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const currentDid = (this as any).activeDid;
|
const currentDid = (this as any).activeDid;
|
||||||
|
|
||||||
if (!currentDid) {
|
if (!currentDid) {
|
||||||
return await this.$saveSettings(changes);
|
return await this.$saveSettings(changes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1011,9 +1011,6 @@ export default class AccountViewView extends Vue {
|
|||||||
* Initializes component state with values from the database or defaults.
|
* Initializes component state with values from the database or defaults.
|
||||||
*/
|
*/
|
||||||
async initializeState(): Promise<void> {
|
async initializeState(): Promise<void> {
|
||||||
// First get the master settings to see the active DID
|
|
||||||
const masterSettings = await this.$settings();
|
|
||||||
|
|
||||||
// Then get the account-specific settings
|
// Then get the account-specific settings
|
||||||
const settings: AccountSettings = await this.$accountSettings();
|
const settings: AccountSettings = await this.$accountSettings();
|
||||||
|
|
||||||
@@ -1391,7 +1388,7 @@ export default class AccountViewView extends Vue {
|
|||||||
this.loadingLimits = true;
|
this.loadingLimits = true;
|
||||||
try {
|
try {
|
||||||
const did = this.activeDid;
|
const did = this.activeDid;
|
||||||
|
|
||||||
if (!did) {
|
if (!did) {
|
||||||
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
|
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
|
||||||
return;
|
return;
|
||||||
@@ -1404,7 +1401,7 @@ export default class AccountViewView extends Vue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const imageResp = await fetchImageRateLimits(this.axios, did);
|
const imageResp = await fetchImageRateLimits(this.axios, did);
|
||||||
|
|
||||||
if (imageResp.status === 200) {
|
if (imageResp.status === 200) {
|
||||||
this.imageLimits = imageResp.data;
|
this.imageLimits = imageResp.data;
|
||||||
} else {
|
} else {
|
||||||
@@ -1422,7 +1419,7 @@ export default class AccountViewView extends Vue {
|
|||||||
this.axios,
|
this.axios,
|
||||||
did,
|
did,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (endorserResp.status === 200) {
|
if (endorserResp.status === 200) {
|
||||||
this.endorserLimits = endorserResp.data;
|
this.endorserLimits = endorserResp.data;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ export default class HomeView extends Vue {
|
|||||||
this.axios,
|
this.axios,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
// Ultra-concise settings update with automatic cache invalidation!
|
// Ultra-concise settings update with automatic cache invalidation!
|
||||||
await this.$saveMySettings({ isRegistered: true });
|
await this.$saveMySettings({ isRegistered: true });
|
||||||
@@ -1837,7 +1837,7 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for registration status
|
* Computed property for registration status
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* Used in template for registration-dependent UI elements
|
* Used in template for registration-dependent UI elements
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
async switchAccount(did?: string) {
|
async switchAccount(did?: string) {
|
||||||
// Save the new active DID to master settings
|
// Save the new active DID to master settings
|
||||||
await this.$saveSettings({ activeDid: did });
|
await this.$saveSettings({ activeDid: did });
|
||||||
|
|
||||||
// Check if we need to load user-specific settings for the new DID
|
// Check if we need to load user-specific settings for the new DID
|
||||||
if (did) {
|
if (did) {
|
||||||
try {
|
try {
|
||||||
@@ -230,7 +230,7 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
// Handle error silently - user settings will be loaded when needed
|
// Handle error silently - user settings will be loaded when needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to home page to trigger the watcher
|
// Navigate to home page to trigger the watcher
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "home" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<span v-if="hasImage" class="flex justify-between">
|
<span v-if="hasImage" class="flex justify-between">
|
||||||
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
||||||
<img
|
<img
|
||||||
:src="transformImageUrlForCors(imageUrl)"
|
:src="imageUrl"
|
||||||
class="h-24 rounded-xl"
|
class="h-24 rounded-xl"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -261,8 +261,7 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import {
|
import {
|
||||||
retrieveAccountCount,
|
retrieveAccountCount,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount
|
||||||
transformImageUrlForCors,
|
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -861,12 +860,6 @@ export default class NewEditProjectView extends Vue {
|
|||||||
this.longitude = event.latlng.lng;
|
this.longitude = event.latlng.lng;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms image URLs to avoid CORS issues in development
|
|
||||||
* @param imageUrl - Original image URL
|
|
||||||
* @returns Transformed URL for proxy or original URL
|
|
||||||
*/
|
|
||||||
transformImageUrlForCors = transformImageUrlForCors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for character count display
|
* Computed property for character count display
|
||||||
|
|||||||
@@ -562,7 +562,7 @@
|
|||||||
<div v-if="give.fullClaim.image" class="flex justify-center">
|
<div v-if="give.fullClaim.image" class="flex justify-center">
|
||||||
<a :href="give.fullClaim.image" target="_blank">
|
<a :href="give.fullClaim.image" target="_blank">
|
||||||
<img
|
<img
|
||||||
:src="transformImageUrlForCors(give.fullClaim.image)"
|
:src="give.fullClaim.image"
|
||||||
class="h-24 mt-2 rounded-xl"
|
class="h-24 mt-2 rounded-xl"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -607,7 +607,6 @@ import { retrieveAccountDids } from "../libs/util";
|
|||||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { transformImageUrlForCors } from "../libs/util";
|
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
||||||
@@ -1444,12 +1443,5 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
|
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms image URLs to avoid CORS issues in development
|
|
||||||
* @param imageUrl - Original image URL
|
|
||||||
* @returns Transformed URL for proxy or original URL
|
|
||||||
*/
|
|
||||||
transformImageUrlForCors = transformImageUrlForCors;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -157,6 +157,52 @@
|
|||||||
{{ simpleEncryptionTestResultDisplay }}
|
{{ simpleEncryptionTestResultDisplay }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Component Tests</h2>
|
||||||
|
Interactive tests for Vue components and their functionality.
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">EntityGrid Function Props</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
Test the new function prop functionality in EntityGrid component.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
:class="primaryButtonClasses"
|
||||||
|
@click="showEntityGridTest = !showEntityGridTest"
|
||||||
|
>
|
||||||
|
{{ showEntityGridTest ? "Hide" : "Show" }} EntityGrid Function Prop
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showEntityGridTest"
|
||||||
|
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
|
||||||
|
>
|
||||||
|
<EntityGridFunctionPropTest />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<h3 class="text-lg font-semibold mb-2">Platform Service Mixin</h3>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
Test database operations through PlatformServiceMixin.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
:class="primaryButtonClasses"
|
||||||
|
@click="showPlatformServiceTest = !showPlatformServiceTest"
|
||||||
|
>
|
||||||
|
{{ showPlatformServiceTest ? "Hide" : "Show" }} Platform Service Test
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="showPlatformServiceTest"
|
||||||
|
class="mt-4 p-4 border border-gray-300 rounded-md bg-gray-50"
|
||||||
|
>
|
||||||
|
<PlatformServiceMixinTest />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -192,6 +238,8 @@ import {
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Account } from "../db/tables/accounts";
|
import { Account } from "../db/tables/accounts";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
|
import EntityGridFunctionPropTest from "../test/EntityGridFunctionPropTest.vue";
|
||||||
|
import PlatformServiceMixinTest from "../test/PlatformServiceMixinTest.vue";
|
||||||
|
|
||||||
const inputFileNameRef = ref<Blob>();
|
const inputFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -231,7 +279,11 @@ const TEST_PAYLOAD = {
|
|||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
components: { QuickNav },
|
components: {
|
||||||
|
QuickNav,
|
||||||
|
EntityGridFunctionPropTest,
|
||||||
|
PlatformServiceMixinTest,
|
||||||
|
},
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
export default class Help extends Vue {
|
export default class Help extends Vue {
|
||||||
@@ -258,6 +310,10 @@ export default class Help extends Vue {
|
|||||||
|
|
||||||
cryptoLib = cryptoLib;
|
cryptoLib = cryptoLib;
|
||||||
|
|
||||||
|
// for component tests
|
||||||
|
showEntityGridTest = false;
|
||||||
|
showPlatformServiceTest = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed properties for template streamlining
|
* Computed properties for template streamlining
|
||||||
* Eliminates repeated classes and logic in template
|
* Eliminates repeated classes and logic in template
|
||||||
@@ -469,7 +525,10 @@ export default class Help extends Vue {
|
|||||||
* Method to trigger notification test
|
* Method to trigger notification test
|
||||||
* Centralizes notification testing logic
|
* Centralizes notification testing logic
|
||||||
*/
|
*/
|
||||||
triggerTestNotification(config: any) {
|
triggerTestNotification(config: {
|
||||||
|
notification: NotificationIface;
|
||||||
|
timeout?: number;
|
||||||
|
}) {
|
||||||
this.$notify(config.notification, config.timeout);
|
this.$notify(config.notification, config.timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user