Compare commits

...

7 Commits

  1. 10
      CHANGELOG.md
  2. 6
      README.md
  3. 4
      package-lock.json
  4. 4
      package.json
  5. 81
      src/components/MembersList.vue
  6. 19
      src/components/UserNameDialog.vue
  7. 1
      src/views/ClaimView.vue
  8. 112
      src/views/NewEditProjectView.vue
  9. 158
      src/views/OnboardMeetingMembersView.vue
  10. 9
      src/views/OnboardMeetingSetupView.vue
  11. 15
      src/views/ProjectViewView.vue
  12. 1
      test-playwright/20-create-project.spec.ts
  13. 2
      test-playwright/25-create-project-x10.spec.ts
  14. 2
      test-playwright/50-record-offer.spec.ts

10
CHANGELOG.md

@ -6,6 +6,16 @@ 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.3.57] - 2025.02.11
### Added
- Automatic user creation in onboarding meetings
## [0.3.55] - 2025.02.07
### Added
- End time for projects
## [0.3.54] - 2025.02.06 ## [0.3.54] - 2025.02.06
### Added ### Added
- Group onboarding meetings - Group onboarding meetings

6
README.md

@ -50,7 +50,7 @@ Look below for the "test-all" instructions.
* Put the commit hash in the changelog (which will help you remember to bump the version later). * Put the commit hash in the changelog (which will help you remember to bump the version later).
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.36` && `git push origin 0.3.36`. * Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
@ -70,11 +70,11 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* `pkgx +npm sh` * `pkgx +npm sh`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.36 && npm install && npm run build && cd -` * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
(The plain `npm run build` uses the .env.production file.) (The plain `npm run build` uses the .env.production file.)
* Back up the time-safari/dist folder, then `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/` * Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
* 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.

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.54", "version": "0.3.57",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.54", "version": "0.3.57",
"dependencies": { "dependencies": {
"@capacitor/android": "^6.1.2", "@capacitor/android": "^6.1.2",
"@capacitor/cli": "^6.1.2", "@capacitor/cli": "^6.1.2",

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "TimeSafari", "name": "TimeSafari",
"version": "0.3.54", "version": "0.3.57",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"serve": "vite preview", "serve": "vite preview",
@ -58,7 +58,7 @@
"lru-cache": "^10.2.0", "lru-cache": "^10.2.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"nostr-tools": "^2.7.2", "nostr-tools": "^2.10.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",

81
src/components/MembersList.vue

@ -1,40 +1,41 @@
<template> <template>
<div class="space-y-4"> <div class="space-y-4">
<!-- Loading State --> <!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8"> <div
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
>
<fa icon="spinner" class="fa-spin-pulse" /> <fa icon="spinner" class="fa-spin-pulse" />
</div> </div>
<!-- Members List --> <!-- Members List -->
<p <div v-else>
v-if="decryptedMembers.length < members.length" <div class="text-center text-red-600 py-4">
class="text-center text-red-600 py-4" {{ decryptionErrorMessage() }}
> </div>
{{
decryptFailureMessage ||
"Your password failed. Please go back and try again."
}}
</p>
<div v-else class="space-y-4">
<div v-if="missingMyself" class="py-4 text-red-600"> <div v-if="missingMyself" class="py-4 text-red-600">
You are not admitted. The organizer will admit you. You are not currently admitted by the organizer.
</div>
<div v-if="!firstName" class="py-4 text-red-600">
Your name is not set, so others may not recognize you. Reload this page
to set it.
</div> </div>
<div> <div>
<span <span
v-if="showOrganizerTools && isOrganizer" v-if="membersToShow().length > 0 && showOrganizerTools && isOrganizer"
class="inline-flex items-center flex-wrap" class="inline-flex items-center flex-wrap"
> >
<span class="inline-flex items-center"> <span class="inline-flex items-center">
Use &bull; Click
<span <span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600" class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
> >
<fa icon="plus" class="text-sm" /> <fa icon="plus" class="text-sm" />
</span> </span>
and /
<span <span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600" class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
> >
@ -45,8 +46,11 @@
</span> </span>
</div> </div>
<div> <div>
<span class="inline-flex items-center"> <span
Use v-if="membersToShow().length > 0"
class="inline-flex items-center"
>
&bull; Click
<span <span
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600" class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
> >
@ -56,7 +60,8 @@
</span> </span>
</div> </div>
<div v-if="members.length > 0" class="flex justify-center"> <div class="flex justify-center">
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
<button <button
@click="fetchMembers" @click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors" class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
@ -68,7 +73,7 @@
<div <div
v-for="member in membersToShow()" v-for="member in membersToShow()"
:key="member.member.memberId" :key="member.member.memberId"
class="p-4 bg-gray-50 rounded-lg" class="mt-2 p-4 bg-gray-50 rounded-lg"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
@ -131,7 +136,7 @@
{{ member.did }} {{ member.did }}
</p> </p>
</div> </div>
<div v-if="members.length > 0" class="flex justify-center mt-4"> <div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
<button <button
@click="fetchMembers" @click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors" class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
@ -187,16 +192,15 @@ export default class MembersList extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
@Prop({ required: true }) password!: string; @Prop({ required: true }) password!: string;
@Prop({ default: "Your password failed. Please go back and try again." })
decryptFailureMessage!: string;
@Prop({ default: false }) showOrganizerTools!: boolean; @Prop({ default: false }) showOrganizerTools!: boolean;
decryptedMembers: DecryptedMember[] = []; decryptedMembers: DecryptedMember[] = [];
missingPassword = false; firstName = "";
missingMyself = false; isLoading = true;
isLoading = false;
isOrganizer = false; isOrganizer = false;
members: Member[] = []; members: Member[] = [];
missingPassword = false;
missingMyself = false;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
@ -205,13 +209,14 @@ export default class MembersList extends Vue {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
await this.fetchMembers(); await this.fetchMembers();
await this.loadContacts(); await this.loadContacts();
} }
async fetchMembers() { async fetchMembers() {
this.isLoading = true;
try { try {
this.isLoading = true;
const headers = await getHeaders(this.activeDid); const headers = await getHeaders(this.activeDid);
const response = await this.axios.get( const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMembers`, `${this.apiServer}/api/partner/groupOnboardMembers`,
@ -274,6 +279,28 @@ export default class MembersList extends Vue {
this.missingMyself = !foundMyself; this.missingMyself = !foundMyself;
} }
decryptionErrorMessage(): string {
if (this.isOrganizer) {
if (this.decryptedMembers.length < this.members.length) {
return "Some members have data that cannot be decrypted with that password.";
} else {
// the lists must be equal
return "";
}
} else {
// non-organizers should only see problems if the first (organizer) member is not decrypted
if (
this.decryptedMembers.length === 0 ||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
) {
return "Your password is not the same as the organizer. Reload or have them check their password.";
} else {
// the first (organizer) member was decrypted OK
return "";
}
}
}
membersToShow(): DecryptedMember[] { membersToShow(): DecryptedMember[] {
if (this.isOrganizer) { if (this.isOrganizer) {
if (this.showOrganizerTools) { if (this.showOrganizerTools) {
@ -356,7 +383,7 @@ export default class MembersList extends Vue {
group: "modal", group: "modal",
type: "confirm", type: "confirm",
title: "Continue Without Adding?", title: "Continue Without Adding?",
text: "Are you sure you want to proceed with admission even though they are not a contact?", text: "Are you sure you want to proceed with admission? If they are not a contact, you will not know their name after this meeting.",
yesText: "Continue", yesText: "Continue",
onYes: async () => { onYes: async () => {
await this.toggleAdmission(decrMember); await this.toggleAdmission(decrMember);

19
src/components/UserNameDialog.vue

@ -4,8 +4,7 @@
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1> <h1 class="text-xl font-bold text-center mb-4">Set Your Name</h1>
This is not sent to servers. It is only shared with people when you send {{ sharingExplanation }}
it to them.
<input <input
type="text" type="text"
placeholder="Name" placeholder="Name"
@ -36,7 +35,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "@/db/index"; import { db, retrieveSettingsForActiveAccount } from "@/db/index";
@ -46,14 +45,21 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
export default class UserNameDialog extends Vue { export default class UserNameDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
callback: (name: string) => void = () => {}; @Prop({
default:
"This is not sent to servers. It is only shared with people when you send it to them.",
})
sharingExplanation!: string;
@Prop({ default: false }) callbackOnCancel!: boolean;
callback: (name?: string) => void = () => {};
givenName = ""; givenName = "";
visible = false; visible = false;
/** /**
* @param aCallback - callback function for name, which may be "" * @param aCallback - callback function for name, which may be ""
*/ */
async open(aCallback?: (name: string) => void) { async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback; this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.givenName = settings.firstName || ""; this.givenName = settings.firstName || "";
@ -70,6 +76,9 @@ export default class UserNameDialog extends Vue {
onClickCancel() { onClickCancel() {
this.visible = false; this.visible = false;
if (this.callbackOnCancel) {
this.callback();
}
} }
} }
</script> </script>

1
src/views/ClaimView.vue

@ -74,6 +74,7 @@
</div> </div>
<div> <div>
<fa icon="calendar" class="fa-fw text-slate-400" /> <fa icon="calendar" class="fa-fw text-slate-400" />
Recorded
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }} {{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div> </div>
<div v-if="veriClaim.claim.image" class="flex justify-center"> <div v-if="veriClaim.claim.image" class="flex justify-center">

112
src/views/NewEditProjectView.vue

@ -71,17 +71,17 @@
<textarea <textarea
placeholder="Description" placeholder="Description"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 px-3 py-2"
rows="5" rows="5"
v-model="fullClaim.description" v-model="fullClaim.description"
maxlength="5000" maxlength="5000"
></textarea> ></textarea>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic">
If you want to be contacted, be sure to include your contact information If you want to be contacted, be sure to include your contact information
-- just remember that this information is public and saved in a public -- just remember that this information is public and saved in a public
history. history.
</div> </div>
<div class="text-xs text-slate-500 italic -mt-3 mb-4"> <div class="text-xs text-slate-500 italic">
{{ fullClaim.description?.length }}/5000 max. characters {{ fullClaim.description?.length }}/5000 max. characters
</div> </div>
@ -89,28 +89,55 @@
v-model="fullClaim.url" v-model="fullClaim.url"
placeholder="Website" placeholder="Website"
autocapitalize="none" autocapitalize="none"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mt-4 px-3 py-2"
/> />
<div class="flex mb-4 columns-3 w-full"> <div>
<input <div class="flex items-center mt-4">
v-model="startDateInput" <span class="mr-2">Starts At</span>
placeholder="Start Date" <input
type="date" v-model="startDateInput"
class="col-span-1 w-full rounded border border-slate-400 px-3 py-2" placeholder="Start Date"
/> type="date"
<input class="rounded border border-slate-400 px-3 py-2"
:disabled="!startDateInput" />
placeholder="Start Time" <input
v-model="startTimeInput" :disabled="!startDateInput"
type="time" placeholder="Start Time"
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2" v-model="startTimeInput"
/> type="time"
<span class="col-span-1 w-full flex justify-center">{{ zoneName }}</span> class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
</div>
<div class="flex w-full justify-end items-center">
<span class="w-full flex justify-end items-center">
{{ zoneName }} time zone
</span>
</div>
<div class="flex items-center">
<div class="mr-2">
<span>Ends at</span>
</div>
<input
v-model="endDateInput"
placeholder="End Date"
type="date"
class="ml-2 rounded border border-slate-400 px-3 py-2"
/>
<input
:disabled="!endDateInput"
placeholder="End Time"
v-model="endTimeInput"
type="time"
class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
</div>
</div> </div>
<div <div
class="flex items-center mb-4" class="flex items-center mt-4"
@click="includeLocation = !includeLocation" @click="includeLocation = !includeLocation"
> >
<input type="checkbox" class="mr-2" v-model="includeLocation" /> <input type="checkbox" class="mr-2" v-model="includeLocation" />
@ -203,17 +230,7 @@ import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios"; import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { hexToBytes } from "@noble/hashes/utils"; import { hexToBytes } from "@noble/hashes/utils";
// these core imports could also be included as "import type ..." import { finalizeEvent, serializeEvent } from "nostr-tools";
import {
EventTemplate,
UnsignedEvent,
VerifiedEvent,
} from "nostr-tools/lib/types/core";
import {
accountFromExtendedKey,
extendedKeysFromSeedWords,
} from "nostr-tools/nip06";
import { finalizeEvent, serializeEvent } from "nostr-tools/pure";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@ -251,6 +268,8 @@ export default class NewEditProjectView extends Vue {
activeDid = ""; activeDid = "";
agentDid = ""; agentDid = "";
apiServer = ""; apiServer = "";
endDateInput?: string;
endTimeInput?: string;
errorMessage = ""; errorMessage = "";
fullClaim: PlanVerifiableCredential = { fullClaim: PlanVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
@ -325,6 +344,13 @@ export default class NewEditProjectView extends Vue {
this.startDateInput = localDateTime.toFormat("yyyy-MM-dd"); this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
this.startTimeInput = localDateTime.toFormat("HH:mm"); this.startTimeInput = localDateTime.toFormat("HH:mm");
} }
if (this.fullClaim.endTime) {
const localDateTime = DateTime.fromISO(
this.fullClaim.endTime as string,
).toLocal();
this.endDateInput = localDateTime.toFormat("yyyy-MM-dd");
this.endTimeInput = localDateTime.toFormat("HH:mm");
}
} }
} catch (error) { } catch (error) {
console.error("Got error retrieving that project", error); console.error("Got error retrieving that project", error);
@ -460,7 +486,7 @@ export default class NewEditProjectView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Date Error", title: "Date Error",
text: "The date was invalid so it was not set.", text: "The start date was invalid so it was not set.",
}, },
5000, 5000,
); );
@ -468,6 +494,28 @@ export default class NewEditProjectView extends Vue {
} else { } else {
delete vcClaim.startTime; delete vcClaim.startTime;
} }
if (this.endDateInput) {
try {
const endTimeFull = this.endTimeInput || "23:59:59";
const fullTimeString = this.endDateInput + " " + endTimeFull;
// throw an error on an invalid date or time string
vcClaim.endTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
} catch {
// it's not a valid date so erase it and tell the user
delete vcClaim.endTime;
this.$notify(
{
group: "alert",
type: "danger",
title: "Date Error",
text: "The end date was invalid so it was not set.",
},
5000,
);
}
} else {
delete vcClaim.endTime;
}
const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim); const vcJwt = await createEndorserJwtVcFromClaim(this.activeDid, vcClaim);
// Make the xhr request payload // Make the xhr request payload

158
src/views/OnboardMeetingMembersView.vue

@ -8,8 +8,16 @@
Meeting Members Meeting Members
</h1> </h1>
<!-- Loading Animation -->
<div
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
v-if="isLoading"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Error State --> <!-- Error State -->
<div v-if="errorMessage"> <div v-else-if="errorMessage">
<div class="text-center text-red-600 py-8"> <div class="text-center text-red-600 py-8">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
@ -19,13 +27,14 @@
</div> </div>
<!-- Members List --> <!-- Members List -->
<MembersList <MembersList v-else :password="password" @error="handleError" />
v-else
:password="password"
:decrypt-failure-message="'That password failed. You may be in the wrong meeting. Go back and try again.'"
@error="handleError"
/>
</section> </section>
<UserNameDialog
ref="userNameDialog"
:callback-on-cancel="true"
sharing-explanation="This is encrypted and shared only with people in this meeting."
/>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -35,16 +44,35 @@ import { RouteLocation } from "vue-router";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue"; import TopMessage from "@/components/TopMessage.vue";
import MembersList from "@/components/MembersList.vue"; import MembersList from "@/components/MembersList.vue";
import UserNameDialog from "@/components/UserNameDialog.vue";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "@/db/index";
import { encryptMessage } from "@/libs/crypto";
import {
errorStringForLog,
getHeaders,
serverMessageForUser,
} from "@/libs/endorserServer";
import { generateSaveAndActivateIdentity } from "@/libs/util";
@Component({ @Component({
components: { components: {
QuickNav, QuickNav,
TopMessage, TopMessage,
MembersList, MembersList,
UserNameDialog,
}, },
}) })
export default class OnboardMeetingMembersView extends Vue { export default class OnboardMeetingMembersView extends Vue {
activeDid = "";
apiServer = "";
errorMessage = ""; errorMessage = "";
firstName = "";
isRegistered = false;
isLoading = true;
$refs!: {
userNameDialog: InstanceType<typeof UserNameDialog>;
};
get groupId(): string { get groupId(): string {
return (this.$route as RouteLocation).params.groupId as string; return (this.$route as RouteLocation).params.groupId as string;
@ -63,6 +91,122 @@ export default class OnboardMeetingMembersView extends Vue {
this.errorMessage = "The password is missing. Go back and try again."; this.errorMessage = "The password is missing. Go back and try again.";
return; return;
} }
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
this.isRegistered = settings.isRegistered || false;
try {
if (!this.activeDid) {
this.activeDid = await generateSaveAndActivateIdentity();
this.isRegistered = false;
}
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ headers },
);
const member = response.data?.data;
if (!member) {
if (!this.firstName) {
this.$refs.userNameDialog.open(this.addMemberToMeeting);
// addMemberToMeeting sets isLoading to false
} else {
await this.addMemberToMeeting(this.firstName);
// addMemberToMeeting sets isLoading to false
}
} else if (String(member.groupId) !== this.groupId) {
this.errorMessage =
"You are already in a different meeting. Reload or go back and try again.";
this.isLoading = false;
} else {
// must be already in the right meeting
if (!this.firstName) {
this.$refs.userNameDialog.open(this.updateMemberInMeeting);
// updateMemberInMeeting sets isLoading to false
} else {
await this.updateMemberInMeeting(this.firstName);
// updateMemberInMeeting sets isLoading to false
}
}
} catch (error) {
this.errorMessage =
serverMessageForUser(error) ||
"There was an error checking for that meeting. Reload or go back and try again.";
logConsoleAndDb(
"Error checking meeting: " + errorStringForLog(error),
true,
);
this.isLoading = false;
}
}
async addMemberToMeeting(name?: string) {
if (name != null) {
this.firstName = name;
}
const memberData = {
name: this.firstName,
did: this.activeDid,
isRegistered: this.isRegistered,
};
const memberDataString = JSON.stringify(memberData);
const encryptedMemberData = await encryptMessage(
memberDataString,
this.password,
);
const headers = await getHeaders(this.activeDid);
try {
await this.axios.post(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ groupId: this.groupId, content: encryptedMemberData },
{ headers },
);
} catch (error) {
logConsoleAndDb(
"Error adding member to meeting: " + errorStringForLog(error),
true,
);
this.errorMessage =
serverMessageForUser(error) ||
"You're not in a meeting and couldn't be added to this one. Reload or go back and try again.";
}
this.isLoading = false;
}
async updateMemberInMeeting(name?: string) {
if (name != null) {
this.firstName = name;
}
const memberData = {
name: this.firstName,
did: this.activeDid,
isRegistered: this.isRegistered,
};
const memberDataString = JSON.stringify(memberData);
const encryptedMemberData = await encryptMessage(
memberDataString,
this.password,
);
const headers = await getHeaders(this.activeDid);
try {
await this.axios.put(
`${this.apiServer}/api/partner/groupOnboardMember`,
{ content: encryptedMemberData },
{ headers },
);
} catch (error) {
logConsoleAndDb(
"Error updating member in meeting: " + errorStringForLog(error),
true,
);
this.errorMessage =
serverMessageForUser(error) ||
"There was an error updating your name. Reload or go back and try again.";
}
this.isLoading = false;
} }
handleError(message: string) { handleError(message: string) {

9
src/views/OnboardMeetingSetupView.vue

@ -192,23 +192,23 @@
<!-- Members Section --> <!-- Members Section -->
<div <div
v-if="!isLoading && currentMeeting != null" v-if="!isLoading && currentMeeting != null && !!currentMeeting.password"
class="mt-8 p-4 border rounded-lg bg-white shadow" class="mt-8 p-4 border rounded-lg bg-white shadow"
> >
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-2xl">Meeting Members</h2> <h2 class="text-2xl">Meeting Members</h2>
</div> </div>
<router-link <router-link
v-if="!!currentMeeting.password"
:to="onboardMeetingMembersLink()" :to="onboardMeetingMembersLink()"
class="inline-block text-blue-600" class="inline-block text-blue-600"
target="_blank" target="_blank"
> >
Open shortcut page for members <fa icon="external-link" /> &bull; Open shortcut page for members <fa icon="external-link" />
</router-link> </router-link>
<MembersList <MembersList
:password="currentMeeting.password || ''" :password="currentMeeting.password || ''"
:decrypt-failure-message="DECRYPT_FAILURE_MESSAGE"
:show-organizer-tools="true" :show-organizer-tools="true"
@error="handleMembersError" @error="handleMembersError"
class="mt-4" class="mt-4"
@ -264,9 +264,6 @@ export default class OnboardMeetingView extends Vue {
timeout?: number, timeout?: number,
) => void; ) => void;
DECRYPT_FAILURE_MESSAGE =
"Unable to decrypt some member information. Check your password, or have them reset theirs if they don't show here.";
currentMeeting: ServerMeeting | null = null; currentMeeting: ServerMeeting | null = null;
newOrUpdatedMeeting: MeetingSetupInfo | null = null; newOrUpdatedMeeting: MeetingSetupInfo | null = null;
activeDid = ""; activeDid = "";

15
src/views/ProjectViewView.vue

@ -69,7 +69,11 @@
</div> </div>
<div v-if="startTime"> <div v-if="startTime">
<fa icon="calendar" class="fa-fw text-slate-400"></fa> <fa icon="calendar" class="fa-fw text-slate-400"></fa>
{{ startTime }} Starts {{ startTime }}
</div>
<div v-if="endTime">
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
Ends {{ endTime }}
</div> </div>
<div v-if="latitude || longitude"> <div v-if="latitude || longitude">
<fa icon="location-dot" class="fa-fw text-slate-400"></fa> <fa icon="location-dot" class="fa-fw text-slate-400"></fa>
@ -541,6 +545,7 @@ export default class ProjectViewView extends Vue {
apiServer = ""; apiServer = "";
checkingConfirmationForJwtId = ""; checkingConfirmationForJwtId = "";
description = ""; description = "";
endTime = "";
expanded = false; expanded = false;
fulfilledByThis: PlanSummaryRecord | null = null; fulfilledByThis: PlanSummaryRecord | null = null;
fulfillersToThis: Array<PlanSummaryRecord> = []; fulfillersToThis: Array<PlanSummaryRecord> = [];
@ -641,6 +646,14 @@ export default class ProjectViewView extends Vue {
" " + " " +
startDateTime.toLocaleTimeString(); startDateTime.toLocaleTimeString();
} }
const endTime = resp.data.claim?.endTime;
if (endTime != null) {
const endDateTime = new Date(endTime);
this.endTime =
endDateTime.toLocaleDateString() +
" " +
endDateTime.toLocaleTimeString();
}
this.agentDid = resp.data.claim?.agent?.identifier; this.agentDid = resp.data.claim?.agent?.identifier;
this.agentDidVisibleToDids = this.agentDidVisibleToDids =
resp.data.claim?.agent?.identifierVisibleToDids || []; resp.data.claim?.agent?.identifierVisibleToDids || [];

1
test-playwright/20-create-project.spec.ts

@ -2,7 +2,6 @@ import { test, expect } from '@playwright/test';
import { importUser } from './testUtils'; import { importUser } from './testUtils';
test('Create new project, then search for it', async ({ page }) => { test('Create new project, then search for it', async ({ page }) => {
test.slow();
// Generate a random string of 16 characters // Generate a random string of 16 characters
let randomString = Math.random().toString(36).substring(2, 18); let randomString = Math.random().toString(36).substring(2, 18);

2
test-playwright/25-create-project-x10.spec.ts

@ -2,7 +2,7 @@ import { test, expect } from '@playwright/test';
import { importUser, createUniqueStringsArray } from './testUtils'; import { importUser, createUniqueStringsArray } from './testUtils';
test('Create 10 new projects', async ({ page }) => { test('Create 10 new projects', async ({ page }) => {
test.setTimeout(40000); // Set timeout longer since it often fails at 30 seconds test.slow(); // Set timeout longer since it often fails at 30 seconds
const projectCount = 10; const projectCount = 10;

2
test-playwright/50-record-offer.spec.ts

@ -2,6 +2,8 @@ import { test, expect } from '@playwright/test';
import { importUser } from './testUtils'; import { importUser } from './testUtils';
test('Record an offer', async ({ page }) => { test('Record an offer', async ({ page }) => {
test.setTimeout(45000);
// Generate a random string of 3 characters, skipping the "0." at the beginning // Generate a random string of 3 characters, skipping the "0." at the beginning
const randomString = Math.random().toString(36).substring(2, 5); const randomString = Math.random().toString(36).substring(2, 5);
// Standard title prefix // Standard title prefix

Loading…
Cancel
Save