feat: remove the 'lock' from the giving & receiving sides #229
@@ -21,7 +21,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: 4,
|
workers: 3,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: [
|
reporter: [
|
||||||
['list'],
|
['list'],
|
||||||
|
|||||||
@@ -165,15 +165,19 @@ export default class EntitySelectionStep extends Vue {
|
|||||||
*/
|
*/
|
||||||
get stepLabel(): string {
|
get stepLabel(): string {
|
||||||
if (this.stepType === "recipient") {
|
if (this.stepType === "recipient") {
|
||||||
return "Choose who received the gift";
|
|
||||||
} else if (this.stepType === "giver") {
|
|
||||||
if (this.shouldShowProjects) {
|
if (this.shouldShowProjects) {
|
||||||
return "Choose a project benefitted from";
|
return "Choose recipient project";
|
||||||
} else {
|
} else {
|
||||||
return "Choose a person received from";
|
return "Choose recipient person";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this.stepType === "giver"
|
||||||
|
if (this.shouldShowProjects) {
|
||||||
|
return "Choose giving project";
|
||||||
|
} else {
|
||||||
|
return "Choose giving person";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Choose entity";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
/** * EntitySummaryButton.vue - Displays selected entity with edit capability *
|
/* EntitySummaryButton.vue - Displays selected entity with edit capability */
|
||||||
* Extracted from GiftedDialog.vue to handle entity summary display in the gift *
|
|
||||||
details step with edit functionality. * * Features: * - Shows entity avatar
|
|
||||||
(person or project) * - Displays entity name and role label * - Handles editable
|
|
||||||
vs locked states * - Function props for parent control over edit behavior * -
|
|
||||||
Supports both person and project entity types * - Template streamlined with
|
|
||||||
computed CSS properties * * @author Matthew Raymer */
|
|
||||||
<template>
|
<template>
|
||||||
<component
|
<button :class="containerClasses" @click="handleClick">
|
||||||
:is="editable ? 'button' : 'div'"
|
|
||||||
:class="containerClasses"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<!-- Entity Icon/Avatar -->
|
<!-- Entity Icon/Avatar -->
|
||||||
<div>
|
<div>
|
||||||
<template v-if="entityType === 'project'">
|
<template v-if="entityType === 'project'">
|
||||||
@@ -47,14 +37,11 @@ computed CSS properties * * @author Matthew Raymer */
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Edit/Lock Icon -->
|
<!-- Edit Icon -->
|
||||||
<p class="ms-auto text-sm pe-1" :class="iconClasses">
|
<p class="ms-auto text-sm pe-1 text-blue-500">
|
||||||
<font-awesome
|
<font-awesome icon="pen" title="Change" />
|
||||||
:icon="editable ? 'pen' : 'lock'"
|
|
||||||
:title="editable ? 'Change' : 'Can\'t be changed'"
|
|
||||||
/>
|
|
||||||
</p>
|
</p>
|
||||||
</component>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -75,12 +62,12 @@ interface EntityData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntitySummaryButton - Displays selected entity with optional edit capability
|
* EntitySummaryButton - Displays selected entity with edit capability
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Shows entity avatar (person or project)
|
* - Shows entity avatar (person or project)
|
||||||
* - Displays entity name and role label
|
* - Displays entity name and role label
|
||||||
* - Handles editable vs locked states
|
* - Always editable - click to change entity
|
||||||
* - Function props for parent control over edit behavior
|
* - Function props for parent control over edit behavior
|
||||||
* - Supports both person and project entity types
|
* - Supports both person and project entity types
|
||||||
* - Template streamlined with computed CSS properties
|
* - Template streamlined with computed CSS properties
|
||||||
@@ -104,13 +91,9 @@ export default class EntitySummaryButton extends Vue {
|
|||||||
@Prop({ required: true })
|
@Prop({ required: true })
|
||||||
label!: string;
|
label!: string;
|
||||||
|
|
||||||
/** Whether the entity can be edited */
|
|
||||||
@Prop({ default: true })
|
|
||||||
editable!: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function prop for handling edit requests
|
* Function prop for handling edit requests
|
||||||
* Called when the button is clicked and editable, allowing parent to control edit behavior
|
* Called when the button is clicked, allowing parent to control edit behavior
|
||||||
*/
|
*/
|
||||||
@Prop({ type: Function, default: () => {} })
|
@Prop({ type: Function, default: () => {} })
|
||||||
onEditRequested!: (data: {
|
onEditRequested!: (data: {
|
||||||
@@ -132,13 +115,6 @@ export default class EntitySummaryButton extends Vue {
|
|||||||
return this.entity !== null && "profileImageUrl" in this.entity;
|
return this.entity !== null && "profileImageUrl" in this.entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed CSS classes for the edit/lock icon
|
|
||||||
*/
|
|
||||||
get iconClasses(): string {
|
|
||||||
return this.editable ? "text-blue-500" : "text-slate-400";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed CSS classes for the entity name
|
* Computed CSS classes for the entity name
|
||||||
*/
|
*/
|
||||||
@@ -172,16 +148,13 @@ export default class EntitySummaryButton extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle click event - only call function prop if editable
|
* Handle click event - call function prop to allow parent to control edit behavior
|
||||||
* Allows parent to control edit behavior and validation
|
|
||||||
*/
|
*/
|
||||||
handleClick(): void {
|
handleClick(): void {
|
||||||
if (this.editable) {
|
this.onEditRequested({
|
||||||
this.onEditRequested({
|
entityType: this.entityType,
|
||||||
entityType: this.entityType,
|
entity: this.entity,
|
||||||
entity: this.entity,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -195,8 +168,4 @@ button {
|
|||||||
button:hover {
|
button:hover {
|
||||||
background-color: #f1f5f9; /* hover:bg-slate-100 */
|
background-color: #f1f5f9; /* hover:bg-slate-100 */
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ control over updates and validation * * @author Matthew Raymer */
|
|||||||
:entity="giver"
|
:entity="giver"
|
||||||
:entity-type="giverEntityType"
|
:entity-type="giverEntityType"
|
||||||
:label="giverLabel"
|
:label="giverLabel"
|
||||||
:editable="canEditGiver"
|
|
||||||
:on-edit-requested="handleEditGiver"
|
:on-edit-requested="handleEditGiver"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ control over updates and validation * * @author Matthew Raymer */
|
|||||||
:entity="receiver"
|
:entity="receiver"
|
||||||
:entity-type="recipientEntityType"
|
:entity-type="recipientEntityType"
|
||||||
:label="recipientLabel"
|
:label="recipientLabel"
|
||||||
:editable="canEditRecipient"
|
|
||||||
:on-edit-requested="handleEditRecipient"
|
:on-edit-requested="handleEditRecipient"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,14 +186,6 @@ export default class GiftDetailsStep extends Vue {
|
|||||||
@Prop({ default: "" })
|
@Prop({ default: "" })
|
||||||
toProjectId!: string;
|
toProjectId!: string;
|
||||||
|
|
||||||
/** Whether the giver is locked and cannot be edited */
|
|
||||||
@Prop({ default: false })
|
|
||||||
isGiverLocked!: boolean;
|
|
||||||
|
|
||||||
/** Whether the recipient is locked and cannot be edited */
|
|
||||||
@Prop({ default: false })
|
|
||||||
isRecipientLocked!: boolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function prop for handling description updates
|
* Function prop for handling description updates
|
||||||
* Called when the description input changes, allowing parent to control validation
|
* Called when the description input changes, allowing parent to control validation
|
||||||
@@ -281,21 +271,6 @@ export default class GiftDetailsStep extends Vue {
|
|||||||
: "Given to:";
|
: "Given to:";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the giver can be edited
|
|
||||||
*/
|
|
||||||
get canEditGiver(): boolean {
|
|
||||||
// If giver is locked via prop, it cannot be edited
|
|
||||||
return !this.isGiverLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the recipient can be edited
|
|
||||||
*/
|
|
||||||
get canEditRecipient(): boolean {
|
|
||||||
return !this.isRecipientLocked;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed CSS classes for submit button
|
* Computed CSS classes for submit button
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -48,8 +48,6 @@
|
|||||||
:offer-id="offerId"
|
:offer-id="offerId"
|
||||||
:from-project-id="fromProjectId"
|
:from-project-id="fromProjectId"
|
||||||
:to-project-id="toProjectId"
|
:to-project-id="toProjectId"
|
||||||
:is-giver-locked="isGiverLocked"
|
|
||||||
:is-recipient-locked="isRecipientLocked"
|
|
||||||
:on-update-description="(desc: string) => (description = desc)"
|
:on-update-description="(desc: string) => (description = desc)"
|
||||||
:on-update-amount="handleAmountUpdate"
|
:on-update-amount="handleAmountUpdate"
|
||||||
:on-update-unit-code="(code: string) => (unitCode = code)"
|
:on-update-unit-code="(code: string) => (unitCode = code)"
|
||||||
@@ -140,23 +138,11 @@ export default class GiftedDialog extends Vue {
|
|||||||
stepType = "giver";
|
stepType = "giver";
|
||||||
unitCode = "HUR";
|
unitCode = "HUR";
|
||||||
visible = false;
|
visible = false;
|
||||||
isGiverLocked = false;
|
|
||||||
isRecipientLocked = false;
|
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
didInfo = didInfo;
|
didInfo = didInfo;
|
||||||
|
|
||||||
// Computed property to help debug template logic
|
|
||||||
get shouldShowProjects() {
|
|
||||||
const result =
|
|
||||||
(this.stepType === "giver" &&
|
|
||||||
this.currentGiverEntityType === "project") ||
|
|
||||||
(this.stepType === "recipient" &&
|
|
||||||
this.currentRecipientEntityType === "project");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property to check if current selection would create a conflict
|
// Computed property to check if current selection would create a conflict
|
||||||
get hasPersonConflict() {
|
get hasPersonConflict() {
|
||||||
// Only check for conflicts when both entities are persons
|
// Only check for conflicts when both entities are persons
|
||||||
@@ -265,40 +251,13 @@ export default class GiftedDialog extends Vue {
|
|||||||
const activeIdentity = await (this as any).$getActiveIdentity();
|
const activeIdentity = await (this as any).$getActiveIdentity();
|
||||||
this.activeDid = activeIdentity.activeDid || "";
|
this.activeDid = activeIdentity.activeDid || "";
|
||||||
|
|
||||||
// Determine if entities should be locked
|
// Skip Step 1 if both giver and receiver are provided
|
||||||
// An entity is locked if it's provided as an input property (has did or handleId)
|
const hasGiver = giver && (!!giver.did || !!giver.handleId);
|
||||||
// For persons: locked if did is provided and not "You" (activeDid)
|
const hasReceiver = receiver && (!!receiver.did || !!receiver.handleId);
|
||||||
// For projects: locked if handleId is provided
|
this.firstStep = !hasGiver || !hasReceiver;
|
||||||
// When entities come from ContactsView, ContactGiftingView, or ProjectViewView context,
|
if (this.firstStep) {
|
||||||
// they should be locked if they have a valid identifier (did or handleId)
|
this.stepType = giver ? "receiver" : "giver";
|
||||||
const isGiverProvided =
|
}
|
||||||
giver &&
|
|
||||||
((giver.did && giver.did !== this.activeDid) ||
|
|
||||||
(giver.handleId && giver.handleId !== ""));
|
|
||||||
const isReceiverProvided =
|
|
||||||
receiver &&
|
|
||||||
((receiver.did && receiver.did !== this.activeDid) ||
|
|
||||||
(receiver.handleId && receiver.handleId !== ""));
|
|
||||||
|
|
||||||
// Lock entities that are provided (from context or explicitly set)
|
|
||||||
// This ensures that when entities are chosen from ContactsView, ContactGiftingView,
|
|
||||||
// or ProjectViewView, the other entity (giver or recipient) that was already set
|
|
||||||
// from context is locked
|
|
||||||
this.isGiverLocked = !!isGiverProvided;
|
|
||||||
this.isRecipientLocked = !!isReceiverProvided;
|
|
||||||
|
|
||||||
// Determine if receiver should be locked (for step navigation logic)
|
|
||||||
// Receiver is locked only if it's provided AND it's not "You" (activeDid)
|
|
||||||
// "You" is treated as a default that can be changed
|
|
||||||
const isReceiverLocked =
|
|
||||||
receiver && receiver.did && receiver.did !== this.activeDid;
|
|
||||||
|
|
||||||
// Only skip Step 1 if both giver and receiver are provided AND receiver is locked
|
|
||||||
// If receiver is "You" (default), still show Step 1 so user can change it
|
|
||||||
this.firstStep = !(giver && isReceiverLocked);
|
|
||||||
// If giver is provided but receiver is not locked, start with recipient selection
|
|
||||||
// Otherwise, start with giver selection
|
|
||||||
this.stepType = giver && !isReceiverLocked ? "recipient" : "giver";
|
|
||||||
|
|
||||||
logger.debug("[GiftedDialog] Settings received:", {
|
logger.debug("[GiftedDialog] Settings received:", {
|
||||||
activeDid: this.activeDid,
|
activeDid: this.activeDid,
|
||||||
@@ -357,9 +316,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.firstStep = true;
|
this.firstStep = true;
|
||||||
// Reset to initial prop values
|
// Reset to initial prop values
|
||||||
this.currentGiverEntityType = this.initialGiverEntityType;
|
this.currentGiverEntityType = this.initialGiverEntityType;
|
||||||
// Reset lock states
|
|
||||||
this.isGiverLocked = false;
|
|
||||||
this.isRecipientLocked = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
@@ -692,13 +648,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
entityType: string;
|
entityType: string;
|
||||||
currentEntity: { did: string; name: string };
|
currentEntity: { did: string; name: string };
|
||||||
}) {
|
}) {
|
||||||
// Prevent editing if the entity is locked
|
// Always allow editing - go back to Step 1 to select a new entity
|
||||||
if (data.entityType === "giver" && this.isGiverLocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.entityType === "recipient" && this.isRecipientLocked) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.goBackToStep1(data.entityType);
|
this.goBackToStep1(data.entityType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1088,7 +1088,7 @@ export default class ContactsView extends Vue {
|
|||||||
{
|
{
|
||||||
group: "modal",
|
group: "modal",
|
||||||
type: "confirm",
|
type: "confirm",
|
||||||
title: "Delete",
|
title: "Confirm First?",
|
||||||
text: message,
|
text: message,
|
||||||
onNo: async () => {
|
onNo: async () => {
|
||||||
this.showGiftedDialog(giverDid, recipientDid);
|
this.showGiftedDialog(giverDid, recipientDid);
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
get recipientAssignmentLabel() {
|
get recipientAssignmentLabel() {
|
||||||
return this.recipientDid
|
return this.recipientDid
|
||||||
? `This is offered to ${this.recipientName}`
|
? `This is offered to ${this.recipientName}`
|
||||||
: "No recipient was chosen.";
|
: "No named individual recipient was chosen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -282,9 +282,9 @@ test('Check User 0 can register a random person', async ({ page }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Could not force close dialog, continuing...');
|
console.log('Could not force close dialog, continuing...');
|
||||||
}
|
}
|
||||||
// Wait for Person button to be ready - simplified approach
|
// Wait for Thank button to be ready - simplified approach
|
||||||
await page.waitForSelector('button:has-text("Person")', { timeout: 10000 });
|
await page.waitForSelector('button:has-text("Thank")', { timeout: 10000 });
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
await page.getByRole('button', { name: 'Thank' }).click();
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||||
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ test('Record something given', async ({ page }) => {
|
|||||||
return !document.querySelector('.dialog-overlay');
|
return !document.querySelector('.dialog-overlay');
|
||||||
}, { timeout: 5000 });
|
}, { timeout: 5000 });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
await page.getByRole('button', { name: 'Thank' }).click();
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ test('Record 9 new gifts', async ({ page }) => {
|
|||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
}
|
}
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
await page.getByRole('button', { name: 'Thank' }).click();
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||||
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
|
await page.getByPlaceholder('What was given').fill(finalTitles[i]);
|
||||||
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
|
await page.getByRole('spinbutton').fill(finalNumbers[i].toString());
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { test, expect, Page } from '@playwright/test';
|
import { test, expect, Page } from '@playwright/test';
|
||||||
import { importUser } from './testUtils';
|
import { importUser } from './testUtils';
|
||||||
|
|
||||||
async function testProjectGive(page: Page, selector: string) {
|
async function testProjectGive(page: Page, isToProject: boolean) {
|
||||||
|
const selector = isToProject ? 'gives-to' : 'gives-from';
|
||||||
|
|
||||||
// Generate a random string of a few characters
|
// Generate a random string of a few characters
|
||||||
const randomString = Math.random().toString(36).substring(2, 6);
|
const randomString = Math.random().toString(36).substring(2, 6);
|
||||||
@@ -42,9 +43,9 @@ async function testProjectGive(page: Page, selector: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test('Record a give to a project', async ({ page }) => {
|
test('Record a give to a project', async ({ page }) => {
|
||||||
await testProjectGive(page, 'gives-to');
|
await testProjectGive(page, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Record a give from a project', async ({ page }) => {
|
test('Record a give from a project', async ({ page }) => {
|
||||||
await testProjectGive(page, 'gives-from');
|
await testProjectGive(page, false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
// Confirm that home shows contact in "Record Something…"
|
// Confirm that home shows contact in "Record Something…"
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
await page.getByRole('button', { name: 'Thank' }).click();
|
||||||
await expect(page.locator('#sectionGiftedGiver').getByRole('listitem').filter({ hasText: contactName })).toBeVisible();
|
await expect(page.locator('#sectionGiftedGiver').getByRole('listitem').filter({ hasText: contactName })).toBeVisible();
|
||||||
|
|
||||||
// Record something given by new contact
|
// Record something given by new contact
|
||||||
|
|||||||
Reference in New Issue
Block a user