Merge branch 'master' into deep_linking
chore: Clean up merge conflicts in documentation files - Resolve merge conflicts in BUILDING.md and CHANGELOG.md - Remove duplicate entries in documentation - Fix formatting inconsistencies from merge - Maintain consistent documentation style - Clean up git diff markers The cleanup improves documentation by: 1. Resolving all merge conflicts 2. Ensuring consistent formatting 3. Removing duplicate content 4. Maintaining documentation standards 5. Preserving change history
This commit is contained in:
@@ -338,11 +338,7 @@ Run local tests:
|
|||||||
npm run test-local
|
npm run test-local
|
||||||
```
|
```
|
||||||
|
|
||||||
Run all tests (includes building):
|
See [TESTING.md](test-playwright/TESTING.md) for more details.
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run test-all
|
|
||||||
```
|
|
||||||
|
|
||||||
## Linting
|
## Linting
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.4.5] - 2025.02.23
|
||||||
|
### Added
|
||||||
|
- Total amounts of gives on project page
|
||||||
|
### Changed in DB or environment
|
||||||
|
- Requires Endorser.ch version 4.2.6+
|
||||||
|
|
||||||
|
|
||||||
## [0.4.4] - 2025.02.17
|
## [0.4.4] - 2025.02.17
|
||||||
|
|
||||||
### Fixed in 0.4.4
|
### Fixed in 0.4.4
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -17,23 +17,15 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use <http://localhost:3000> for local endorser.ch
|
See [BUILDING.md](BUILDING.md) for more details.
|
||||||
|
|
||||||
### Build the test & production app
|
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run serve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint and fix files
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run all UI tests
|
### Run all UI tests
|
||||||
|
|
||||||
Look below for the "test-all" instructions.
|
Look at [BUILDING.md](BUILDING.md) for the "test-all" instructions and [TESTING.md](test-playwright/TESTING.md) for more details.
|
||||||
|
|
||||||
|
|
||||||
### Compile and minify for test & production
|
### Compile and minify for test & production
|
||||||
|
|
||||||
@@ -79,10 +71,17 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
|
|||||||
|
|
||||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
|
||||||
|
|||||||
@@ -355,22 +355,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3>
|
<h3 class="text-lg font-bold mt-4">Given To This Idea</h3>
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0">
|
<div v-if="givesToThis.length === 0" class="text-sm">
|
||||||
(None yet. If you've seen something, say something by clicking a
|
(None yet. If you've seen something, say something by clicking a
|
||||||
contact above.)
|
contact above.)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<div v-else class="mt-1 text-sm">
|
||||||
|
<!-- Totals section -->
|
||||||
|
<div class="mt-1 flex items-center min-h-[1.5rem]">
|
||||||
|
<div v-if="loadingTotals" class="flex-1">
|
||||||
|
<fa icon="spinner" class="fa-spin-pulse text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="givesTotalsByUnit.length > 0" class="flex-1">
|
||||||
|
<span class="font-semibold mr-2 shrink-0">Totals</span>
|
||||||
|
<span class="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
<a
|
||||||
|
@click="totalsExpanded = !totalsExpanded"
|
||||||
|
class="cursor-pointer text-blue-500"
|
||||||
|
>
|
||||||
|
<!-- just show the hours, or alternatively whatever is first -->
|
||||||
|
<span v-if="givenTotalHours() > 0">
|
||||||
|
{{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ givesTotalsByUnit[0].amount }}
|
||||||
|
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }}
|
||||||
|
</span>
|
||||||
|
<span v-if="givesTotalsByUnit.length > 1">...</span>
|
||||||
|
<span>
|
||||||
|
<fa
|
||||||
|
:icon="totalsExpanded ? 'chevron-up' : 'chevron-right'"
|
||||||
|
class="fa-fw text-xs ml-1"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<!-- show the full list when expanded -->
|
||||||
|
<div v-if="totalsExpanded">
|
||||||
|
<div
|
||||||
|
v-for="total in givesTotalsByUnit"
|
||||||
|
:key="total.unit"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
<fa
|
||||||
|
:icon="libsUtil.iconForUnitCode(total.unit)"
|
||||||
|
class="fa-fw text-slate-400 mr-1"
|
||||||
|
/>
|
||||||
|
{{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="font-semibold mr-2 shrink-0">
|
||||||
|
{{ givesToThis.length }}{{ givesHitLimit ? "+" : "" }} record{{
|
||||||
|
givesToThis.length === 1 ? "" : "s"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- List of gives -->
|
||||||
|
<ul class="mt-2 text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
v-for="give in givesToThis"
|
v-for="give in givesToThis"
|
||||||
:key="give.jwtId"
|
:key="give.id"
|
||||||
class="py-1.5 border-b border-slate-300"
|
class="py-1.5 border-b border-slate-300"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between gap-4">
|
<div class="flex justify-between gap-4">
|
||||||
<span>
|
<span>
|
||||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
<fa icon="user" class="fa-fw text-slate-400" />
|
||||||
{{
|
{{
|
||||||
serverUtil.didInfo(
|
serverUtil.didInfo(
|
||||||
give.agentDid,
|
give.agentDid,
|
||||||
@@ -381,26 +436,23 @@
|
|||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="give.amount" class="whitespace-nowrap">
|
<span v-if="give.amount" class="whitespace-nowrap">
|
||||||
<font-awesome
|
<fa
|
||||||
:icon="libsUtil.iconForUnitCode(give.unit)"
|
:icon="libsUtil.iconForUnitCode(give.unit)"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
/>{{ give.amount }}
|
/>{{ give.amount }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-slate-500">
|
<div class="text-slate-500">
|
||||||
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||||
{{ give.issuedAt?.substring(0, 10) }}
|
{{ give.issuedAt?.substring(0, 10) }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="give.description" class="text-slate-500">
|
<div v-if="give.description" class="text-slate-500">
|
||||||
<font-awesome icon="comment" class="fa-fw text-slate-400" />
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
||||||
{{ give.description }}
|
{{ give.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<a @click="onClickLoadClaim(give.jwtId)">
|
<a @click="onClickLoadClaim(give.jwtId)">
|
||||||
<font-awesome
|
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
|
||||||
icon="file-lines"
|
|
||||||
class="text-blue-500 cursor-pointer"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -410,16 +462,16 @@
|
|||||||
"
|
"
|
||||||
@click="deepCheckConfirmable(give)"
|
@click="deepCheckConfirmable(give)"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<fa
|
||||||
icon="circle-check"
|
icon="circle-check"
|
||||||
class="text-blue-500 cursor-pointer"
|
class="text-blue-500 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
|
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
|
||||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
<fa icon="spinner" class="fa-spin-pulse" />
|
||||||
</a>
|
</a>
|
||||||
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
|
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
|
||||||
<font-awesome
|
<fa
|
||||||
icon="circle-check"
|
icon="circle-check"
|
||||||
class="text-slate-500 cursor-pointer"
|
class="text-slate-500 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
@@ -427,11 +479,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<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 :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" />
|
<img
|
||||||
|
:src="give.fullClaim.image"
|
||||||
|
class="h-24 mt-2 rounded-xl"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
<div v-if="givesHitLimit" class="text-center text-blue-500">
|
<div v-if="givesHitLimit" class="text-center text-blue-500">
|
||||||
<button @click="loadGives()">Load More</button>
|
<button @click="loadGives()">Load More</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,7 +702,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
||||||
/** Flag for fulfiller pagination */
|
/** Flag for fulfiller pagination */
|
||||||
fulfillersToHitLimit = false;
|
fulfillersToHitLimit = false;
|
||||||
/** Project image URL */
|
givesToThis: Array<GiveSummaryRecord> = [];
|
||||||
|
givesHitLimit = false;
|
||||||
|
givesProvidedByThis: Array<GiveSummaryRecord> = [];
|
||||||
|
givesProvidedByHitLimit = false;
|
||||||
|
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
|
||||||
imageUrl = "";
|
imageUrl = "";
|
||||||
/** Project issuer DID */
|
/** Project issuer DID */
|
||||||
issuer = "";
|
issuer = "";
|
||||||
@@ -660,6 +720,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
issuerVisibleToDids: Array<string> = [];
|
issuerVisibleToDids: Array<string> = [];
|
||||||
/** Project location data */
|
/** Project location data */
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
|
loadingTotals = false;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
/** Project name */
|
/** Project name */
|
||||||
name = "";
|
name = "";
|
||||||
@@ -689,6 +750,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
checkingConfirmationForJwtId = "";
|
checkingConfirmationForJwtId = "";
|
||||||
/** Recently checked unconfirmable JWTs */
|
/** Recently checked unconfirmable JWTs */
|
||||||
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
||||||
|
startTime = "";
|
||||||
|
totalsExpanded = false;
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
/** Truncation length */
|
/** Truncation length */
|
||||||
truncateLength = 40;
|
truncateLength = 40;
|
||||||
@@ -740,21 +803,26 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.projectId = decodeURIComponent(pathParam);
|
this.projectId = decodeURIComponent(pathParam);
|
||||||
}
|
}
|
||||||
this.loadProject(this.projectId, this.activeDid);
|
this.loadProject(this.projectId, this.activeDid);
|
||||||
|
this.loadTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditClick() {
|
||||||
|
const route = {
|
||||||
|
name: "new-edit-project",
|
||||||
|
query: { projectId: this.projectId },
|
||||||
|
};
|
||||||
|
(this.$router as Router).push(route);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Isn't there a better way to make this available to the template?
|
||||||
|
expandText() {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseText() {
|
||||||
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads project data and related information
|
|
||||||
*
|
|
||||||
* Workflow:
|
|
||||||
* 1. Fetches project details from API
|
|
||||||
* 2. Updates component state with project data
|
|
||||||
* 3. Initializes related data loading (gifts, offers, fulfillments)
|
|
||||||
*
|
|
||||||
* @param projectId Project handle ID
|
|
||||||
* @param userDid Active user's DID
|
|
||||||
* @throws Logs errors and notifies user
|
|
||||||
* @emits Notification on loading errors
|
|
||||||
*/
|
|
||||||
async loadProject(projectId: string, userDid: string) {
|
async loadProject(projectId: string, userDid: string) {
|
||||||
this.projectId = projectId;
|
this.projectId = projectId;
|
||||||
|
|
||||||
@@ -1394,5 +1462,56 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.allMyDids,
|
this.allMyDids,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadTotals() {
|
||||||
|
this.loadingTotals = true;
|
||||||
|
const url =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/givesToPlans?planIds=" +
|
||||||
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
||||||
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(url, { headers });
|
||||||
|
if (resp.status === 200 && resp.data.data) {
|
||||||
|
// Calculate totals by unit
|
||||||
|
const totals: { [key: string]: number } = {};
|
||||||
|
resp.data.data.forEach((give: GiveSummaryRecord) => {
|
||||||
|
const amount = give.fullClaim.object?.amountOfThisGood;
|
||||||
|
const unit = give.fullClaim.object?.unitCode;
|
||||||
|
if (amount && unit) {
|
||||||
|
totals[unit] = (totals[unit] || 0) + amount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert totals object to array format
|
||||||
|
this.givesTotalsByUnit = Object.entries(totals).map(
|
||||||
|
([unit, amount]) => ({
|
||||||
|
unit,
|
||||||
|
amount,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading totals:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to load totals for this project.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.loadingTotals = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
givenTotalHours(): number {
|
||||||
|
return (
|
||||||
|
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
*/
|
*/
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { importUser } from './testUtils';
|
import { importUserAndCloseOnboarding } from './testUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: by default, this test uses the test image API server.
|
* Note: by default, this test uses the test image API server.
|
||||||
@@ -65,7 +65,7 @@ test('Record item given from image-share', async ({ page }) => {
|
|||||||
// Combine title prefix with the random string
|
// Combine title prefix with the random string
|
||||||
const finalTitle = `Gift ${randomString} from image-share`;
|
const finalTitle = `Gift ${randomString} from image-share`;
|
||||||
|
|
||||||
await importUser(page, '00');
|
await importUserAndCloseOnboarding(page, '00');
|
||||||
|
|
||||||
// Record something given
|
// Record something given
|
||||||
await page.goto('./test');
|
await page.goto('./test');
|
||||||
@@ -84,10 +84,8 @@ test('Record item given from image-share', async ({ page }) => {
|
|||||||
await page.getByRole('spinbutton').fill('2');
|
await page.getByRole('spinbutton').fill('2');
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
|
||||||
// we end up on a page with the onboarding info
|
// const recorded = await page.getByText('That gift was recorded.');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await expect(await page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
||||||
|
|
||||||
// Refresh home view and check gift
|
// Refresh home view and check gift
|
||||||
|
|||||||
@@ -207,7 +207,6 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
|||||||
// Add another new contact
|
// Add another new contact
|
||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b, User #222, asdf1234');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"]')).toBeVisible();
|
|
||||||
await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible();
|
await expect(page.locator('div[role="alert"] span:has-text("No")')).toBeVisible();
|
||||||
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register
|
||||||
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ export async function importUser(page: Page, id?: string): Promise<string> {
|
|||||||
return did;
|
return did;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function importUserAndCloseOnboarding(page: Page, id?: string): Promise<string> {
|
||||||
|
const did = await importUser(page, id);
|
||||||
|
await page.goto('./');
|
||||||
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
|
return did;
|
||||||
|
}
|
||||||
|
|
||||||
// This is to switch to someone already in the identity table. It doesn't include registration.
|
// This is to switch to someone already in the identity table. It doesn't include registration.
|
||||||
export async function switchToUser(page: Page, did: string): Promise<void> {
|
export async function switchToUser(page: Page, did: string): Promise<void> {
|
||||||
// This is the direct approach but users have to tap on things so we'll do that instead.
|
// This is the direct approach but users have to tap on things so we'll do that instead.
|
||||||
|
|||||||
Reference in New Issue
Block a user