Browse Source

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.
pull/142/head
Matthew Raymer 1 week ago
parent
commit
73a472d8b7
  1. 11
      src/components/ActivityListItem.vue
  2. 46
      src/components/EntityGrid.vue
  3. 3
      src/components/EntityIcon.vue
  4. 5
      src/components/ImageViewer.vue
  5. 3
      src/components/ProjectIcon.vue
  6. 10
      src/components/UsageLimitsSection.vue
  7. 25
      src/libs/endorserServer.ts
  8. 224
      src/test/EntityGridFunctionPropTest.vue
  9. 4
      src/test/PlatformServiceMixinTest.vue
  10. 12
      src/utils/PlatformServiceMixin.ts
  11. 3
      src/views/AccountViewView.vue
  12. 11
      src/views/NewEditProjectView.vue
  13. 10
      src/views/ProjectViewView.vue
  14. 63
      src/views/TestView.vue

11
src/components/ActivityListItem.vue

@ -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>

46
src/components/EntityGrid.vue

@ -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);
} }

3
src/components/EntityIcon.vue

@ -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

5
src/components/ImageViewer.vue

@ -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>

3
src/components/ProjectIcon.vue

@ -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);

10
src/components/UsageLimitsSection.vue

@ -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;

25
src/libs/endorserServer.ts

@ -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

@ -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>

4
src/test/PlatformServiceMixinTest.vue

@ -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 {

12
src/utils/PlatformServiceMixin.ts

@ -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: {
@ -198,7 +200,9 @@ export const PlatformServiceMixin = {
(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();
} }

3
src/views/AccountViewView.vue

@ -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();

11
src/views/NewEditProjectView.vue

@ -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

10
src/views/ProjectViewView.vue

@ -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>

63
src/views/TestView.vue

@ -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);
} }

Loading…
Cancel
Save