|
|
|
<template>
|
|
|
|
<QuickNav selected="Projects"></QuickNav>
|
|
|
|
<section id="Content" class="p-6 pb-24">
|
|
|
|
<!-- Heading -->
|
|
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
|
|
Your Plans
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
<!-- Quick Search -->
|
|
|
|
|
|
|
|
<div id="QuickSearch" class="mb-4 flex">
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder="Search…"
|
|
|
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
|
|
|
>
|
|
|
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- New Project -->
|
|
|
|
<button
|
|
|
|
class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full"
|
|
|
|
@click="onClickNewProject()"
|
|
|
|
>
|
|
|
|
<fa icon="plus" class="fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<!-- Loading Animation -->
|
|
|
|
<div
|
|
|
|
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
|
|
|
v-if="isLoading"
|
|
|
|
>
|
|
|
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Results List -->
|
|
|
|
<InfiniteScroll @reached-bottom="loadMoreData">
|
|
|
|
<ul class="border-t border-slate-300">
|
|
|
|
<li
|
|
|
|
class="border-b border-slate-300"
|
|
|
|
v-for="project in projects"
|
|
|
|
:key="project.handleId"
|
|
|
|
>
|
|
|
|
<a
|
|
|
|
@click="onClickLoadProject(project.handleId)"
|
|
|
|
class="block py-4 flex gap-4"
|
|
|
|
>
|
|
|
|
<div class="flex-none w-12">
|
|
|
|
<EntityIcon
|
|
|
|
:entityId="project.handleId"
|
|
|
|
:iconSize="48"
|
|
|
|
class="inline-block align-middle border border-slate-300 rounded-md"
|
|
|
|
></EntityIcon>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="grow overflow-hidden">
|
|
|
|
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
|
|
|
<div class="text-sm truncate">
|
|
|
|
{{ project.description }}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</InfiniteScroll>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
|
|
import { accountsDB, db } from "@/db/index";
|
|
|
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
|
|
import { accessToken } from "@/libs/crypto";
|
|
|
|
import { IIdentifier } from "@veramo/core";
|
|
|
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
|
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
|
|
import EntityIcon from "@/components/EntityIcon.vue";
|
|
|
|
import { ProjectData } from "@/libs/endorserServer";
|
|
|
|
|
|
|
|
interface Notification {
|
|
|
|
group: string;
|
|
|
|
type: string;
|
|
|
|
title: string;
|
|
|
|
text: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: { InfiniteScroll, QuickNav, EntityIcon },
|
|
|
|
})
|
|
|
|
export default class ProjectsView extends Vue {
|
|
|
|
$notify!: (notification: Notification, timeout?: number) => void;
|
|
|
|
|
|
|
|
apiServer = "";
|
|
|
|
projects: ProjectData[] = [];
|
|
|
|
current: IIdentifier;
|
|
|
|
isLoading = false;
|
|
|
|
alertTitle = "";
|
|
|
|
alertMessage = "";
|
|
|
|
numAccounts = 0;
|
|
|
|
|
|
|
|
async beforeCreate() {
|
|
|
|
await accountsDB.open();
|
|
|
|
this.numAccounts = await accountsDB.accounts.count();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Core project data loader
|
|
|
|
* @param url the url used to fetch the data
|
|
|
|
* @param token Authorization token
|
|
|
|
**/
|
|
|
|
async dataLoader(url: string, token: string) {
|
|
|
|
const headers: { [key: string]: string } = {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
Authorization: `Bearer ${token}`,
|
|
|
|
};
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.isLoading = true;
|
|
|
|
const resp = await this.axios.get(url, { headers });
|
|
|
|
if (resp.status === 200 || !resp.data.data) {
|
|
|
|
const plans: ProjectData[] = resp.data.data;
|
|
|
|
for (const plan of plans) {
|
|
|
|
const { name, description, handleId, rowid } = plan;
|
|
|
|
this.projects.push({ name, description, handleId, rowid });
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.log("Bad server response & data:", resp.status, resp.data);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Failed to get projects from the server. Try again later.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
} catch (error: any) {
|
|
|
|
console.error("Got error loading projects:", error.message);
|
|
|
|
const additional = error.message ? " " + error.message : "";
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Got an error loading projects." + additional,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} finally {
|
|
|
|
this.isLoading = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data loader used by infinite scroller
|
|
|
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
|
|
|
**/
|
|
|
|
async loadMoreData(payload: boolean) {
|
|
|
|
if (this.projects.length > 0 && payload) {
|
|
|
|
const latestProject = this.projects[this.projects.length - 1];
|
|
|
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
|
|
|
|
const token = await accessToken(this.current);
|
|
|
|
await this.dataLoader(url, token);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle clicking on a project entry found in the list
|
|
|
|
* @param id of the project
|
|
|
|
**/
|
|
|
|
onClickLoadProject(id: string) {
|
|
|
|
localStorage.setItem("projectId", id);
|
|
|
|
const route = {
|
|
|
|
name: "project",
|
|
|
|
};
|
|
|
|
this.$router.push(route);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load projects initially
|
|
|
|
* @param identity of the user
|
|
|
|
**/
|
|
|
|
async LoadProjects(identity: IIdentifier) {
|
|
|
|
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
|
|
|
|
const token: string = await accessToken(identity);
|
|
|
|
await this.dataLoader(url, token);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getIdentity(activeDid: string) {
|
|
|
|
await accountsDB.open();
|
|
|
|
const account = await accountsDB.accounts
|
|
|
|
.where("did")
|
|
|
|
.equals(activeDid)
|
|
|
|
.first();
|
|
|
|
const identity = JSON.parse(account?.identity || "null");
|
|
|
|
|
|
|
|
if (!identity) {
|
|
|
|
throw new Error(
|
|
|
|
"Attempted to load project records with no identity available.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return identity;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 'created' hook runs when the Vue instance is first created
|
|
|
|
**/
|
|
|
|
async created() {
|
|
|
|
try {
|
|
|
|
await db.open();
|
|
|
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
|
|
|
const activeDid = settings?.activeDid || "";
|
|
|
|
this.apiServer = settings?.apiServer || "";
|
|
|
|
|
|
|
|
if (this.numAccounts === 0) {
|
|
|
|
console.error("No accounts found.");
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "You need an identity to load your projects.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
const identity = await this.getIdentity(activeDid);
|
|
|
|
this.current = identity;
|
|
|
|
this.LoadProjects(identity);
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.log("Error initializing:", err);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Something went wrong loading your projects.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handling clicking on the new project button
|
|
|
|
**/
|
|
|
|
onClickNewProject(): void {
|
|
|
|
localStorage.removeItem("projectId");
|
|
|
|
const route = {
|
|
|
|
name: "new-edit-project",
|
|
|
|
};
|
|
|
|
this.$router.push(route);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|